// Originally written in 2015 by Dmitry Chestnykh.
// Modified in 2017 by Tony Arcieri.
//
// Miscreant implements Synthetic Initialization Vector (SIV)-based
// authenticated encryption using the AES block cipher (RFC 5297).

package miscreant

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/subtle"
	"errors"
	"github.com/miscreant/miscreant.go/block"
	"github.com/miscreant/miscreant.go/cmac"
	"github.com/miscreant/miscreant.go/pmac"
	"hash"
)

// MaxAssociatedDataItems is the maximum number of associated data items
const MaxAssociatedDataItems = 126

var (
	// ErrKeySize indicates the given key size is not supported
	ErrKeySize = errors.New("siv: bad key size")

	// ErrNotAuthentic indicates a ciphertext is malformed or corrupt
	ErrNotAuthentic = errors.New("siv: authentication failed")

	// ErrTooManyAssociatedDataItems indicates more than MaxAssociatedDataItems were given
	ErrTooManyAssociatedDataItems = errors.New("siv: too many associated data items")
)

// Cipher is an instance of AES-SIV, configured with either AES-CMAC or
// AES-PMAC as a message authentication code.
type Cipher struct {
	// MAC function used to derive a synthetic IV and authenticate the message
	h hash.Hash

	// Block cipher function used to encrypt the message
	b cipher.Block

	// Internal buffers
	tmp1, tmp2 block.Block
}

// NewAESCMACSIV returns a new AES-SIV cipher with the given key, which must be
// twice as long as an AES key, either 32 or 64 bytes to select AES-128
// (AES-CMAC-SIV-256), or AES-256 (AES-CMAC-SIV-512).
func NewAESCMACSIV(key []byte) (c *Cipher, err error) {
	n := len(key)
	if n != 32 && n != 64 {
		return nil, ErrKeySize
	}

	macBlock, err := aes.NewCipher(key[:n/2])
	if err != nil {
		return nil, err
	}

	ctrBlock, err := aes.NewCipher(key[n/2:])
	if err != nil {
		return nil, err
	}

	c = new(Cipher)
	c.h = cmac.New(macBlock)
	c.b = ctrBlock

	return c, nil
}

// NewAESPMACSIV returns a new AES-SIV cipher with the given key, which must be
// twice as long as an AES key, either 32 or 64 bytes to select AES-128
// (AES-PMAC-SIV-256), or AES-256 (AES-PMAC-SIV-512).
func NewAESPMACSIV(key []byte) (c *Cipher, err error) {
	n := len(key)
	if n != 32 && n != 64 {
		return nil, ErrKeySize
	}

	macBlock, err := aes.NewCipher(key[:n/2])
	if err != nil {
		return nil, err
	}

	ctrBlock, err := aes.NewCipher(key[n/2:])
	if err != nil {
		return nil, err
	}

	c = new(Cipher)
	c.h = pmac.New(macBlock)
	c.b = ctrBlock

	return c, nil
}

// Overhead returns the difference between plaintext and ciphertext lengths.
func (c *Cipher) Overhead() int {
	return c.h.Size()
}

// Seal encrypts and authenticates plaintext, authenticates the given
// associated data items, and appends the result to dst, returning the updated
// slice.
//
// The plaintext and dst may alias exactly or not at all.
//
// For nonce-based encryption, the nonce should be the last associated data item.
func (c *Cipher) Seal(dst []byte, plaintext []byte, data ...[]byte) ([]byte, error) {
	if len(data) > MaxAssociatedDataItems {
		return nil, ErrTooManyAssociatedDataItems
	}

	// Authenticate
	iv := c.s2v(data, plaintext)
	ret, out := sliceForAppend(dst, len(iv)+len(plaintext))
	copy(out, iv)

	// Encrypt
	zeroIVBits(iv)
	ctr := cipher.NewCTR(c.b, iv)
	ctr.XORKeyStream(out[len(iv):], plaintext)

	return ret, nil
}

// Open decrypts ciphertext, authenticates the decrypted plaintext and the given
// associated data items and, if successful, appends the resulting plaintext
// to dst, returning the updated slice. The additional data items must match the
// items passed to Seal.
//
// The ciphertext and dst may alias exactly or not at all.
//
// For nonce-based encryption, the nonce should be the last associated data item.
func (c *Cipher) Open(dst []byte, ciphertext []byte, data ...[]byte) ([]byte, error) {
	if len(data) > MaxAssociatedDataItems {
		return nil, ErrTooManyAssociatedDataItems
	}
	if len(ciphertext) < c.Overhead() {
		return nil, ErrNotAuthentic
	}

	// Decrypt
	iv := c.tmp1[:c.Overhead()]
	copy(iv, ciphertext)
	zeroIVBits(iv)
	ctr := cipher.NewCTR(c.b, iv)
	ret, out := sliceForAppend(dst, len(ciphertext)-len(iv))
	ctr.XORKeyStream(out, ciphertext[len(iv):])

	// Authenticate
	expected := c.s2v(data, out)
	if subtle.ConstantTimeCompare(ciphertext[:len(iv)], expected) != 1 {
		return nil, ErrNotAuthentic
	}
	return ret, nil
}

func (c *Cipher) s2v(s [][]byte, sn []byte) []byte {
	h := c.h
	h.Reset()

	tmp, d := c.tmp1, c.tmp2
	tmp.Clear()

	// NOTE(dchest): The standalone S2V returns CMAC(1) if the number of
	// passed vectors is zero, however in SIV construction this case is
	// never triggered, since we always pass plaintext as the last vector
	// (even if it's zero-length), so we omit this case.

	_, err := h.Write(tmp[:])
	if err != nil {
		panic(err)
	}

	copy(d[:], h.Sum(d[:0]))
	h.Reset()

	for _, v := range s {
		_, err := h.Write(v)
		if err != nil {
			panic(err)
		}

		copy(tmp[:], h.Sum(tmp[:0]))
		h.Reset()
		d.Dbl()
		xor(d[:], tmp[:])
	}

	tmp.Clear()

	if len(sn) >= h.BlockSize() {
		n := len(sn) - len(d)
		copy(tmp[:], sn[n:])
		_, err = h.Write(sn[:n])
		if err != nil {
			panic(err)
		}
	} else {
		copy(tmp[:], sn)
		tmp[len(sn)] = 0x80
		d.Dbl()
	}
	xor(tmp[:], d[:])
	_, err = h.Write(tmp[:])
	if err != nil {
		panic(err)
	}

	return h.Sum(tmp[:0])
}

func xor(a, b []byte) {
	for i, v := range b {
		a[i] ^= v
	}
}

func zeroIVBits(iv []byte) {
	// "We zero-out the top bit in each of the last two 32-bit words
	// of the IV before assigning it to Ctr"
	//  — http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf
	iv[len(iv)-8] &= 0x7f
	iv[len(iv)-4] &= 0x7f
}

// sliceForAppend takes a slice and a requested number of bytes. It returns a
// slice with the contents of the given slice followed by that many bytes and a
// second slice that aliases into it and contains only the extra bytes. If the
// original slice has sufficient capacity then no allocation is performed.
func sliceForAppend(in []byte, n int) (head, tail []byte) {
	if total := len(in) + n; cap(in) >= total {
		head = in[:total]
	} else {
		head = make([]byte, total)
		copy(head, in)
	}
	tail = head[len(in):]
	return
}
