/*
`AEGIS-128L` and `AEGIS-256` AEAD algorithms.

Where AEAD stands for Authenticated Encryption with Additional Data.

See:
- [[ https://www.ietf.org/archive/id/draft-irtf-cfrg-aegis-aead-12.txt ]]
*/
package aegis

import "core:bytes"
import "core:crypto"
import "core:crypto/aes"

// KEY_SIZE_128L is the AEGIS-128L key size in bytes.
KEY_SIZE_128L :: 16
// KEY_SIZE_256 is the AEGIS-256 key size in bytes.
KEY_SIZE_256 :: 32
// IV_SIZE_128L is the AEGIS-128L IV size in bytes.
IV_SIZE_128L :: 16
// IV_SIZE_256 is the AEGIS-256 IV size in bytes.
IV_SIZE_256 :: 32
// TAG_SIZE_128 is the AEGIS-128L or AEGIS-256 128-bit tag size in bytes.
TAG_SIZE_128 :: 16
// TAG_SIZE_256 is the AEGIS-128L or AEGIS-256 256-bit tag size in bytes.
TAG_SIZE_256 :: 32

@(private)
_RATE_128L :: 32
@(private)
_RATE_256 :: 16
@(private)
_RATE_MAX :: _RATE_128L

@(private, rodata)
_C0 := [16]byte{
	0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x08, 0x0d,
	0x15, 0x22, 0x37, 0x59, 0x90, 0xe9, 0x79, 0x62,
}

@(private, rodata)
_C1 := [16]byte {
	0xdb, 0x3d, 0x18, 0x55, 0x6d, 0xc2, 0x2f, 0xf1,
	0x20, 0x11, 0x31, 0x42, 0x73, 0xb5, 0x28, 0xdd,
}

// Context is a keyed AEGIS-128L or AEGIS-256 instance.
Context :: struct {
	_key:            [KEY_SIZE_256]byte,
	_key_len:        int,
	_impl:           aes.Implementation,
	_is_initialized: bool,
}

@(private)
_validate_common_slice_sizes :: proc (ctx: ^Context, tag, iv, aad, text: []byte) {
	switch len(tag) {
	case TAG_SIZE_128, TAG_SIZE_256:
	case:
		panic("crypto/aegis: invalid tag size")
	}

	iv_ok: bool
	switch ctx._key_len {
	case KEY_SIZE_128L:
		iv_ok = len(iv) == IV_SIZE_128L
	case KEY_SIZE_256:
		iv_ok = len(iv) == IV_SIZE_256
	}
	ensure(iv_ok,"crypto/aegis: invalid IV size")

	#assert(size_of(int) == 8 || size_of(int) <= 4)
	// As A_MAX and P_MAX are both defined to be 2^61 - 1 bytes, and
	// the maximum length of a slice is bound by `size_of(int)`, where
	// `int` is register sized, there is no need to check AAD/text
	// lengths.
}

// init initializes a Context with the provided key, for AEGIS-128L or AEGIS-256.
init :: proc(ctx: ^Context, key: []byte, impl := aes.DEFAULT_IMPLEMENTATION) {
	switch len(key) {
	case KEY_SIZE_128L, KEY_SIZE_256:
	case:
		panic("crypto/aegis: invalid key size")
	}

	copy(ctx._key[:], key)
	ctx._key_len = len(key)
	ctx._impl = impl
	if ctx._impl == .Hardware && !is_hardware_accelerated() {
		ctx._impl = .Portable
	}
	ctx._is_initialized = true
}

// seal encrypts the plaintext and authenticates the aad and ciphertext,
// with the provided Context and iv, stores the output in dst and tag.
//
// dst and plaintext MUST alias exactly or not at all.
seal :: proc(ctx: ^Context, dst, tag, iv, aad, plaintext: []byte) {
	ensure(ctx._is_initialized)

	_validate_common_slice_sizes(ctx, tag, iv, aad, plaintext)
	ensure(len(dst) == len(plaintext), "crypto/aegis: invalid destination ciphertext size")
	ensure(!bytes.alias_inexactly(dst, plaintext), "crypto/aegis: dst and plaintext alias inexactly")

	switch ctx._impl {
	case .Hardware:
		st: State_HW
		defer reset_state_hw(&st)

		init_hw(ctx, &st, iv)

		aad_len, pt_len := len(aad), len(plaintext)
		if aad_len > 0 {
			absorb_hw(&st, aad)
		}

		if pt_len > 0 {
			enc_hw(&st, dst, plaintext)
		}

		finalize_hw(&st, tag, aad_len, pt_len)
	case .Portable:
		st: State_SW
		defer reset_state_sw(&st)

		init_sw(ctx, &st, iv)

		aad_len, pt_len := len(aad), len(plaintext)
		if aad_len > 0 {
			absorb_sw(&st, aad)
		}

		if pt_len > 0 {
			enc_sw(&st, dst, plaintext)
		}

		finalize_sw(&st, tag, aad_len, pt_len)
	case:
		panic("core/crypto/aegis: not implemented")
	}
}

// open authenticates the aad and ciphertext, and decrypts the ciphertext,
// with the provided Context, iv, and tag, and stores the output in dst,
// returning true if and only if (⟺) the authentication was successful.  If authentication
// fails, the destination buffer will be zeroed.
//
// dst and plaintext MUST alias exactly or not at all.
@(require_results)
open :: proc(ctx: ^Context, dst, iv, aad, ciphertext, tag: []byte) -> bool {
	ensure(ctx._is_initialized)

	_validate_common_slice_sizes(ctx, tag, iv, aad, ciphertext)
	ensure(len(dst) == len(ciphertext), "crypto/aegis: invalid destination plaintext size")
	ensure(!bytes.alias_inexactly(dst, ciphertext), "crypto/aegis: dst and ciphertext alias inexactly")

	tmp: [TAG_SIZE_256]byte
	derived_tag := tmp[:len(tag)]
	aad_len, ct_len := len(aad), len(ciphertext)

	switch ctx._impl {
	case .Hardware:
		st: State_HW
		defer reset_state_hw(&st)

		init_hw(ctx, &st, iv)

		if aad_len > 0 {
			absorb_hw(&st, aad)
		}

		if ct_len > 0 {
			dec_hw(&st, dst, ciphertext)
		}

		finalize_hw(&st, derived_tag, aad_len, ct_len)
	case .Portable:
		st: State_SW
		defer reset_state_sw(&st)

		init_sw(ctx, &st, iv)

		if aad_len > 0 {
			absorb_sw(&st, aad)
		}

		if ct_len > 0 {
			dec_sw(&st, dst, ciphertext)
		}

		finalize_sw(&st, derived_tag, aad_len, ct_len)
	case:
		panic("core/crypto/aegis: not implemented")
	}

	if crypto.compare_constant_time(tag, derived_tag) != 1 {
		crypto.zero_explicit(raw_data(derived_tag), len(derived_tag))
		crypto.zero_explicit(raw_data(dst), ct_len)
		return false
	}

	return true
}

// reset sanitizes the Context.  The Context must be
// re-initialized to be used again.
reset :: proc "contextless" (ctx: ^Context) {
	crypto.zero_explicit(&ctx._key, len(ctx._key))
	ctx._key_len = 0
	ctx._is_initialized = false
}