// Copyright 2020 Cloudflare, Inc. All rights reserved. Use of this source code
// is governed by a BSD-style license that can be found in the LICENSE file.

package tls

import (
	"errors"
	"fmt"

	"github.com/cloudflare/circl/hpke"
	"github.com/cloudflare/circl/kem"

	"golang.org/x/crypto/cryptobyte"
)

// ECHProvider specifies the interface of an ECH service provider that decrypts
// the ECH payload on behalf of the client-facing server. It also defines the
// set of acceptable ECH configurations.
type ECHProvider interface {
	// GetDecryptionContext attempts to construct the HPKE context used by the
	// client-facing server for decryption. (See draft-irtf-cfrg-hpke-07,
	// Section 5.2.)
	//
	// handle encodes the parameters of the client's "encrypted_client_hello"
	// extension that are needed to construct the context. Since
	// draft-ietf-tls-esni-10 these are the ECH cipher suite, the identity of
	// the ECH configuration, and the encapsulated key.
	//
	// version is the version of ECH indicated by the client.
	//
	// res.Status == ECHProviderStatusSuccess indicates the call was successful
	// and the caller may proceed. res.Context is set.
	//
	// res.Status == ECHProviderStatusReject indicates the caller must reject
	// ECH. res.RetryConfigs may be set.
	//
	// res.Status == ECHProviderStatusAbort indicates the caller should abort
	// the handshake.  Note that, in some cases, it's appropriate to reject
	// rather than abort. In particular, aborting with "illegal_parameter" might
	// "stick out". res.Alert and res.Error are set.
	GetDecryptionContext(handle []byte, version uint16) (res ECHProviderResult)
}

// ECHProviderStatus is the status of the ECH provider's response.
type ECHProviderStatus uint

const (
	ECHProviderSuccess ECHProviderStatus = 0
	ECHProviderReject                    = 1
	ECHProviderAbort                     = 2

	errHPKEInvalidPublicKey = "hpke: invalid KEM public key"
)

// ECHProviderResult represents the result of invoking the ECH provider.
type ECHProviderResult struct {
	Status ECHProviderStatus

	// Alert is the TLS alert sent by the caller when aborting the handshake.
	Alert uint8

	// Error is the error propagated by the caller when aborting the handshake.
	Error error

	// RetryConfigs is the sequence of ECH configs to offer to the client for
	// retrying the handshake. This may be set in case of success or rejection.
	RetryConfigs []byte

	// Context is the server's HPKE context. This is set if ECH is not rejected
	// by the provider and no error was reported. The data has the following
	// format (in TLS syntax):
	//
	// enum { sealer(0), opener(1) } HpkeRole;
	//
	// struct {
	//     HpkeRole role;
	//     HpkeKemId kem_id;   // as defined in draft-irtf-cfrg-hpke-07
	//     HpkeKdfId kdf_id;   // as defined in draft-irtf-cfrg-hpke-07
	//     HpkeAeadId aead_id; // as defined in draft-irtf-cfrg-hpke-07
	//     opaque exporter_secret<0..255>;
	//     opaque key<0..255>;
	//     opaque base_nonce<0..255>;
	//     opaque seq<0..255>;
	// } HpkeContext;
	Context []byte
}

// EXP_ECHKeySet implements the ECHProvider interface for a sequence of ECH keys.
//
// NOTE: This API is EXPERIMENTAL and subject to change.
type EXP_ECHKeySet struct {
	// The serialized ECHConfigs, in order of the server's preference.
	configs []byte

	// Maps a configuration identifier to its secret key.
	sk map[uint8]EXP_ECHKey
}

// EXP_NewECHKeySet constructs an EXP_ECHKeySet.
func EXP_NewECHKeySet(keys []EXP_ECHKey) (*EXP_ECHKeySet, error) {
	if len(keys) > 255 {
		return nil, fmt.Errorf("tls: ech provider: unable to support more than 255 ECH configurations at once")
	}

	keySet := new(EXP_ECHKeySet)
	keySet.sk = make(map[uint8]EXP_ECHKey)
	configs := make([]byte, 0)
	for _, key := range keys {
		if _, ok := keySet.sk[key.config.configId]; ok {
			return nil, fmt.Errorf("tls: ech provider: ECH config conflict for configId %d", key.config.configId)
		}

		keySet.sk[key.config.configId] = key
		configs = append(configs, key.config.raw...)
	}

	var b cryptobyte.Builder
	b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
		b.AddBytes(configs)
	})
	keySet.configs = b.BytesOrPanic()

	return keySet, nil
}

// GetDecryptionContext is required by the ECHProvider interface.
func (keySet *EXP_ECHKeySet) GetDecryptionContext(rawHandle []byte, version uint16) (res ECHProviderResult) {
	// Propagate retry configurations regardless of the result. The caller sends
	// these to the clients only if it rejects.
	res.RetryConfigs = keySet.configs

	// Ensure we know how to proceed, i.e., the caller has indicated a supported
	// version of ECH. Currently only draft-ietf-tls-esni-13 is supported.
	if version != extensionECH {
		res.Status = ECHProviderAbort
		res.Alert = uint8(alertInternalError)
		res.Error = errors.New("version not supported")
		return // Abort
	}

	// Parse the handle.
	s := cryptobyte.String(rawHandle)
	handle := new(echContextHandle)
	if !echReadContextHandle(&s, handle) || !s.Empty() {
		// This is the result of a client-side error. However, aborting with
		// "illegal_parameter" would stick out, so we reject instead.
		res.Status = ECHProviderReject
		res.RetryConfigs = keySet.configs
		return // Reject
	}
	handle.raw = rawHandle

	// Look up the secret key for the configuration indicated by the client.
	key, ok := keySet.sk[handle.configId]
	if !ok {
		res.Status = ECHProviderReject
		res.RetryConfigs = keySet.configs
		return // Reject
	}

	// Ensure that support for the selected ciphersuite is indicated by the
	// configuration.
	suite := handle.suite
	if !key.config.isPeerCipherSuiteSupported(suite) {
		// This is the result of a client-side error. However, aborting with
		// "illegal_parameter" would stick out, so we reject instead.
		res.Status = ECHProviderReject
		res.RetryConfigs = keySet.configs
		return // Reject
	}

	// Ensure the version indicated by the client matches the version supported
	// by the configuration.
	if version != key.config.version {
		// This is the result of a client-side error. However, aborting with
		// "illegal_parameter" would stick out, so we reject instead.
		res.Status = ECHProviderReject
		res.RetryConfigs = keySet.configs
		return // Reject
	}

	// Compute the decryption context.
	opener, err := key.setupOpener(handle.enc, suite)
	if err != nil {
		if err.Error() == errHPKEInvalidPublicKey {
			// This occurs if the KEM algorithm used to generate handle.enc is
			// not the same as the KEM algorithm of the key. One way this can
			// happen is if the client sent a GREASE ECH extension with a
			// config_id that happens to match a known config, but which uses a
			// different KEM algorithm.
			res.Status = ECHProviderReject
			res.RetryConfigs = keySet.configs
			return // Reject
		}

		res.Status = ECHProviderAbort
		res.Alert = uint8(alertInternalError)
		res.Error = err
		return // Abort
	}

	// Serialize the decryption context.
	res.Context, err = opener.MarshalBinary()
	if err != nil {
		res.Status = ECHProviderAbort
		res.Alert = uint8(alertInternalError)
		res.Error = err
		return // Abort
	}

	res.Status = ECHProviderSuccess
	return // Success
}

// EXP_ECHKey represents an ECH key and its corresponding configuration. The
// encoding of an ECH Key has the format defined below (in TLS syntax). Note
// that the ECH standard does not specify this format.
//
//	struct {
//	    opaque sk<0..2^16-1>;
//	    ECHConfig config<0..2^16>; // draft-ietf-tls-esni-13
//	} ECHKey;
type EXP_ECHKey struct {
	sk     kem.PrivateKey
	config ECHConfig
}

// EXP_UnmarshalECHKeys parses a sequence of ECH keys.
func EXP_UnmarshalECHKeys(raw []byte) ([]EXP_ECHKey, error) {
	var (
		err                  error
		key                  EXP_ECHKey
		sk, config, contents cryptobyte.String
	)
	s := cryptobyte.String(raw)
	keys := make([]EXP_ECHKey, 0)
KeysLoop:
	for !s.Empty() {
		if !s.ReadUint16LengthPrefixed(&sk) ||
			!s.ReadUint16LengthPrefixed(&config) {
			return nil, errors.New("error parsing key")
		}

		key.config.raw = config
		if !config.ReadUint16(&key.config.version) ||
			!config.ReadUint16LengthPrefixed(&contents) ||
			!config.Empty() {
			return nil, errors.New("error parsing config")
		}

		if key.config.version != extensionECH {
			continue KeysLoop
		}
		if !readConfigContents(&contents, &key.config) {
			return nil, errors.New("error parsing config contents")
		}

		for _, suite := range key.config.suites {
			if !hpke.KDF(suite.kdfId).IsValid() ||
				!hpke.AEAD(suite.aeadId).IsValid() {
				continue KeysLoop
			}
		}

		kem := hpke.KEM(key.config.kemId)
		if !kem.IsValid() {
			continue KeysLoop
		}
		key.config.pk, err = kem.Scheme().UnmarshalBinaryPublicKey(key.config.rawPublicKey)
		if err != nil {
			return nil, fmt.Errorf("error parsing public key: %s", err)
		}
		key.sk, err = kem.Scheme().UnmarshalBinaryPrivateKey(sk)
		if err != nil {
			return nil, fmt.Errorf("error parsing secret key: %s", err)
		}

		keys = append(keys, key)
	}
	return keys, nil
}

// setupOpener computes the HPKE context used by the server in the ECH
// extension.i
func (key *EXP_ECHKey) setupOpener(enc []byte, suite hpkeSymmetricCipherSuite) (hpke.Opener, error) {
	if key.config.raw == nil {
		panic("raw config not set")
	}
	hpkeSuite, err := hpkeAssembleSuite(
		key.config.kemId,
		suite.kdfId,
		suite.aeadId,
	)
	if err != nil {
		return nil, err
	}
	info := append(append([]byte(echHpkeInfoSetup), 0), key.config.raw...)
	receiver, err := hpkeSuite.NewReceiver(key.sk, info)
	if err != nil {
		return nil, err
	}
	return receiver.Setup(enc)
}
