// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package transit

import (
	"context"
	"crypto/rsa"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/base64"
	"errors"
	"fmt"
	"hash"
	"strconv"
	"strings"
	"time"

	"github.com/google/tink/go/kwp/subtle"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/helper/keysutil"
	"github.com/hashicorp/vault/sdk/logical"
)

const EncryptedKeyBytes = 512

func (b *backend) pathImport() *framework.Path {
	return &framework.Path{
		Pattern: "keys/" + framework.GenericNameRegex("name") + "/import",

		DisplayAttrs: &framework.DisplayAttributes{
			OperationPrefix: operationPrefixTransit,
			OperationVerb:   "import",
			OperationSuffix: "key",
		},

		Fields: map[string]*framework.FieldSchema{
			"name": {
				Type:        framework.TypeString,
				Description: "The name of the key",
			},
			"type": {
				Type:    framework.TypeString,
				Default: "aes256-gcm96",
				Description: `The type of key being imported. Currently, "aes128-gcm96" (symmetric), "aes256-gcm96" (symmetric), "ecdsa-p256"
(asymmetric), "ecdsa-p384" (asymmetric), "ecdsa-p521" (asymmetric), "ed25519" (asymmetric), "rsa-2048" (asymmetric), "rsa-3072"
(asymmetric), "rsa-4096" (asymmetric) are supported.  Defaults to "aes256-gcm96".
`,
			},
			"hash_function": {
				Type:    framework.TypeString,
				Default: "SHA256",
				Description: `The hash function used as a random oracle in the OAEP wrapping of the user-generated,
ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", or "SHA512"`,
			},
			"ciphertext": {
				Type: framework.TypeString,
				Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP 
with the wrapping key and then concatenated with the import key, wrapped by the AES key.`,
			},
			"public_key": {
				Type:        framework.TypeString,
				Description: `The plaintext PEM public key to be imported. If "ciphertext" is set, this field is ignored.`,
			},
			"allow_rotation": {
				Type:        framework.TypeBool,
				Description: "True if the imported key may be rotated within Vault; false otherwise.",
			},
			"derived": {
				Type: framework.TypeBool,
				Description: `Enables key derivation mode. This
allows for per-transaction unique
keys for encryption operations.`,
			},

			"exportable": {
				Type: framework.TypeBool,
				Description: `Enables keys to be exportable.
This allows for all the valid keys
in the key ring to be exported.`,
			},

			"allow_plaintext_backup": {
				Type: framework.TypeBool,
				Description: `Enables taking a backup of the named
key in plaintext format. Once set,
this cannot be disabled.`,
			},

			"context": {
				Type: framework.TypeString,
				Description: `Base64 encoded context for key derivation.
When reading a key with key derivation enabled,
if the key type supports public keys, this will
return the public key for the given context.`,
			},
			"auto_rotate_period": {
				Type:    framework.TypeDurationSecond,
				Default: 0,
				Description: `Amount of time the key should live before
being automatically rotated. A value of 0
(default) disables automatic rotation for the
key.`,
			},
		},
		Callbacks: map[logical.Operation]framework.OperationFunc{
			logical.UpdateOperation: b.pathImportWrite,
		},
		HelpSynopsis:    pathImportWriteSyn,
		HelpDescription: pathImportWriteDesc,
	}
}

func (b *backend) pathImportVersion() *framework.Path {
	return &framework.Path{
		Pattern: "keys/" + framework.GenericNameRegex("name") + "/import_version",

		DisplayAttrs: &framework.DisplayAttributes{
			OperationPrefix: operationPrefixTransit,
			OperationVerb:   "import",
			OperationSuffix: "key-version",
		},

		Fields: map[string]*framework.FieldSchema{
			"name": {
				Type:        framework.TypeString,
				Description: "The name of the key",
			},
			"ciphertext": {
				Type: framework.TypeString,
				Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP 
with the wrapping key and then concatenated with the import key, wrapped by the AES key.`,
			},
			"public_key": {
				Type:        framework.TypeString,
				Description: `The plaintext public key to be imported. If "ciphertext" is set, this field is ignored.`,
			},
			"hash_function": {
				Type:    framework.TypeString,
				Default: "SHA256",
				Description: `The hash function used as a random oracle in the OAEP wrapping of the user-generated,
ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", or "SHA512"`,
			},
			"version": {
				Type: framework.TypeInt,
				Description: `Key version to be updated, if left empty, a new version will be created unless
a private key is specified and the 'Latest' key is missing a private key.`,
			},
		},
		Callbacks: map[logical.Operation]framework.OperationFunc{
			logical.UpdateOperation: b.pathImportVersionWrite,
		},
		HelpSynopsis:    pathImportVersionWriteSyn,
		HelpDescription: pathImportVersionWriteDesc,
	}
}

func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
	name := d.Get("name").(string)
	derived := d.Get("derived").(bool)
	keyType := d.Get("type").(string)
	exportable := d.Get("exportable").(bool)
	allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool)
	autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int))
	allowRotation := d.Get("allow_rotation").(bool)

	// Ensure the caller didn't supply "convergent_encryption" as a field, since it's not supported on import.
	if _, ok := d.Raw["convergent_encryption"]; ok {
		return nil, errors.New("import cannot be used on keys with convergent encryption enabled")
	}

	if autoRotatePeriod > 0 && !allowRotation {
		return nil, errors.New("allow_rotation must be set to true if auto-rotation is enabled")
	}

	// Ensure that at least on `key` field has been set
	isCiphertextSet, err := checkKeyFieldsSet(d)
	if err != nil {
		return nil, err
	}

	polReq := keysutil.PolicyRequest{
		Storage:                  req.Storage,
		Name:                     name,
		Derived:                  derived,
		Exportable:               exportable,
		AllowPlaintextBackup:     allowPlaintextBackup,
		AutoRotatePeriod:         autoRotatePeriod,
		AllowImportedKeyRotation: allowRotation,
		IsPrivateKey:             isCiphertextSet,
	}

	switch strings.ToLower(keyType) {
	case "aes128-gcm96":
		polReq.KeyType = keysutil.KeyType_AES128_GCM96
	case "aes256-gcm96":
		polReq.KeyType = keysutil.KeyType_AES256_GCM96
	case "chacha20-poly1305":
		polReq.KeyType = keysutil.KeyType_ChaCha20_Poly1305
	case "ecdsa-p256":
		polReq.KeyType = keysutil.KeyType_ECDSA_P256
	case "ecdsa-p384":
		polReq.KeyType = keysutil.KeyType_ECDSA_P384
	case "ecdsa-p521":
		polReq.KeyType = keysutil.KeyType_ECDSA_P521
	case "ed25519":
		polReq.KeyType = keysutil.KeyType_ED25519
	case "rsa-2048":
		polReq.KeyType = keysutil.KeyType_RSA2048
	case "rsa-3072":
		polReq.KeyType = keysutil.KeyType_RSA3072
	case "rsa-4096":
		polReq.KeyType = keysutil.KeyType_RSA4096
	case "hmac":
		polReq.KeyType = keysutil.KeyType_HMAC
	default:
		return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest
	}

	p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader())
	if err != nil {
		return nil, err
	}

	if p != nil {
		if b.System().CachingDisabled() {
			p.Unlock()
		}
		return nil, errors.New("the import path cannot be used with an existing key; use import-version to rotate an existing imported key")
	}

	key, resp, err := b.extractKeyFromFields(ctx, req, d, polReq.KeyType, isCiphertextSet)
	if err != nil {
		return resp, err
	}

	err = b.lm.ImportPolicy(ctx, polReq, key, b.GetRandomReader())
	if err != nil {
		return nil, err
	}

	return nil, nil
}

func (b *backend) pathImportVersionWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
	name := d.Get("name").(string)

	isCiphertextSet, err := checkKeyFieldsSet(d)
	if err != nil {
		return nil, err
	}

	polReq := keysutil.PolicyRequest{
		Storage:      req.Storage,
		Name:         name,
		Upsert:       false,
		IsPrivateKey: isCiphertextSet,
	}
	p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader())
	if err != nil {
		return nil, err
	}
	if p == nil {
		return nil, fmt.Errorf("no key found with name %s; to import a new key, use the import/ endpoint", name)
	}
	if !p.Imported {
		return nil, errors.New("the import_version endpoint can only be used with an imported key")
	}
	if p.ConvergentEncryption {
		return nil, errors.New("import_version cannot be used on keys with convergent encryption enabled")
	}

	if !b.System().CachingDisabled() {
		p.Lock(true)
	}
	defer p.Unlock()

	key, resp, err := b.extractKeyFromFields(ctx, req, d, p.Type, isCiphertextSet)
	if err != nil {
		return resp, err
	}

	// Get param version if set else import a new version.
	if version, ok := d.GetOk("version"); ok {
		versionToUpdate := version.(int)

		// Check if given version can be updated given input
		err = p.KeyVersionCanBeUpdated(versionToUpdate, isCiphertextSet)
		if err == nil {
			err = p.ImportPrivateKeyForVersion(ctx, req.Storage, versionToUpdate, key)
		}
	} else {
		err = p.ImportPublicOrPrivate(ctx, req.Storage, key, isCiphertextSet, b.GetRandomReader())
	}

	if err != nil {
		return nil, err
	}

	return nil, nil
}

func (b *backend) decryptImportedKey(ctx context.Context, storage logical.Storage, ciphertext []byte, hashFn hash.Hash) ([]byte, error) {
	// Bounds check the ciphertext to avoid panics
	if len(ciphertext) <= EncryptedKeyBytes {
		return nil, errors.New("provided ciphertext is too short")
	}

	wrappedEphKey := ciphertext[:EncryptedKeyBytes]
	wrappedImportKey := ciphertext[EncryptedKeyBytes:]

	wrappingKey, err := b.getWrappingKey(ctx, storage)
	if err != nil {
		return nil, err
	}
	if wrappingKey == nil {
		return nil, fmt.Errorf("error importing key: wrapping key was nil")
	}

	privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey
	ephKey, err := rsa.DecryptOAEP(hashFn, b.GetRandomReader(), privWrappingKey, wrappedEphKey, []byte{})
	if err != nil {
		return nil, err
	}

	// Zero out the ephemeral AES key just to be extra cautious. Note that this
	// isn't a guarantee against memory analysis! See the documentation for the
	// `vault.memzero` utility function for more information.
	defer func() {
		for i := range ephKey {
			ephKey[i] = 0
		}
	}()

	// Ensure the ephemeral AES key is 256-bit
	if len(ephKey) != 32 {
		return nil, errors.New("expected ephemeral AES key to be 256-bit")
	}

	kwp, err := subtle.NewKWP(ephKey)
	if err != nil {
		return nil, err
	}

	importKey, err := kwp.Unwrap(wrappedImportKey)
	if err != nil {
		return nil, err
	}

	return importKey, nil
}

func (b *backend) extractKeyFromFields(ctx context.Context, req *logical.Request, d *framework.FieldData, keyType keysutil.KeyType, isPrivateKey bool) ([]byte, *logical.Response, error) {
	var key []byte
	if isPrivateKey {
		hashFnStr := d.Get("hash_function").(string)
		hashFn, err := parseHashFn(hashFnStr)
		if err != nil {
			return key, logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
		}

		ciphertextString := d.Get("ciphertext").(string)
		ciphertext, err := base64.StdEncoding.DecodeString(ciphertextString)
		if err != nil {
			return key, nil, err
		}

		key, err = b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn)
		if err != nil {
			return key, nil, err
		}
	} else {
		publicKeyString := d.Get("public_key").(string)
		if !keyType.ImportPublicKeySupported() {
			return key, nil, errors.New("provided type does not support public_key import")
		}
		key = []byte(publicKeyString)
	}

	return key, nil, nil
}

func parseHashFn(hashFn string) (hash.Hash, error) {
	switch strings.ToUpper(hashFn) {
	case "SHA1":
		return sha1.New(), nil
	case "SHA224":
		return sha256.New224(), nil
	case "SHA256":
		return sha256.New(), nil
	case "SHA384":
		return sha512.New384(), nil
	case "SHA512":
		return sha512.New(), nil
	default:
		return nil, fmt.Errorf("unknown hash function: %s", hashFn)
	}
}

// checkKeyFieldsSet: Checks which key fields are set. If both are set, an error is returned
func checkKeyFieldsSet(d *framework.FieldData) (bool, error) {
	ciphertextSet := isFieldSet("ciphertext", d)
	publicKeySet := isFieldSet("publicKey", d)

	if ciphertextSet && publicKeySet {
		return false, errors.New("only one of the following fields, ciphertext and public_key, can be set")
	} else if ciphertextSet {
		return true, nil
	} else {
		return false, nil
	}
}

func isFieldSet(fieldName string, d *framework.FieldData) bool {
	_, fieldSet := d.Raw[fieldName]
	if !fieldSet {
		return false
	}

	return true
}

const (
	pathImportWriteSyn  = "Imports an externally-generated key into a new transit key"
	pathImportWriteDesc = "This path is used to import an externally-generated " +
		"key into Vault. The import operation creates a new key and cannot be used to " +
		"replace an existing key."
)

const pathImportVersionWriteSyn = "Imports an externally-generated key into an " +
	"existing imported key"

const pathImportVersionWriteDesc = "This path is used to import a new version of an " +
	"externally-generated key into an existing import key. The import_version endpoint " +
	"only supports importing key material into existing imported keys."
