package transit

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/base64"
	"fmt"
	"hash"
	"strconv"
	"strings"

	"github.com/hashicorp/vault/logical"
	"github.com/hashicorp/vault/logical/framework"
)

func (b *backend) pathHMAC() *framework.Path {
	return &framework.Path{
		Pattern: "hmac/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("urlalgorithm"),
		Fields: map[string]*framework.FieldSchema{
			"name": &framework.FieldSchema{
				Type:        framework.TypeString,
				Description: "The key to use for the HMAC function",
			},

			"input": &framework.FieldSchema{
				Type:        framework.TypeString,
				Description: "The base64-encoded input data",
			},

			"algorithm": &framework.FieldSchema{
				Type:    framework.TypeString,
				Default: "sha2-256",
				Description: `Algorithm to use (POST body parameter). Valid values are:

* sha2-224
* sha2-256
* sha2-384
* sha2-512

Defaults to "sha2-256".`,
			},

			"urlalgorithm": &framework.FieldSchema{
				Type:        framework.TypeString,
				Description: `Algorithm to use (POST URL parameter)`,
			},

			"key_version": &framework.FieldSchema{
				Type: framework.TypeInt,
				Description: `The version of the key to use for generating the HMAC.
Must be 0 (for latest) or a value greater than or equal
to the min_encryption_version configured on the key.`,
			},
		},

		Callbacks: map[logical.Operation]framework.OperationFunc{
			logical.UpdateOperation: b.pathHMACWrite,
		},

		HelpSynopsis:    pathHMACHelpSyn,
		HelpDescription: pathHMACHelpDesc,
	}
}

func (b *backend) pathHMACWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
	name := d.Get("name").(string)
	ver := d.Get("key_version").(int)
	inputB64 := d.Get("input").(string)
	algorithm := d.Get("urlalgorithm").(string)
	if algorithm == "" {
		algorithm = d.Get("algorithm").(string)
	}

	input, err := base64.StdEncoding.DecodeString(inputB64)
	if err != nil {
		return logical.ErrorResponse(fmt.Sprintf("unable to decode input as base64: %s", err)), logical.ErrInvalidRequest
	}

	// Get the policy
	p, lock, err := b.lm.GetPolicyShared(ctx, req.Storage, name)
	if lock != nil {
		defer lock.RUnlock()
	}
	if err != nil {
		return nil, err
	}
	if p == nil {
		return logical.ErrorResponse("encryption key not found"), logical.ErrInvalidRequest
	}

	switch {
	case ver == 0:
		// Allowed, will use latest; set explicitly here to ensure the string
		// is generated properly
		ver = p.LatestVersion
	case ver == p.LatestVersion:
		// Allowed
	case p.MinEncryptionVersion > 0 && ver < p.MinEncryptionVersion:
		return logical.ErrorResponse("cannot generate HMAC: version is too old (disallowed by policy)"), logical.ErrInvalidRequest
	}

	key, err := p.HMACKey(ver)
	if err != nil {
		return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
	}
	if key == nil {
		return nil, fmt.Errorf("HMAC key value could not be computed")
	}

	var hf hash.Hash
	switch algorithm {
	case "sha2-224":
		hf = hmac.New(sha256.New224, key)
	case "sha2-256":
		hf = hmac.New(sha256.New, key)
	case "sha2-384":
		hf = hmac.New(sha512.New384, key)
	case "sha2-512":
		hf = hmac.New(sha512.New, key)
	default:
		return logical.ErrorResponse(fmt.Sprintf("unsupported algorithm %s", algorithm)), nil
	}
	hf.Write(input)
	retBytes := hf.Sum(nil)

	retStr := base64.StdEncoding.EncodeToString(retBytes)
	retStr = fmt.Sprintf("vault:v%s:%s", strconv.Itoa(ver), retStr)

	// Generate the response
	resp := &logical.Response{
		Data: map[string]interface{}{
			"hmac": retStr,
		},
	}
	return resp, nil
}

func (b *backend) pathHMACVerify(ctx context.Context, req *logical.Request, d *framework.FieldData, verificationHMAC string) (*logical.Response, error) {
	name := d.Get("name").(string)
	inputB64 := d.Get("input").(string)
	algorithm := d.Get("urlalgorithm").(string)
	if algorithm == "" {
		algorithm = d.Get("algorithm").(string)
	}

	input, err := base64.StdEncoding.DecodeString(inputB64)
	if err != nil {
		return logical.ErrorResponse(fmt.Sprintf("unable to decode input as base64: %s", err)), logical.ErrInvalidRequest
	}

	// Verify the prefix
	if !strings.HasPrefix(verificationHMAC, "vault:v") {
		return logical.ErrorResponse("invalid HMAC to verify: no prefix"), logical.ErrInvalidRequest
	}

	splitVerificationHMAC := strings.SplitN(strings.TrimPrefix(verificationHMAC, "vault:v"), ":", 2)
	if len(splitVerificationHMAC) != 2 {
		return logical.ErrorResponse("invalid HMAC: wrong number of fields"), logical.ErrInvalidRequest
	}

	ver, err := strconv.Atoi(splitVerificationHMAC[0])
	if err != nil {
		return logical.ErrorResponse("invalid HMAC: version number could not be decoded"), logical.ErrInvalidRequest
	}

	verBytes, err := base64.StdEncoding.DecodeString(splitVerificationHMAC[1])
	if err != nil {
		return logical.ErrorResponse(fmt.Sprintf("unable to decode verification HMAC as base64: %s", err)), logical.ErrInvalidRequest
	}

	// Get the policy
	p, lock, err := b.lm.GetPolicyShared(ctx, req.Storage, name)
	if lock != nil {
		defer lock.RUnlock()
	}
	if err != nil {
		return nil, err
	}
	if p == nil {
		return logical.ErrorResponse("encryption key not found"), logical.ErrInvalidRequest
	}

	if ver > p.LatestVersion {
		return logical.ErrorResponse("invalid HMAC: version is too new"), logical.ErrInvalidRequest
	}

	if p.MinDecryptionVersion > 0 && ver < p.MinDecryptionVersion {
		return logical.ErrorResponse("cannot verify HMAC: version is too old (disallowed by policy)"), logical.ErrInvalidRequest
	}

	key, err := p.HMACKey(ver)
	if err != nil {
		return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
	}
	if key == nil {
		return nil, fmt.Errorf("HMAC key value could not be computed")
	}

	var hf hash.Hash
	switch algorithm {
	case "sha2-224":
		hf = hmac.New(sha256.New224, key)
	case "sha2-256":
		hf = hmac.New(sha256.New, key)
	case "sha2-384":
		hf = hmac.New(sha512.New384, key)
	case "sha2-512":
		hf = hmac.New(sha512.New, key)
	default:
		return logical.ErrorResponse(fmt.Sprintf("unsupported algorithm %s", algorithm)), nil
	}
	hf.Write(input)
	retBytes := hf.Sum(nil)

	return &logical.Response{
		Data: map[string]interface{}{
			"valid": hmac.Equal(retBytes, verBytes),
		},
	}, nil
}

const pathHMACHelpSyn = `Generate an HMAC for input data using the named key`

const pathHMACHelpDesc = `
Generates an HMAC sum of the given algorithm and key against the given input data.
`
