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