// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT // Keying is a module that allows for subkeys to be determistically generated // from the same master key. It allows for domain seperation 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 ( "crypto/rand" "crypto/sha256" "encoding/binary" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" ) var ( // The hash used for HKDF. hash = sha256.New // The AEAD used for encryption/decryption. aead = chacha20poly1305.NewX aeadKeySize = chacha20poly1305.KeySize aeadNonceSize = chacha20poly1305.NonceSizeX // The pseudorandom key generated by HKDF-Extract. prk []byte ) // 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. prk = hkdf.Extract(hash, ikm, nil) } // Specifies the context for which a subkey should be derived for. // This must be a hardcoded string and must not be arbitrarily constructed. type Context string var ( // Used for the `push_mirror` table. ContextPushMirror Context = "pushmirror" // Used for the `two_factor` table. ContextTOTP Context = "totp" ) // Derive *the* key for a given context, this is a determistic function. The // same key will be provided for the same context. func DeriveKey(context Context) *Key { if len(prk) == 0 { panic("keying: not initialized") } r := hkdf.Expand(hash, prk, []byte(context)) key := make([]byte, aeadKeySize) // This should never return an error, but if it does, panic. if _, err := r.Read(key); err != nil { panic(err) } return &Key{key} } type Key struct { key []byte } // 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 derrived this allows for more granuality 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 *Key) Encrypt(plaintext, additionalData []byte) []byte { // Construct a new AEAD with the key. e, err := aead(k.key) if err != nil { panic(err) } // Generate a random nonce. nonce := make([]byte, aeadNonceSize) if _, err := rand.Read(nonce); err != nil { panic(err) } // Returns the ciphertext of this plaintext. return e.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 *Key) Decrypt(ciphertext, additionalData []byte) ([]byte, error) { if len(ciphertext) <= aeadNonceSize { panic("keying: ciphertext is too short") } e, err := aead(k.key) if err != nil { panic(err) } nonce, ciphertext := ciphertext[:aeadNonceSize], ciphertext[aeadNonceSize:] return e.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)) }