feat: cache derived keys for faster keying (#10114)

Currently `DeriveKey` is called every time that a secret must be encoded/decoded. Since this function is deterministic, its result can be cached to allow a 250x speedup (the original took less than half a microsecond, so this more of a micro-optimization...).

```
go test -bench=.
goos: linux
goarch: amd64
pkg: forgejo.org/modules/keying
cpu: Intel(R) Core(TM) Ultra 5 125H
BenchmarkExpandPRK-18            2071627               564.2 ns/op
BenchmarkExpandPRKOnce-18       541438192                2.206 ns/op
PASS
ok      forgejo.org/modules/keying      2.369s
```

## Other changes

- Since the keys can be constructed once, it simplifies a bit the callsites (`keying.TOTP.Encrypt(...)` instead of `keying.DeriveKey(keying.ContextTOTP).Encrypt(...)`)
- All `Encrypt`/`Decrypt` calls will panic forever if called before `Init` has been called (current it panics as long as `Init` has not been called)
- Calling `Init` twice with different keys will trigger a panic (currently racy)
- Calling `Decrypt` with a short ciphertext does not panic anymore (like when calling with long-enough garbage)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10114
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: oliverpool <git@olivier.pfad.fr>
Co-committed-by: oliverpool <git@olivier.pfad.fr>
This commit is contained in:
oliverpool 2025-11-16 14:29:14 +01:00 committed by Gusted
commit 67df538958
13 changed files with 151 additions and 118 deletions

View file

@ -120,7 +120,7 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
return nil, err
}
key := keying.DeriveKey(keying.ContextMigrateTask)
key := keying.MigrateTask
// decrypt credentials
if opts.CloneAddrEncrypted != "" {

View file

@ -87,14 +87,12 @@ func (t *TwoFactor) VerifyScratchToken(token string) bool {
// SetSecret sets the 2FA secret.
func (t *TwoFactor) SetSecret(secretString string) {
key := keying.DeriveKey(keying.ContextTOTP)
t.Secret = key.Encrypt([]byte(secretString), keying.ColumnAndID("secret", t.ID))
t.Secret = keying.TOTP.Encrypt([]byte(secretString), keying.ColumnAndID("secret", t.ID))
}
// ValidateTOTP validates the provided passcode.
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
key := keying.DeriveKey(keying.ContextTOTP)
secret, err := key.Decrypt(t.Secret, keying.ColumnAndID("secret", t.ID))
secret, err := keying.TOTP.Decrypt(t.Secret, keying.ColumnAndID("secret", t.ID))
if err != nil {
return false, err
}

View file

@ -33,7 +33,7 @@ func migrateTaskSecrets(x *xorm.Engine) error {
return db.WithTx(db.DefaultContext, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
key := keying.DeriveKey(keying.ContextMigrateTask)
key := keying.MigrateTask
oldEncryptionKey := setting.SecretKey
messages := make([]string, 0, 100)

View file

@ -56,7 +56,7 @@ func Test_MigrateTaskSecretsToKeying(t *testing.T) {
var opts migration.MigrateOptions
require.NoError(t, json.Unmarshal([]byte(task.PayloadContent), &opts))
key := keying.DeriveKey(keying.ContextMigrateTask)
key := keying.MigrateTask
encryptedCloneAddr, err := base64.RawStdEncoding.DecodeString(opts.CloneAddrEncrypted)
require.NoError(t, err)

View file

@ -48,7 +48,7 @@ func Test_MigrateTwoFactorToKeying(t *testing.T) {
_, err = x.Table("two_factor").ID(1).Get(&twofactor)
require.NoError(t, err)
secretBytes, err := keying.DeriveKey(keying.ContextTOTP).Decrypt(twofactor.Secret, keying.ColumnAndID("secret", twofactor.ID))
secretBytes, err := keying.TOTP.Decrypt(twofactor.Secret, keying.ColumnAndID("secret", twofactor.ID))
require.NoError(t, err)
assert.Equal(t, []byte("AVDYS32OPIAYSNBG2NKYV4AHBVEMKKKIGBQ46OXTLMJO664G4TIECOGEANMSNBLS"), secretBytes)
}

View file

@ -46,7 +46,7 @@ func Test_MigrateActionSecretToKeying(t *testing.T) {
_, err = x.Table("secret").ID(1).Get(&secret)
require.NoError(t, err)
secretBytes, err := keying.DeriveKey(keying.ContextActionSecret).Decrypt(secret.Data, keying.ColumnAndID("data", secret.ID))
secretBytes, err := keying.ActionSecret.Decrypt(secret.Data, keying.ColumnAndID("data", secret.ID))
require.NoError(t, err)
assert.Equal(t, []byte("A deep dark secret"), secretBytes)
}

View file

@ -98,8 +98,7 @@ func (m *PushMirror) GetPublicKey() string {
// The ID of the push mirror must be known, so this should be done after the
// push mirror is inserted.
func (m *PushMirror) SetPrivatekey(ctx context.Context, privateKey []byte) error {
key := keying.DeriveKey(keying.ContextPushMirror)
m.PrivateKey = key.Encrypt(privateKey, keying.ColumnAndID("private_key", m.ID))
m.PrivateKey = keying.PushMirror.Encrypt(privateKey, keying.ColumnAndID("private_key", m.ID))
_, err := db.GetEngine(ctx).ID(m.ID).Cols("private_key").Update(m)
return err
@ -107,8 +106,7 @@ func (m *PushMirror) SetPrivatekey(ctx context.Context, privateKey []byte) error
// Privatekey retrieves the encrypted private key and decrypts it.
func (m *PushMirror) Privatekey() ([]byte, error) {
key := keying.DeriveKey(keying.ContextPushMirror)
return key.Decrypt(m.PrivateKey, keying.ColumnAndID("private_key", m.ID))
return keying.PushMirror.Decrypt(m.PrivateKey, keying.ColumnAndID("private_key", m.ID))
}
// UpdatePushMirror updates the push-mirror

View file

@ -117,8 +117,7 @@ func (opts FindSecretsOptions) ToConds() builder.Cond {
}
func (s *Secret) SetSecret(data string) {
key := keying.DeriveKey(keying.ContextActionSecret)
s.Data = key.Encrypt([]byte(data), keying.ColumnAndID("data", s.ID))
s.Data = keying.ActionSecret.Encrypt([]byte(data), keying.ColumnAndID("data", s.ID))
}
func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) {
@ -146,7 +145,7 @@ func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[
return nil, err
}
key := keying.DeriveKey(keying.ContextActionSecret)
key := keying.ActionSecret
for _, secret := range append(ownerSecrets, repoSecrets...) {
v, err := key.Decrypt(secret.Data, keying.ColumnAndID("data", secret.ID))
if err != nil {

View file

@ -25,7 +25,7 @@ func TestInsertEncryptedSecret(t *testing.T) {
assert.Nil(t, secret)
})
key := keying.DeriveKey(keying.ContextActionSecret)
key := keying.ActionSecret
t.Run("Insert repository secret", func(t *testing.T) {
secret, err := InsertEncryptedSecret(t.Context(), 0, 1, "REPO_SECRET", "some repository secret")

View file

@ -16,71 +16,90 @@
package keying
import (
"bytes"
"crypto/cipher"
"crypto/hkdf"
crand "crypto/rand"
"crypto/sha256"
"encoding/binary"
"forgejo.org/modules/util"
"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")
)
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 []byte
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
)
// 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.
var err error
prk, err = hkdf.Extract(hash, ikm, nil)
if err != nil {
panic(err)
}
}
// 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"
// Used for the `secret` table.
ContextActionSecret Context = "action_secret"
// Used for the `task` table where type == TaskTypeMigrateRepo.
ContextMigrateTask Context = "migrate_repo_task"
)
// 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 Context) *Key {
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, string(context), aeadKeySize)
key, err := hkdf.Expand(hash, prk, context, aeadKeySize)
if err != nil {
panic(err)
}
return &Key{key}
e, err := aead(key)
if err != nil {
panic(err)
}
return e
}
type Key struct {
key []byte
type Context struct {
aead func() cipher.AEAD
}
// Encrypts the specified plaintext with some additional data that is tied to
@ -92,36 +111,25 @@ type Key struct {
// 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 := util.CryptoRandomBytes(aeadNonceSize)
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 e.Seal(nonce, nonce, plaintext, additionalData)
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 *Key) Decrypt(ciphertext, additionalData []byte) ([]byte, error) {
func (k Context) 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)
return nil, errors.New("keying: ciphertext is too short")
}
nonce, ciphertext := ciphertext[:aeadNonceSize], ciphertext[aeadNonceSize:]
return e.Open(nil, nonce, ciphertext, additionalData)
return k.aead().Open(nil, nonce, ciphertext, additionalData)
}
// ColumnAndID generates a context that can be used as additional context for

View file

@ -1,33 +1,39 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package keying_test
package keying
import (
"crypto/cipher"
"crypto/hkdf"
"encoding/base64"
"math"
"sync"
"testing"
"forgejo.org/modules/keying"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/chacha20poly1305"
)
func TestKeying(t *testing.T) {
t.Run("Not initialized", func(t *testing.T) {
assert.Panics(t, func() {
keying.DeriveKey(keying.Context("TESTING"))
t.Run("Initialization", func(t *testing.T) {
Init([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07})
})
t.Run("Double initialization", func(t *testing.T) {
t.Run("Same key allowed", func(t *testing.T) {
Init([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07})
})
t.Run("Different key panics", func(t *testing.T) {
assert.Panics(t, func() {
Init([]byte{0x02, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07})
})
})
})
t.Run("Initialization", func(t *testing.T) {
keying.Init([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07})
})
t.Run("Context separation", func(t *testing.T) {
key1 := keying.DeriveKey(keying.Context("TESTING"))
key2 := keying.DeriveKey(keying.Context("TESTING2"))
key1 := deriveKey("TESTING")
key2 := deriveKey("TESTING2")
ciphertext := key1.Encrypt([]byte("This is for context TESTING"), nil)
@ -40,12 +46,10 @@ func TestKeying(t *testing.T) {
assert.EqualValues(t, "This is for context TESTING", plaintext)
})
context := keying.Context("TESTING PURPOSES")
key := deriveKey("TESTING PURPOSES")
plainText := []byte("Forgejo is run by [Redacted]")
var cipherText []byte
t.Run("Encrypt", func(t *testing.T) {
key := keying.DeriveKey(context)
cipherText = key.Encrypt(plainText, []byte{0x05, 0x06})
cipherText2 := key.Encrypt(plainText, []byte{0x05, 0x06})
@ -54,14 +58,21 @@ func TestKeying(t *testing.T) {
})
t.Run("Decrypt", func(t *testing.T) {
key := keying.DeriveKey(context)
t.Run("Successful", func(t *testing.T) {
convertedPlainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06})
require.NoError(t, err)
assert.Equal(t, plainText, convertedPlainText)
})
t.Run("Old secret", func(t *testing.T) {
// ensure that new code can still decode old secrets
known, err := base64.RawStdEncoding.DecodeString("LABcdFTke+FAESOAUkaQvdFO/tLFdugvXHqUYQaESy9eCedUsorjpe1N350NN+AU7gv6xyK3DHuugD+wjnVcNvt+9hA")
require.NoError(t, err)
convertedPlainText, err := key.Decrypt(known, []byte{0x05, 0x06})
require.NoError(t, err)
assert.Equal(t, plainText, convertedPlainText)
})
t.Run("Not enough additional data", func(t *testing.T) {
plainText, err := key.Decrypt(cipherText, []byte{0x05})
require.Error(t, err)
@ -84,48 +95,67 @@ func TestKeying(t *testing.T) {
})
t.Run("Incorrect ciphertext", func(t *testing.T) {
assert.Panics(t, func() {
key.Decrypt(nil, nil)
})
_, err := key.Decrypt(nil, nil)
require.Error(t, err)
assert.Panics(t, func() {
cipherText := make([]byte, chacha20poly1305.NonceSizeX)
key.Decrypt(cipherText, nil)
})
cipherText := make([]byte, chacha20poly1305.NonceSizeX)
_, err = key.Decrypt(cipherText, nil)
require.Error(t, err)
})
})
}
func TestKeyingColumnAndID(t *testing.T) {
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, keying.ColumnAndID("table", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table", math.MaxInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ColumnAndID("table", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndID("table", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ColumnAndID("table", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, ColumnAndID("table", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndID("table", math.MaxInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table2", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table2", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table2", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, keying.ColumnAndID("table2", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table2", math.MaxInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ColumnAndID("table2", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndID("table2", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ColumnAndID("table2", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, ColumnAndID("table2", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndID("table2", math.MaxInt64))
}
func TestColumnAndJSONSelectorAndID(t *testing.T) {
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, keying.ColumnAndJSONSelectorAndID("table", "field1", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndJSONSelectorAndID("table", "field1", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, keying.ColumnAndJSONSelectorAndID("table", "field1", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, keying.ColumnAndJSONSelectorAndID("table", "field1", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndJSONSelectorAndID("table", "field1", math.MaxInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table", "field1", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table", "field1", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table", "field1", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, ColumnAndJSONSelectorAndID("table", "field1", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table", "field1", math.MaxInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, keying.ColumnAndJSONSelectorAndID("table", "field2", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndJSONSelectorAndID("table", "field2", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, keying.ColumnAndJSONSelectorAndID("table", "field2", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, keying.ColumnAndJSONSelectorAndID("table", "field2", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndJSONSelectorAndID("table", "field2", math.MaxInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table", "field2", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table", "field2", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table", "field2", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, ColumnAndJSONSelectorAndID("table", "field2", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table", "field2", math.MaxInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, keying.ColumnAndJSONSelectorAndID("table2", "field1", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndJSONSelectorAndID("table2", "field1", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, keying.ColumnAndJSONSelectorAndID("table2", "field1", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, keying.ColumnAndJSONSelectorAndID("table2", "field1", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndJSONSelectorAndID("table2", "field1", math.MaxInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table2", "field1", math.MinInt64))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table2", "field1", -1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table2", "field1", 0))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, ColumnAndJSONSelectorAndID("table2", "field1", 1))
assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table2", "field1", math.MaxInt64))
}
// 500 ns/op
func BenchmarkExpandPRK(b *testing.B) {
prk, err := hkdf.Extract(hash, []byte("secret"), nil)
require.NoError(b, err)
for b.Loop() {
expandPRK(prk, "testing")
}
}
// 2 ns/op
func BenchmarkExpandPRKOnce(b *testing.B) {
prk, err := hkdf.Extract(hash, []byte("secret"), nil)
require.NoError(b, err)
once := sync.OnceValue(func() cipher.AEAD {
return expandPRK(prk, "testing")
})
for b.Loop() {
once()
}
}

View file

@ -84,7 +84,7 @@ func CreateMigrateTask(ctx context.Context, doer, u *user_model.User, opts base.
return err
}
key := keying.DeriveKey(keying.ContextMigrateTask)
key := keying.MigrateTask
opts.CloneAddrEncrypted = base64.RawStdEncoding.EncodeToString(key.Encrypt([]byte(opts.CloneAddr), keying.ColumnAndJSONSelectorAndID("payload_content", "clone_addr_encrypted", task.ID)))
opts.CloneAddr = util.SanitizeCredentialURLs(opts.CloneAddr)

View file

@ -444,7 +444,7 @@ func loginUserWithTOTP(t testing.TB, user *user_model.User) *TestSession {
twoFactor, err := auth.GetTwoFactorByUID(db.DefaultContext, user.ID)
require.NoError(t, err)
key := keying.DeriveKey(keying.ContextTOTP)
key := keying.TOTP
code, err := key.Decrypt(twoFactor.Secret, keying.ColumnAndID("secret", twoFactor.ID))
require.NoError(t, err)