// Copyright (c) 2016 Andreas Auernhammer. All rights reserved.
// Use of this source code is governed by a license that can be
// found in the LICENSE file.

// Package chacha implements some low-level functions of the
// ChaCha cipher family.
package chacha

import (
	"encoding/binary"
	"errors"
	"math"
)

const (
	// NonceSize is the size of the ChaCha20 nonce in bytes.
	NonceSize = 8

	// INonceSize is the size of the IETF-ChaCha20 nonce in bytes.
	INonceSize = 12

	// XNonceSize is the size of the XChaCha20 nonce in bytes.
	XNonceSize = 24

	// KeySize is the size of the key in bytes.
	KeySize = 32
)

var (
	useSSE2  bool
	useSSSE3 bool
	useAVX   bool
	useAVX2  bool
)

var (
	errKeySize      = errors.New("chacha20/chacha: bad key length")
	errInvalidNonce = errors.New("chacha20/chacha: bad nonce length")
)

func setup(state *[64]byte, nonce, key []byte) (err error) {
	if len(key) != KeySize {
		err = errKeySize
		return
	}
	var Nonce [16]byte
	switch len(nonce) {
	case NonceSize:
		copy(Nonce[8:], nonce)
		initialize(state, key, &Nonce)
	case INonceSize:
		copy(Nonce[4:], nonce)
		initialize(state, key, &Nonce)
	case XNonceSize:
		var tmpKey [32]byte
		var hNonce [16]byte

		copy(hNonce[:], nonce[:16])
		copy(tmpKey[:], key)
		HChaCha20(&tmpKey, &hNonce, &tmpKey)
		copy(Nonce[8:], nonce[16:])
		initialize(state, tmpKey[:], &Nonce)

		// BUG(aead): A "good" compiler will remove this (optimizations)
		//			  But using the provided key instead of tmpKey,
		//			  will change the key (-> probably confuses users)
		for i := range tmpKey {
			tmpKey[i] = 0
		}
	default:
		err = errInvalidNonce
	}
	return
}

// XORKeyStream crypts bytes from src to dst using the given nonce and key.
// The length of the nonce determinds the version of ChaCha20:
// - NonceSize:  ChaCha20/r with a 64 bit nonce and a 2^64 * 64 byte period.
// - INonceSize: ChaCha20/r as defined in RFC 7539 and a 2^32 * 64 byte period.
// - XNonceSize: XChaCha20/r with a 192 bit nonce and a 2^64 * 64 byte period.
// The rounds argument specifies the number of rounds performed for keystream
// generation - valid values are 8, 12 or 20. The src and dst may be the same slice
// but otherwise should not overlap. If len(dst) < len(src) this function panics.
// If the nonce is neither 64, 96 nor 192 bits long, this function panics.
func XORKeyStream(dst, src, nonce, key []byte, rounds int) {
	if rounds != 20 && rounds != 12 && rounds != 8 {
		panic("chacha20/chacha: bad number of rounds")
	}
	if len(dst) < len(src) {
		panic("chacha20/chacha: dst buffer is to small")
	}
	if len(nonce) == INonceSize && uint64(len(src)) > (1<<38) {
		panic("chacha20/chacha: src is too large")
	}

	var block, state [64]byte
	if err := setup(&state, nonce, key); err != nil {
		panic(err)
	}
	xorKeyStream(dst, src, &block, &state, rounds)
}

// Cipher implements ChaCha20/r (XChaCha20/r) for a given number of rounds r.
type Cipher struct {
	state, block [64]byte
	off          int
	rounds       int // 20 for ChaCha20
	noncesize    int

	ignoreCounterOverflow bool
}

// NewCipher returns a new *chacha.Cipher implementing the ChaCha20/r or XChaCha20/r
// (r = 8, 12 or 20) stream cipher. The nonce must be unique for one key for all time.
// The length of the nonce determinds the version of ChaCha20:
// - NonceSize:  ChaCha20/r with a 64 bit nonce and a 2^64 * 64 byte period.
// - INonceSize: ChaCha20/r as defined in RFC 7539 and a 2^32 * 64 byte period.
// - XNonceSize: XChaCha20/r with a 192 bit nonce and a 2^64 * 64 byte period.
// If the nonce is neither 64, 96 nor 192 bits long, a non-nil error is returned.
func NewCipher(nonce, key []byte, rounds int) (*Cipher, error) {
	if rounds != 20 && rounds != 12 && rounds != 8 {
		panic("chacha20/chacha: bad number of rounds")
	}

	c := new(Cipher)
	if err := setup(&(c.state), nonce, key); err != nil {
		return nil, err
	}
	c.rounds = rounds

	if len(nonce) == INonceSize {
		c.noncesize = INonceSize
	} else {
		c.noncesize = NonceSize
	}

	return c, nil
}

// XORKeyStream crypts bytes from src to dst. Src and dst may be the same slice
// but otherwise should not overlap. If len(dst) < len(src) the function panics.
func (c *Cipher) XORKeyStream(dst, src []byte) {
	if len(dst) < len(src) {
		panic("chacha20/chacha: dst buffer is to small")
	}

	if c.off > 0 {
		n := len(c.block[c.off:])
		if len(src) <= n {
			for i, v := range src {
				dst[i] = v ^ c.block[c.off]
				c.off++
			}
			if c.off == 64 {
				c.off = 0
			}
			return
		}

		for i, v := range c.block[c.off:] {
			dst[i] = src[i] ^ v
		}
		src = src[n:]
		dst = dst[n:]
		c.off = 0
	}

	if !c.ignoreCounterOverflow {
		// check for counter overflow
		blocksToXOR := len(src) / 64
		if len(src)%64 != 0 {
			blocksToXOR++
		}
		var overflow bool
		if c.noncesize == INonceSize {
			overflow = binary.LittleEndian.Uint32(c.state[48:]) > math.MaxUint32-uint32(blocksToXOR)
		} else {
			overflow = binary.LittleEndian.Uint64(c.state[48:]) > math.MaxUint64-uint64(blocksToXOR)
		}
		if overflow {
			panic("chacha20/chacha: counter overflow")
		}
	}

	c.off += xorKeyStream(dst, src, &(c.block), &(c.state), c.rounds)
}

// SetCounter skips ctr * 64 byte blocks. SetCounter(0) resets the cipher.
// This function always skips the unused keystream of the current 64 byte block.
func (c *Cipher) SetCounter(ctr uint64) {
	if c.noncesize == INonceSize {
		binary.LittleEndian.PutUint32(c.state[48:], uint32(ctr))
	} else {
		binary.LittleEndian.PutUint64(c.state[48:], ctr)
	}
	c.off = 0
}

// SetIgnoreCounterOverflow ignore the counter-overflow check.
// It's unsafe but useful for compatible
func (c *Cipher) SetIgnoreCounterOverflow(b bool) {
	c.ignoreCounterOverflow = b
}

// HChaCha20 generates 32 pseudo-random bytes from a 128 bit nonce and a 256 bit secret key.
// It can be used as a key-derivation-function (KDF).
func HChaCha20(out *[32]byte, nonce *[16]byte, key *[32]byte) { hChaCha20(out, nonce, key) }
