/*
 *
 * Copyright 2018 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package conn

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/binary"
	"fmt"
	"strconv"
)

// rekeyAEAD holds the necessary information for an AEAD based on
// AES-GCM that performs nonce-based key derivation and XORs the
// nonce with a random mask.
type rekeyAEAD struct {
	kdfKey     []byte
	kdfCounter []byte
	nonceMask  []byte
	nonceBuf   []byte
	gcmAEAD    cipher.AEAD
}

// KeySizeError signals that the given key does not have the correct size.
type KeySizeError int

func (k KeySizeError) Error() string {
	return "alts/conn: invalid key size " + strconv.Itoa(int(k))
}

// newRekeyAEAD creates a new instance of aes128gcm with rekeying.
// The key argument should be 44 bytes, the first 32 bytes are used as a key
// for HKDF-expand and the remaining 12 bytes are used as a random mask for
// the counter.
func newRekeyAEAD(key []byte) (*rekeyAEAD, error) {
	k := len(key)
	if k != kdfKeyLen+nonceLen {
		return nil, KeySizeError(k)
	}
	return &rekeyAEAD{
		kdfKey:     key[:kdfKeyLen],
		kdfCounter: make([]byte, kdfCounterLen),
		nonceMask:  key[kdfKeyLen:],
		nonceBuf:   make([]byte, nonceLen),
		gcmAEAD:    nil,
	}, nil
}

// Seal rekeys if nonce[2:8] is different than in the last call, masks the nonce,
// and calls Seal for aes128gcm.
func (s *rekeyAEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
	if err := s.rekeyIfRequired(nonce); err != nil {
		panic(fmt.Sprintf("Rekeying failed with: %s", err.Error()))
	}
	maskNonce(s.nonceBuf, nonce, s.nonceMask)
	return s.gcmAEAD.Seal(dst, s.nonceBuf, plaintext, additionalData)
}

// Open rekeys if nonce[2:8] is different than in the last call, masks the nonce,
// and calls Open for aes128gcm.
func (s *rekeyAEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
	if err := s.rekeyIfRequired(nonce); err != nil {
		return nil, err
	}
	maskNonce(s.nonceBuf, nonce, s.nonceMask)
	return s.gcmAEAD.Open(dst, s.nonceBuf, ciphertext, additionalData)
}

// rekeyIfRequired creates a new aes128gcm AEAD if the existing AEAD is nil
// or cannot be used with given nonce.
func (s *rekeyAEAD) rekeyIfRequired(nonce []byte) error {
	newKdfCounter := nonce[kdfCounterOffset : kdfCounterOffset+kdfCounterLen]
	if s.gcmAEAD != nil && bytes.Equal(newKdfCounter, s.kdfCounter) {
		return nil
	}
	copy(s.kdfCounter, newKdfCounter)
	a, err := aes.NewCipher(hkdfExpand(s.kdfKey, s.kdfCounter))
	if err != nil {
		return err
	}
	s.gcmAEAD, err = cipher.NewGCM(a)
	return err
}

// maskNonce XORs the given nonce with the mask and stores the result in dst.
func maskNonce(dst, nonce, mask []byte) {
	nonce1 := binary.LittleEndian.Uint64(nonce[:sizeUint64])
	nonce2 := binary.LittleEndian.Uint32(nonce[sizeUint64:])
	mask1 := binary.LittleEndian.Uint64(mask[:sizeUint64])
	mask2 := binary.LittleEndian.Uint32(mask[sizeUint64:])
	binary.LittleEndian.PutUint64(dst[:sizeUint64], nonce1^mask1)
	binary.LittleEndian.PutUint32(dst[sizeUint64:], nonce2^mask2)
}

// NonceSize returns the required nonce size.
func (s *rekeyAEAD) NonceSize() int {
	return s.gcmAEAD.NonceSize()
}

// Overhead returns the ciphertext overhead.
func (s *rekeyAEAD) Overhead() int {
	return s.gcmAEAD.Overhead()
}

// hkdfExpand computes the first 16 bytes of the HKDF-expand function
// defined in RFC5869.
func hkdfExpand(key, info []byte) []byte {
	mac := hmac.New(sha256.New, key)
	mac.Write(info)
	mac.Write([]byte{0x01}[:])
	return mac.Sum(nil)[:aeadKeyLen]
}
