// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT

// Keying is a module that allows for subkeys to be deterministically generated
// from the same master key. It allows for domain separation to take place by
// using new keys for new subsystems/domains. These subkeys are provided with
// an API to encrypt and decrypt data. The module panics if a bad interaction
// happened, the panic should be seen as an non-recoverable error.
//
// HKDF (per RFC 5869) is used to derive new subkeys in a safe manner. It
// provides a KDF security property, which is required for Forgejo, as the
// secret key would be an ASCII string and isn't a random uniform bit string.
// XChaCha-Poly1305 (per draft-irtf-cfrg-xchacha-01) is used as AEAD to encrypt
// and decrypt messages. A new fresh random nonce is generated for every
// encryption. The nonce gets prepended to the ciphertext.
package keying

import (
	"bytes"
	"crypto/cipher"
	"crypto/hkdf"
	crand "crypto/rand"
	"crypto/sha256"
	"encoding/binary"
	"errors"
	"sync"
	"sync/atomic"

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

// Specifies the context for which a subkey should be derived for.
var (
	// Used for the `push_mirror` table.
	PushMirror = deriveKey("pushmirror")
	// Used for the `two_factor` table.
	TOTP = deriveKey("totp")
	// Used for the `secret` table.
	ActionSecret = deriveKey("action_secret")
	// Used for the `task` table where type == TaskTypeMigrateRepo.
	MigrateTask = deriveKey("migrate_repo_task")
	// Used for the `webhook` table.
	Webhook = deriveKey("webhook")
	// Used for the `mirror` table.
	PullMirror = deriveKey("pullmirror")
)

var (
	// The hash used for HKDF.
	hash = sha256.New
	// The AEAD used for encryption/decryption.
	aead = chacha20poly1305.NewX
	// The pseudorandom key generated by HKDF-Extract.
	prk atomic.Value
)

// Set the main IKM for this module.
func Init(ikm []byte) {
	// Salt is intentionally left empty, it's not useful to Forgejo's use case.
	buf, err := hkdf.Extract(hash, ikm, nil)
	if err != nil {
		panic(err)
	}
	if ok := prk.CompareAndSwap(nil, buf); ok {
		return
	}
	// prk was already set
	old := prk.Load().([]byte)
	if bytes.Equal(old, buf) {
		return
	}
	panic("main IKM cannot be updated at runtime")
}

const (
	aeadKeySize   = chacha20poly1305.KeySize
	aeadNonceSize = chacha20poly1305.NonceSizeX
)

// Derive *the* key for a given context, this is a deterministic function.
// The same key will be provided for the same context.
func deriveKey(context string) Context {
	// wrap another sync.Once to prevent panic on initialization (prk would be nil)
	return Context{sync.OnceValue(func() cipher.AEAD {
		return expandPRK(prk.Load().([]byte), context)
	})}
}

func expandPRK(prk []byte, context string) cipher.AEAD {
	if len(prk) != sha256.Size {
		panic("keying: not initialized")
	}

	key, err := hkdf.Expand(hash, prk, context, aeadKeySize)
	if err != nil {
		panic(err)
	}
	e, err := aead(key)
	if err != nil {
		panic(err)
	}
	return e
}

type Context struct {
	aead func() cipher.AEAD
}

// Encrypts the specified plaintext with some additional data that is tied to
// this plaintext. The additional data can be seen as the context in which the
// data is being encrypted for, this is different than the context for which the
// key was derived; this allows for more granularity without deriving new keys.
// Avoid any user-generated data to be passed into the additional data. The most
// common usage of this would be to encrypt a database field, in that case use
// the ID and database column name as additional data. The additional data isn't
// appended to the ciphertext and may be publicly known, it must be available
// when decryping the ciphertext.
func (k Context) Encrypt(plaintext, additionalData []byte) []byte {
	nonce := make([]byte, aeadNonceSize)
	_, _ = crand.Read(nonce) // never returns an error

	// Returns the ciphertext of this plaintext.
	return k.aead().Seal(nonce, nonce, plaintext, additionalData)
}

// Decrypts the ciphertext and authenticates it against the given additional
// data that was given when it was encrypted. It returns an error if the
// authentication failed.
func (k Context) Decrypt(ciphertext, additionalData []byte) ([]byte, error) {
	if len(ciphertext) <= aeadNonceSize {
		return nil, errors.New("keying: ciphertext is too short")
	}

	nonce, ciphertext := ciphertext[:aeadNonceSize], ciphertext[aeadNonceSize:]

	return k.aead().Open(nil, nonce, ciphertext, additionalData)
}

// ColumnAndID generates a context that can be used as additional context for
// encrypting and decrypting data. It requires the column name and the row ID
// (this requires to be known beforehand). Be careful when using this, as the
// table name isn't part of this context. This means it's not bound to a
// particular table. The table should be part of the context that the key was
// derived for, in which case it binds through that.
func ColumnAndID(column string, id int64) []byte {
	return binary.BigEndian.AppendUint64(append([]byte(column), ':'), uint64(id))
}

// ColumnAndJSONSelectorAndID generates a context that can be used as additional context
// for encrypting and decrypting data. It requires the column name, JSON
// selector and the row ID (this requires to be known beforehand). Be careful
// when using this, as the table name isn't part of this context. This means
// it's not bound to a particular table. The table should be part of the context
// that the key was derived for, in which case it binds through that. Use this
// over `ColumnAndID` if you're encrypting data that's stored inside JSON.
// jsonSelector must be a unambiguous selector to the JSON field that stores the
// encrypted data.
func ColumnAndJSONSelectorAndID(column, jsonSelector string, id int64) []byte {
	return binary.BigEndian.AppendUint64(append(append([]byte(column), ':'), append([]byte(jsonSelector), ':')...), uint64(id))
}
