package main

import (
	"crypto"
	"crypto/aes"
	"crypto/cipher"
	"crypto/des"
	"crypto/hmac"
	_ "crypto/md5"
	"crypto/rc4"
	_ "crypto/sha1"
	_ "crypto/sha256"
	_ "crypto/sha512"
	"encoding/hex"
	"flag"
	"fmt"
	"os"
)

var bulkCipher *string = flag.String("cipher", "", "The bulk cipher to use")
var mac *string = flag.String("mac", "", "The hash function to use in the MAC")
var implicitIV *bool = flag.Bool("implicit-iv", false, "If true, generate tests for a cipher using a pre-TLS-1.0 implicit IV")

// rc4Stream produces a deterministic stream of pseudorandom bytes. This is to
// make this script idempotent.
type rc4Stream struct {
	cipher *rc4.Cipher
}

func newRc4Stream(seed string) (*rc4Stream, error) {
	cipher, err := rc4.NewCipher([]byte(seed))
	if err != nil {
		return nil, err
	}
	return &rc4Stream{cipher}, nil
}

func (rs *rc4Stream) fillBytes(p []byte) {
	for i := range p {
		p[i] = 0
	}
	rs.cipher.XORKeyStream(p, p)
}

func getHash(name string) (crypto.Hash, bool) {
	switch name {
	case "md5":
		return crypto.MD5, true
	case "sha1":
		return crypto.SHA1, true
	case "sha256":
		return crypto.SHA256, true
	case "sha384":
		return crypto.SHA384, true
	default:
		return 0, false
	}
}

func getKeySize(name string) int {
	switch name {
	case "aes128":
		return 16
	case "aes256":
		return 32
	case "3des":
		return 24
	default:
		return 0
	}
}

func newBlockCipher(name string, key []byte) (cipher.Block, error) {
	switch name {
	case "aes128":
		return aes.NewCipher(key)
	case "aes256":
		return aes.NewCipher(key)
	case "3des":
		return des.NewTripleDESCipher(key)
	default:
		return nil, fmt.Errorf("unknown cipher '%s'", name)
	}
}

type testCase struct {
	digest     []byte
	key        []byte
	nonce      []byte
	input      []byte
	ad         []byte
	ciphertext []byte
	tag        []byte
	tag_len    int
	noSeal     bool
	fails      bool
}

// options adds additional options for a test.
type options struct {
	// extraPadding causes an extra block of padding to be added.
	extraPadding bool
	// maximalPadding causes the maximum allowed amount of padding to be added.
	maximalPadding bool
	// wrongPadding causes one of the padding bytes to be wrong.
	wrongPadding bool
	// wrongPaddingOffset specifies the byte offset of the incorrect padding
	// byte.
	wrongPaddingOffset int
	// noPadding causes padding is to be omitted. The plaintext + MAC must
	// be a multiple of the block size.
	noPadding bool
	// omitMAC causes the MAC to be omitted.
	omitMAC bool
}

func makeTestCase(length int, options options) (*testCase, error) {
	rand, err := newRc4Stream("input stream")
	if err != nil {
		return nil, err
	}

	input := make([]byte, length)
	rand.fillBytes(input)

	adFull := make([]byte, 13)
	ad := adFull[:len(adFull)-2]
	rand.fillBytes(ad)
	adFull[len(adFull)-2] = uint8(length >> 8)
	adFull[len(adFull)-1] = uint8(length & 0xff)

	hash, ok := getHash(*mac)
	if !ok {
		return nil, fmt.Errorf("unknown hash function '%s'", *mac)
	}

	macKey := make([]byte, hash.Size())
	rand.fillBytes(macKey)

	h := hmac.New(hash.New, macKey)
	h.Write(adFull)
	h.Write(input)
	digest := h.Sum(nil)

	size := getKeySize(*bulkCipher)
	if size == 0 {
		return nil, fmt.Errorf("unknown cipher '%s'", *bulkCipher)
	}
	encKey := make([]byte, size)
	rand.fillBytes(encKey)

	var fixedIV []byte
	var nonce []byte
	var sealed []byte
	var noSeal, fails bool
	block, err := newBlockCipher(*bulkCipher, encKey)
	if err != nil {
		return nil, err
	}

	iv := make([]byte, block.BlockSize())
	rand.fillBytes(iv)
	if *implicitIV {
		fixedIV = iv
	} else {
		nonce = iv
	}

	cbc := cipher.NewCBCEncrypter(block, iv)

	sealed = make([]byte, 0, len(input)+len(digest)+cbc.BlockSize())
	sealed = append(sealed, input...)
	if options.omitMAC {
		noSeal = true
		fails = true
	} else {
		sealed = append(sealed, digest...)
	}
	paddingLen := cbc.BlockSize() - len(sealed)%cbc.BlockSize()
	if options.noPadding {
		if paddingLen != cbc.BlockSize() {
			return nil, fmt.Errorf("invalid length for noPadding")
		}
		noSeal = true
		fails = true
	} else {
		if options.extraPadding || options.maximalPadding {
			if options.extraPadding {
				paddingLen += cbc.BlockSize()
			} else {
				if 256%cbc.BlockSize() != 0 {
					panic("256 is not a whole number of blocks")
				}
				paddingLen = 256 - len(sealed)%cbc.BlockSize()
			}
			noSeal = true
		}
		pad := make([]byte, paddingLen)
		for i := range pad {
			pad[i] = byte(paddingLen - 1)
		}
		sealed = append(sealed, pad...)
		if options.wrongPadding {
			if options.wrongPaddingOffset >= paddingLen {
				return nil, fmt.Errorf("invalid wrongPaddingOffset")
			}
			sealed[len(sealed)-paddingLen+options.wrongPaddingOffset]++
			noSeal = true
			// TLS specifies the all the padding bytes.
			fails = true
		}
	}
	cbc.CryptBlocks(sealed, sealed)

	key := make([]byte, 0, len(macKey)+len(encKey)+len(fixedIV))
	key = append(key, macKey...)
	key = append(key, encKey...)
	key = append(key, fixedIV...)
	t := &testCase{
		digest:     digest,
		key:        key,
		nonce:      nonce,
		input:      input,
		ad:         ad,
		ciphertext: sealed[:len(input)],
		tag:        sealed[len(input):],
		tag_len:    hash.Size(),
		noSeal:     noSeal,
		fails:      fails,
	}
	return t, nil
}

func printTestCase(t *testCase) {
	fmt.Printf("# DIGEST: %s\n", hex.EncodeToString(t.digest))
	fmt.Printf("KEY: %s\n", hex.EncodeToString(t.key))
	fmt.Printf("NONCE: %s\n", hex.EncodeToString(t.nonce))
	fmt.Printf("IN: %s\n", hex.EncodeToString(t.input))
	fmt.Printf("AD: %s\n", hex.EncodeToString(t.ad))
	fmt.Printf("CT: %s\n", hex.EncodeToString(t.ciphertext))
	fmt.Printf("TAG: %s\n", hex.EncodeToString(t.tag))
	fmt.Printf("TAG_LEN: %d\n", t.tag_len)
	if t.noSeal {
		fmt.Printf("NO_SEAL: 01\n")
	}
	if t.fails {
		fmt.Printf("FAILS: 01\n")
	}
}

func addTestCase(length int, options options) {
	t, err := makeTestCase(length, options)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s\n", err)
		os.Exit(1)
	}
	printTestCase(t)
	fmt.Printf("\n")
}

func main() {
	flag.Parse()

	commandLine := fmt.Sprintf("go run make_legacy_aead_tests.go -cipher %s -mac %s", *bulkCipher, *mac)
	if *implicitIV {
		commandLine += " -implicit-iv"
	}
	fmt.Printf("# Generated by\n")
	fmt.Printf("#   %s\n", commandLine)
	fmt.Printf("#\n")
	fmt.Printf("# Note: aead_test's input format splits the ciphertext and tag positions of the\n")
	fmt.Printf("# sealed input. But these legacy AEADs are MAC-then-encrypt and so the 'TAG' may\n")
	fmt.Printf("# also include padding. We write the byte length of the MAC to 'TAG_LEN' and\n")
	fmt.Printf("# include the unencrypted MAC in the 'DIGEST' tag above # each test case.\n")
	fmt.Printf("# each test case.\n")
	fmt.Printf("\n")

	// For CBC-mode ciphers, emit tests for padding flexibility.
	fmt.Printf("# Test with non-minimal padding.\n")
	addTestCase(5, options{extraPadding: true})

	fmt.Printf("# Test with bad padding values.\n")
	addTestCase(5, options{wrongPadding: true})

	hash, ok := getHash(*mac)
	if !ok {
		panic("unknown hash")
	}

	fmt.Printf("# Test with no padding.\n")
	addTestCase(64-hash.Size(), options{noPadding: true})

	// Test with maximal padding at all rotations modulo the hash's block
	// size. Our smallest hash (SHA-1 at 64-byte blocks) exceeds our largest
	// block cipher (AES at 16-byte blocks), so this is also covers all
	// block cipher rotations. This is to ensure full coverage of the
	// kVarianceBlocks value in the constant-time logic.
	hashBlockSize := hash.New().BlockSize()
	for i := range hashBlockSize {
		fmt.Printf("# Test with maximal padding (%d mod %d).\n", i, hashBlockSize)
		addTestCase(hashBlockSize+i, options{maximalPadding: true})
	}

	fmt.Printf("# Test if the unpadded input is too short for a MAC, but not publicly so.\n")
	addTestCase(0, options{omitMAC: true, maximalPadding: true})

	fmt.Printf("# Test that each byte of incorrect padding is noticed.\n")
	for i := range 256 {
		addTestCase(64-hash.Size(), options{
			maximalPadding:     true,
			wrongPadding:       true,
			wrongPaddingOffset: i,
		})
	}

	// Generate long enough of input to cover a non-zero num_starting_blocks
	// value in the constant-time CBC logic.
	for l := 0; l < 500; l += 5 {
		addTestCase(l, options{})
	}
}
