You've already forked agentic-coding-workflow
142 lines
3.7 KiB
Go
142 lines
3.7 KiB
Go
package crypto
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// mutableStore is a KeyStore whose active version can change at runtime,
|
|
// modeling a rotation.
|
|
type mutableStore struct {
|
|
mu sync.Mutex
|
|
v int
|
|
}
|
|
|
|
func (m *mutableStore) ActiveVersion(context.Context) (int, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.v, nil
|
|
}
|
|
|
|
func (m *mutableStore) set(v int) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.v = v
|
|
}
|
|
|
|
func rawKey(t *testing.T) []byte {
|
|
t.Helper()
|
|
k := make([]byte, 32)
|
|
_, err := rand.Read(k)
|
|
require.NoError(t, err)
|
|
return k
|
|
}
|
|
|
|
func TestKeyedEncryptor_RoundTripV1AndV2(t *testing.T) {
|
|
ctx := context.Background()
|
|
prov := NewEnvProvider(nil)
|
|
prov.SetKey(1, rawKey(t))
|
|
prov.SetKey(2, rawKey(t))
|
|
store := &mutableStore{v: 1}
|
|
ke := NewKeyedEncryptor(prov, store)
|
|
|
|
pt := []byte("super secret value")
|
|
|
|
ctV1, v, err := ke.EncryptVersion(ctx, pt)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, v)
|
|
got, err := ke.DecryptVersion(ctx, ctV1, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, pt, got)
|
|
|
|
store.set(2)
|
|
ctV2, v2, err := ke.EncryptVersion(ctx, pt)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, v2)
|
|
require.NotEqual(t, ctV1, ctV2)
|
|
got2, err := ke.DecryptVersion(ctx, ctV2, 2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, pt, got2)
|
|
}
|
|
|
|
func TestKeyedEncryptor_V1BlobDecryptsAfterActiveBumpsToV2(t *testing.T) {
|
|
ctx := context.Background()
|
|
prov := NewEnvProvider(nil)
|
|
prov.SetKey(1, rawKey(t))
|
|
prov.SetKey(2, rawKey(t))
|
|
store := &mutableStore{v: 1}
|
|
ke := NewKeyedEncryptor(prov, store)
|
|
|
|
pt := []byte("encrypted under v1")
|
|
ctV1, v, err := ke.EncryptVersion(ctx, pt)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, v)
|
|
|
|
// Rotate the active write version; the OLD blob must still decrypt under v1.
|
|
store.set(2)
|
|
got, err := ke.DecryptVersion(ctx, ctV1, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, pt, got)
|
|
}
|
|
|
|
func TestKeyedEncryptor_WrongVersionDecryptFailsCleanly(t *testing.T) {
|
|
ctx := context.Background()
|
|
prov := NewEnvProvider(nil)
|
|
prov.SetKey(1, rawKey(t))
|
|
prov.SetKey(2, rawKey(t))
|
|
ke := NewKeyedEncryptor(prov, NewStaticKeyStore(1))
|
|
|
|
ct, _, err := ke.EncryptVersion(ctx, []byte("x"))
|
|
require.NoError(t, err)
|
|
// Decrypting a v1 blob as v2 must fail authentication, not panic.
|
|
_, err = ke.DecryptVersion(ctx, ct, 2)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestKeyedEncryptor_UnknownVersionErrors(t *testing.T) {
|
|
ctx := context.Background()
|
|
prov := NewEnvProvider(nil)
|
|
prov.SetKey(1, rawKey(t))
|
|
ke := NewKeyedEncryptor(prov, NewStaticKeyStore(1))
|
|
_, err := ke.DecryptVersion(ctx, make([]byte, 16), 9)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestEncryptorShim_BacksOntoKeyedV1(t *testing.T) {
|
|
// The legacy Encryptor shim must remain byte-compatible: a blob it produces
|
|
// decrypts via the underlying KeyedEncryptor at version 1, and vice versa.
|
|
key := rawKey(t)
|
|
enc, err := NewEncryptor(key)
|
|
require.NoError(t, err)
|
|
|
|
pt := []byte("legacy callers see no versions")
|
|
ct, err := enc.Encrypt(pt)
|
|
require.NoError(t, err)
|
|
|
|
got, err := enc.Decrypt(ct)
|
|
require.NoError(t, err)
|
|
require.Equal(t, pt, got)
|
|
|
|
// Same key via KeyedEncryptor v1 opens the shim's ciphertext.
|
|
prov := NewEnvProvider(key)
|
|
ke := NewKeyedEncryptor(prov, NewStaticKeyStore(1))
|
|
got2, err := ke.DecryptVersion(context.Background(), ct, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, pt, got2)
|
|
}
|
|
|
|
func TestEnvProvider_ReadsVersionedEnv(t *testing.T) {
|
|
t.Setenv("APP_MASTER_KEY_V2", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
|
|
prov := NewEnvProvider(nil)
|
|
k, err := prov.KeyMaterial(context.Background(), 2)
|
|
require.NoError(t, err)
|
|
require.Len(t, k, 32)
|
|
|
|
_, err = prov.KeyMaterial(context.Background(), 3)
|
|
require.Error(t, err, "unstaged version must error, not silently use a default")
|
|
}
|