package manager

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/kms"
)

// KeyManagement is a sub-set of the capabilities of the KMS client.
type KeyManagement interface {
	GenerateDataKey(*kms.GenerateDataKeyInput) (*kms.GenerateDataKeyOutput, error)
	Decrypt(*kms.DecryptInput) (*kms.DecryptOutput, error)
}

// An Envelope encrypts and decrypts secrets with single-use KMS data keys using
// AES-256-GCM.
type Envelope struct {
	KMS KeyManagement
}

// Seal generates a 256-bit data key using KMS and encrypts the given plaintext
// with AES-256-GCM using a random nonce. The ciphertext is appended to the
// nonce, which is in turn appended to the KMS data key ciphertext and returned.
func (e *Envelope) Seal(keyID string, ctxt map[string]string, plaintext []byte) (key, ciphertext []byte, keyArn string, err error) {
	res, err := e.KMS.GenerateDataKey(&kms.GenerateDataKeyInput{
		EncryptionContext: e.context(ctxt),
		KeySpec:           aws.String("AES_256"),
		KeyId:             aws.String(keyID),
	})
	if err != nil {
		return nil, nil, "", err
	}

	ciphertext, err = encrypt(res.Plaintext, plaintext, []byte(*res.KeyId))
	if err != nil {
		return nil, nil, "", err
	}

	return res.CiphertextBlob, ciphertext, *res.KeyId, nil
}

// Open takes the output of Seal and decrypts it. If any part of the ciphertext
// or context is modified, Seal will return an error instead of the decrypted
// data.
func (e *Envelope) Open(key []byte, ciphertext []byte, ctxt map[string]string) ([]byte, error) {
	res, err := e.KMS.Decrypt(&kms.DecryptInput{
		CiphertextBlob:    key,
		EncryptionContext: e.context(ctxt),
	})
	if err != nil {
		if apiErr, ok := err.(awserr.Error); ok {
			if apiErr.Code() == "InvalidCiphertextException" {
				return nil, fmt.Errorf("unable to decrypt data key")
			}
		}
		return nil, err
	}

	return decrypt(res.Plaintext, ciphertext, []byte(*res.KeyId))
}

func (e *Envelope) context(c map[string]string) map[string]*string {
	ctxt := make(map[string]*string)
	for k, v := range c {
		ctxt[k] = aws.String(v)
	}
	return ctxt
}

func encrypt(key, plaintext, data []byte) ([]byte, error) {
	defer Zero(key)

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, err
	}

	return gcm.Seal(nonce, nonce, plaintext, data), nil
}

func decrypt(key, ciphertext, data []byte) ([]byte, error) {
	defer Zero(key)

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonce, ciphertext := ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():]

	return gcm.Open(nil, nonce, ciphertext, data)
}

// Zero replaces every byte in b with 0
func Zero(b []byte) {
	for i := range b {
		b[i] = 0
	}
}
