summaryrefslogtreecommitdiffstats
path: root/modules/keying/keying.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/keying/keying.go')
-rw-r--r--modules/keying/keying.go125
1 files changed, 125 insertions, 0 deletions
diff --git a/modules/keying/keying.go b/modules/keying/keying.go
new file mode 100644
index 0000000..7c595c7
--- /dev/null
+++ b/modules/keying/keying.go
@@ -0,0 +1,125 @@
+// 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
+
+// Used for the `push_mirror` table.
+var ContextPushMirror Context = "pushmirror"
+
+// 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))
+}