// Copyright 2022 The Sigstore 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 signer

import (
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/ed25519"
	"errors"
	"fmt"
	"math/big"
	"os"
	"path/filepath"
	"strings"

	"github.com/google/tink/go/core/registry"
	"github.com/google/tink/go/integration/awskms"
	"github.com/google/tink/go/integration/gcpkms"
	"github.com/google/tink/go/integration/hcvault"
	signatureSubtle "github.com/google/tink/go/signature/subtle"
	"github.com/google/tink/go/subtle"
	"github.com/google/tink/go/tink"

	"github.com/golang/protobuf/proto" //lint:ignore SA1019 needed for unmarshalling
	"github.com/google/tink/go/insecurecleartextkeyset"
	"github.com/google/tink/go/keyset"
	commonpb "github.com/google/tink/go/proto/common_go_proto"
	ecdsapb "github.com/google/tink/go/proto/ecdsa_go_proto"
	ed25519pb "github.com/google/tink/go/proto/ed25519_go_proto"
	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
)

var (
	ecdsaSignerKeyVersion   = 0
	ecdsaSignerTypeURL      = "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey"
	ed25519SignerKeyVersion = 0
	ed25519SignerTypeURL    = "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey"
)

// NewTinkSigner creates a signer by decrypting a local Tink keyset with a remote KMS encryption key
func NewTinkSigner(_ context.Context, tinkKeysetPath string, primaryKey tink.AEAD) (crypto.Signer, error) {
	f, err := os.Open(filepath.Clean(tinkKeysetPath))
	if err != nil {
		return nil, err
	}
	defer f.Close()

	kh, err := keyset.Read(keyset.NewJSONReader(f), primaryKey)
	if err != nil {
		return nil, err
	}
	signer, err := KeyHandleToSigner(kh)
	if err != nil {
		return nil, err
	}
	return signer, nil
}

// GetPrimaryKey returns a Tink AEAD encryption key from KMS
// Supports GCP, AWS, and Vault
func GetPrimaryKey(ctx context.Context, kmsKey, hcVaultToken string) (tink.AEAD, error) {
	switch {
	case strings.HasPrefix(kmsKey, "gcp-kms://"):
		gcpClient, err := gcpkms.NewClientWithOptions(ctx, kmsKey)
		if err != nil {
			return nil, err
		}
		registry.RegisterKMSClient(gcpClient)
		return gcpClient.GetAEAD(kmsKey)
	case strings.HasPrefix(kmsKey, "aws-kms://"):
		awsClient, err := awskms.NewClient(kmsKey)
		if err != nil {
			return nil, err
		}
		registry.RegisterKMSClient(awsClient)
		return awsClient.GetAEAD(kmsKey)
	case strings.HasPrefix(kmsKey, "hcvault://"):
		hcVaultClient, err := hcvault.NewClient(kmsKey, nil, hcVaultToken)
		if err != nil {
			return nil, err
		}
		registry.RegisterKMSClient(hcVaultClient)
		return hcVaultClient.GetAEAD(kmsKey)
	default:
		return nil, errors.New("unsupported Tink KMS key type")
	}
}

// KeyHandleToSigner converts a key handle to the crypto.Signer interface.
// Heavily pulls from Tink's signature and subtle packages.
func KeyHandleToSigner(kh *keyset.Handle) (crypto.Signer, error) {
	// extract the key material from the key handle
	ks := insecurecleartextkeyset.KeysetMaterial(kh)

	k := getPrimaryKey(ks)
	if k == nil {
		return nil, errors.New("no enabled key found in keyset")
	}

	switch k.GetTypeUrl() {
	case ecdsaSignerTypeURL:
		// https://github.com/google/tink/blob/9753ffddd4d04aa56e0605ff4a0db46f2fb80529/go/signature/ecdsa_signer_key_manager.go#L48
		privKey := new(ecdsapb.EcdsaPrivateKey)
		if err := proto.Unmarshal(k.GetValue(), privKey); err != nil {
			return nil, fmt.Errorf("error unmarshalling ecdsa private key: %w", err)
		}
		if err := validateEcdsaPrivKey(privKey); err != nil {
			return nil, fmt.Errorf("error validating ecdsa private key: %w", err)
		}
		// https://github.com/google/tink/blob/9753ffddd4d04aa56e0605ff4a0db46f2fb80529/go/signature/subtle/ecdsa_signer.go#L39
		_, curve, _ := getECDSAParamNames(privKey.PublicKey.Params)
		p := new(ecdsa.PrivateKey)
		c := subtle.GetCurve(curve)
		p.PublicKey.Curve = c
		p.D = new(big.Int).SetBytes(privKey.GetKeyValue())
		p.PublicKey.X, p.PublicKey.Y = c.ScalarBaseMult(privKey.GetKeyValue())
		return p, nil
	case ed25519SignerTypeURL:
		// https://github.com/google/tink/blob/9753ffddd4d04aa56e0605ff4a0db46f2fb80529/go/signature/ed25519_signer_key_manager.go#L47
		privKey := new(ed25519pb.Ed25519PrivateKey)
		if err := proto.Unmarshal(k.GetValue(), privKey); err != nil {
			return nil, fmt.Errorf("error unmarshalling ed25519 private key: %w", err)
		}
		if err := validateEd25519PrivKey(privKey); err != nil {
			return nil, fmt.Errorf("error validating ed25519 private key: %w", err)
		}
		// https://github.com/google/tink/blob/9753ffddd4d04aa56e0605ff4a0db46f2fb80529/go/signature/subtle/ed25519_signer.go#L29
		p := ed25519.NewKeyFromSeed(privKey.GetKeyValue())
		return p, nil
	default:
		return nil, fmt.Errorf("unsupported key type: %s", k.GetTypeUrl())
	}
}

// getPrimaryKey returns the first enabled key from a keyset.
func getPrimaryKey(ks *tinkpb.Keyset) *tinkpb.KeyData {
	for _, k := range ks.GetKey() {
		if k.GetKeyId() == ks.GetPrimaryKeyId() && k.GetStatus() == tinkpb.KeyStatusType_ENABLED {
			return k.GetKeyData()
		}
	}
	return nil
}

// validateEcdsaPrivKey validates the given ECDSAPrivateKey.
// https://github.com/google/tink/blob/9753ffddd4d04aa56e0605ff4a0db46f2fb80529/go/signature/ecdsa_signer_key_manager.go#L139
func validateEcdsaPrivKey(key *ecdsapb.EcdsaPrivateKey) error {
	if err := keyset.ValidateKeyVersion(key.Version, uint32(ecdsaSignerKeyVersion)); err != nil { //nolint:gosec
		return fmt.Errorf("ecdsa_signer_key_manager: invalid key: %w", err)
	}
	hash, curve, encoding := getECDSAParamNames(key.PublicKey.Params)
	return signatureSubtle.ValidateECDSAParams(hash, curve, encoding)
}

// getECDSAParamNames returns the string representations of each parameter in
// the given ECDSAParams.
// https://github.com/google/tink/blob/4cc630dfc711555f6bbbad64f8c573b39b7af500/go/signature/proto.go#L26
func getECDSAParamNames(params *ecdsapb.EcdsaParams) (string, string, string) {
	hashName := commonpb.HashType_name[int32(params.HashType)]
	curveName := commonpb.EllipticCurveType_name[int32(params.Curve)]
	encodingName := ecdsapb.EcdsaSignatureEncoding_name[int32(params.Encoding)]
	return hashName, curveName, encodingName
}

// validateEd25519PrivKey validates the given ED25519PrivateKey.
// https://github.com/google/tink/blob/9753ffddd4d04aa56e0605ff4a0db46f2fb80529/go/signature/ed25519_signer_key_manager.go#L132
func validateEd25519PrivKey(key *ed25519pb.Ed25519PrivateKey) error {
	if err := keyset.ValidateKeyVersion(key.Version, uint32(ed25519SignerKeyVersion)); err != nil { //nolint:gosec
		return fmt.Errorf("ed25519_signer_key_manager: invalid key: %w", err)
	}
	if len(key.KeyValue) != ed25519.SeedSize {
		return fmt.Errorf("ed2219_signer_key_manager: invalid key length, got %d", len(key.KeyValue))
	}
	return nil
}
