Files
2026-06-22 08:55:57 +08:00

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")
}