loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[SEC] Add `keying` module' (#5041) from gusted/sec-keying into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5041
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

Gusted 61e018f8 a054201e

+214
+5
.deadcode-out
··· 170 170 StdJSON.NewDecoder 171 171 StdJSON.Indent 172 172 173 + code.gitea.io/gitea/modules/keying 174 + DeriveKey 175 + Key.Encrypt 176 + Key.Decrypt 177 + 173 178 code.gitea.io/gitea/modules/markup 174 179 GetRendererByType 175 180 RenderString
+111
modules/keying/keying.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + // Keying is a module that allows for subkeys to be determistically generated 5 + // from the same master key. It allows for domain seperation to take place by 6 + // using new keys for new subsystems/domains. These subkeys are provided with 7 + // an API to encrypt and decrypt data. The module panics if a bad interaction 8 + // happened, the panic should be seen as an non-recoverable error. 9 + // 10 + // HKDF (per RFC 5869) is used to derive new subkeys in a safe manner. It 11 + // provides a KDF security property, which is required for Forgejo, as the 12 + // secret key would be an ASCII string and isn't a random uniform bit string. 13 + // XChaCha-Poly1305 (per draft-irtf-cfrg-xchacha-01) is used as AEAD to encrypt 14 + // and decrypt messages. A new fresh random nonce is generated for every 15 + // encryption. The nonce gets prepended to the ciphertext. 16 + package keying 17 + 18 + import ( 19 + "crypto/rand" 20 + "crypto/sha256" 21 + 22 + "golang.org/x/crypto/chacha20poly1305" 23 + "golang.org/x/crypto/hkdf" 24 + ) 25 + 26 + var ( 27 + // The hash used for HKDF. 28 + hash = sha256.New 29 + // The AEAD used for encryption/decryption. 30 + aead = chacha20poly1305.NewX 31 + aeadKeySize = chacha20poly1305.KeySize 32 + aeadNonceSize = chacha20poly1305.NonceSizeX 33 + // The pseudorandom key generated by HKDF-Extract. 34 + prk []byte 35 + ) 36 + 37 + // Set the main IKM for this module. 38 + func Init(ikm []byte) { 39 + // Salt is intentionally left empty, it's not useful to Forgejo's use case. 40 + prk = hkdf.Extract(hash, ikm, nil) 41 + } 42 + 43 + // Specifies the context for which a subkey should be derived for. 44 + // This must be a hardcoded string and must not be arbitrarily constructed. 45 + type Context string 46 + 47 + // Derive *the* key for a given context, this is a determistic function. The 48 + // same key will be provided for the same context. 49 + func DeriveKey(context Context) *Key { 50 + if len(prk) == 0 { 51 + panic("keying: not initialized") 52 + } 53 + 54 + r := hkdf.Expand(hash, prk, []byte(context)) 55 + 56 + key := make([]byte, aeadKeySize) 57 + // This should never return an error, but if it does, panic. 58 + if _, err := r.Read(key); err != nil { 59 + panic(err) 60 + } 61 + 62 + return &Key{key} 63 + } 64 + 65 + type Key struct { 66 + key []byte 67 + } 68 + 69 + // Encrypts the specified plaintext with some additional data that is tied to 70 + // this plaintext. The additional data can be seen as the context in which the 71 + // data is being encrypted for, this is different than the context for which the 72 + // key was derrived this allows for more granuality without deriving new keys. 73 + // Avoid any user-generated data to be passed into the additional data. The most 74 + // common usage of this would be to encrypt a database field, in that case use 75 + // the ID and database column name as additional data. The additional data isn't 76 + // appended to the ciphertext and may be publicly known, it must be available 77 + // when decryping the ciphertext. 78 + func (k *Key) Encrypt(plaintext, additionalData []byte) []byte { 79 + // Construct a new AEAD with the key. 80 + e, err := aead(k.key) 81 + if err != nil { 82 + panic(err) 83 + } 84 + 85 + // Generate a random nonce. 86 + nonce := make([]byte, aeadNonceSize) 87 + if _, err := rand.Read(nonce); err != nil { 88 + panic(err) 89 + } 90 + 91 + // Returns the ciphertext of this plaintext. 92 + return e.Seal(nonce, nonce, plaintext, additionalData) 93 + } 94 + 95 + // Decrypts the ciphertext and authenticates it against the given additional 96 + // data that was given when it was encrypted. It returns an error if the 97 + // authentication failed. 98 + func (k *Key) Decrypt(ciphertext, additionalData []byte) ([]byte, error) { 99 + if len(ciphertext) <= aeadNonceSize { 100 + panic("keying: ciphertext is too short") 101 + } 102 + 103 + e, err := aead(k.key) 104 + if err != nil { 105 + panic(err) 106 + } 107 + 108 + nonce, ciphertext := ciphertext[:aeadNonceSize], ciphertext[aeadNonceSize:] 109 + 110 + return e.Open(nil, nonce, ciphertext, additionalData) 111 + }
+96
modules/keying/keying_test.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package keying_test 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/modules/keying" 10 + 11 + "github.com/stretchr/testify/assert" 12 + "github.com/stretchr/testify/require" 13 + "golang.org/x/crypto/chacha20poly1305" 14 + ) 15 + 16 + func TestKeying(t *testing.T) { 17 + t.Run("Not initalized", func(t *testing.T) { 18 + assert.Panics(t, func() { 19 + keying.DeriveKey(keying.Context("TESTING")) 20 + }) 21 + }) 22 + 23 + t.Run("Initialization", func(t *testing.T) { 24 + keying.Init([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}) 25 + }) 26 + 27 + t.Run("Context seperation", func(t *testing.T) { 28 + key1 := keying.DeriveKey(keying.Context("TESTING")) 29 + key2 := keying.DeriveKey(keying.Context("TESTING2")) 30 + 31 + ciphertext := key1.Encrypt([]byte("This is for context TESTING"), nil) 32 + 33 + plaintext, err := key2.Decrypt(ciphertext, nil) 34 + require.Error(t, err) 35 + assert.Empty(t, plaintext) 36 + 37 + plaintext, err = key1.Decrypt(ciphertext, nil) 38 + require.NoError(t, err) 39 + assert.EqualValues(t, "This is for context TESTING", plaintext) 40 + }) 41 + 42 + context := keying.Context("TESTING PURPOSES") 43 + plainText := []byte("Forgejo is run by [Redacted]") 44 + var cipherText []byte 45 + t.Run("Encrypt", func(t *testing.T) { 46 + key := keying.DeriveKey(context) 47 + 48 + cipherText = key.Encrypt(plainText, []byte{0x05, 0x06}) 49 + cipherText2 := key.Encrypt(plainText, []byte{0x05, 0x06}) 50 + 51 + // Ensure ciphertexts don't have an determistic output. 52 + assert.NotEqualValues(t, cipherText, cipherText2) 53 + }) 54 + 55 + t.Run("Decrypt", func(t *testing.T) { 56 + key := keying.DeriveKey(context) 57 + 58 + t.Run("Succesful", func(t *testing.T) { 59 + convertedPlainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06}) 60 + require.NoError(t, err) 61 + assert.EqualValues(t, plainText, convertedPlainText) 62 + }) 63 + 64 + t.Run("Not enougn additional data", func(t *testing.T) { 65 + plainText, err := key.Decrypt(cipherText, []byte{0x05}) 66 + require.Error(t, err) 67 + assert.Empty(t, plainText) 68 + }) 69 + 70 + t.Run("Too much additional data", func(t *testing.T) { 71 + plainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06, 0x07}) 72 + require.Error(t, err) 73 + assert.Empty(t, plainText) 74 + }) 75 + 76 + t.Run("Incorrect nonce", func(t *testing.T) { 77 + // Flip the first byte of the nonce. 78 + cipherText[0] = ^cipherText[0] 79 + 80 + plainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06}) 81 + require.Error(t, err) 82 + assert.Empty(t, plainText) 83 + }) 84 + 85 + t.Run("Incorrect ciphertext", func(t *testing.T) { 86 + assert.Panics(t, func() { 87 + key.Decrypt(nil, nil) 88 + }) 89 + 90 + assert.Panics(t, func() { 91 + cipherText := make([]byte, chacha20poly1305.NonceSizeX) 92 + key.Decrypt(cipherText, nil) 93 + }) 94 + }) 95 + }) 96 + }
+2
modules/setting/security.go
··· 10 10 11 11 "code.gitea.io/gitea/modules/auth/password/hash" 12 12 "code.gitea.io/gitea/modules/generate" 13 + "code.gitea.io/gitea/modules/keying" 13 14 "code.gitea.io/gitea/modules/log" 14 15 ) 15 16 ··· 110 111 // Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value 111 112 SecretKey = "!#@FDEWREWR&*(" //nolint:gosec 112 113 } 114 + keying.Init([]byte(SecretKey)) 113 115 114 116 CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible") 115 117