// SPDX-License-Identifier: BSD-3-Clause

//go:build arm

// Package chacha20poly1305 provides a ChaCha20-Poly1305 AEAD built
// from the hand-ported ChaCha20 and Poly1305 kernels in
// tsasm/arm/chacha20 and tsasm/arm/poly1305. It only builds on
// GOARCH=arm; other architectures should use
// golang.org/x/crypto/chacha20poly1305.
package chacha20poly1305

import (
	"crypto/cipher"
	"crypto/subtle"
	"encoding/binary"
	"errors"

	chacha "github.com/metacubex/tailscale-wireguard-go/tsasm/arm/chacha20"
	"github.com/metacubex/tailscale-wireguard-go/tsasm/arm/poly1305"
)

const (
	KeySize   = 32
	NonceSize = 12
	Overhead  = 16
)

type aead struct {
	key [KeySize]byte
}

// New returns a chacha20poly1305 AEAD with the given 32-byte key.
func New(key []byte) (cipher.AEAD, error) {
	if len(key) != KeySize {
		return nil, errors.New("chacha20poly1305: bad key length")
	}
	a := &aead{}
	copy(a.key[:], key)
	return a, nil
}

func (a *aead) NonceSize() int { return NonceSize }
func (a *aead) Overhead() int  { return Overhead }

func (a *aead) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
	if len(nonce) != NonceSize {
		panic("chacha20poly1305: bad nonce length")
	}
	ret, out := sliceForAppend(dst, len(plaintext)+Overhead)

	var nonceArr [chacha.NonceSize]byte
	copy(nonceArr[:], nonce)

	// Derive the Poly1305 one-time key from the first 32 bytes of the
	// ChaCha20 keystream at counter 0, then encrypt at counter 1.
	var (
		zeros   [64]byte
		polyBuf [64]byte
		polyKey [32]byte
	)
	chacha.XORKeyStream(polyBuf[:], zeros[:], &a.key, &nonceArr, 0)
	copy(polyKey[:], polyBuf[:32])
	chacha.XORKeyStream(out[:len(plaintext)], plaintext, &a.key, &nonceArr, 1)

	tag := computeTag(additionalData, out[:len(plaintext)], &polyKey)
	copy(out[len(plaintext):], tag[:])
	return ret
}

func (a *aead) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
	if len(nonce) != NonceSize {
		return nil, errors.New("chacha20poly1305: bad nonce length")
	}
	if len(ciphertext) < Overhead {
		return nil, errors.New("chacha20poly1305: ciphertext too short")
	}
	ctLen := len(ciphertext) - Overhead
	ct := ciphertext[:ctLen]
	receivedTag := ciphertext[ctLen:]

	var nonceArr [chacha.NonceSize]byte
	copy(nonceArr[:], nonce)

	var (
		zeros   [64]byte
		polyBuf [64]byte
		polyKey [32]byte
	)
	chacha.XORKeyStream(polyBuf[:], zeros[:], &a.key, &nonceArr, 0)
	copy(polyKey[:], polyBuf[:32])

	expectedTag := computeTag(additionalData, ct, &polyKey)
	if subtle.ConstantTimeCompare(receivedTag, expectedTag[:]) != 1 {
		return nil, errors.New("chacha20poly1305: message authentication failed")
	}

	ret, out := sliceForAppend(dst, ctLen)
	chacha.XORKeyStream(out, ct, &a.key, &nonceArr, 1)
	return ret, nil
}

var zeros16 [16]byte

// computeTag streams (aad || pad16 || ct || pad16 || u64le(|aad|) ||
// u64le(|ct|)) through a stack-allocated Poly1305 MAC.
func computeTag(aad, ct []byte, polyKey *[32]byte) [16]byte {
	var mac poly1305.MAC
	mac.Init(polyKey)
	mac.Write(aad)
	if rem := len(aad) % 16; rem != 0 {
		mac.Write(zeros16[:16-rem])
	}
	mac.Write(ct)
	if rem := len(ct) % 16; rem != 0 {
		mac.Write(zeros16[:16-rem])
	}
	var lenBuf [16]byte
	binary.LittleEndian.PutUint64(lenBuf[0:8], uint64(len(aad)))
	binary.LittleEndian.PutUint64(lenBuf[8:16], uint64(len(ct)))
	mac.Write(lenBuf[:])

	var tag [16]byte
	mac.Sum(&tag)
	return tag
}

func sliceForAppend(dst []byte, n int) (head, tail []byte) {
	if total := len(dst) + n; cap(dst) >= total {
		head = dst[:total]
	} else {
		head = make([]byte, total)
		copy(head, dst)
	}
	tail = head[len(dst):]
	return
}
