Changeset 0.9.4 (#54)

This commit is contained in:
2026-02-24 10:44:12 +00:00
parent 90021157e6
commit 5e416d3726
26 changed files with 1333 additions and 108 deletions

241
server/crypto/vault_test.go Normal file
View File

@@ -0,0 +1,241 @@
package crypto
import (
"bytes"
"testing"
)
func TestDeriveKeyFromPassword(t *testing.T) {
salt, err := GenerateSalt()
if err != nil {
t.Fatalf("GenerateSalt: %v", err)
}
key1 := DeriveKeyFromPassword("hunter2", salt)
key2 := DeriveKeyFromPassword("hunter2", salt)
if !bytes.Equal(key1, key2) {
t.Fatal("same password+salt should produce same key")
}
if len(key1) != 32 {
t.Fatalf("expected 32-byte key, got %d", len(key1))
}
// Different password → different key
key3 := DeriveKeyFromPassword("different", salt)
if bytes.Equal(key1, key3) {
t.Fatal("different passwords should produce different keys")
}
// Different salt → different key
salt2, _ := GenerateSalt()
key4 := DeriveKeyFromPassword("hunter2", salt2)
if bytes.Equal(key1, key4) {
t.Fatal("different salts should produce different keys")
}
}
func TestDeriveKeyFromEnv(t *testing.T) {
key1, err := DeriveKeyFromEnv("my-secret-key-123")
if err != nil {
t.Fatalf("DeriveKeyFromEnv: %v", err)
}
if len(key1) != 32 {
t.Fatalf("expected 32-byte key, got %d", len(key1))
}
// Deterministic
key2, _ := DeriveKeyFromEnv("my-secret-key-123")
if !bytes.Equal(key1, key2) {
t.Fatal("same input should produce same key")
}
// Empty → error
_, err = DeriveKeyFromEnv("")
if err != ErrNoEncryptionKey {
t.Fatalf("expected ErrNoEncryptionKey, got %v", err)
}
}
func TestUEKRoundTrip(t *testing.T) {
uek, err := GenerateUEK()
if err != nil {
t.Fatalf("GenerateUEK: %v", err)
}
if len(uek) != 32 {
t.Fatalf("expected 32-byte UEK, got %d", len(uek))
}
salt, _ := GenerateSalt()
pdk := DeriveKeyFromPassword("my-password", salt)
ciphertext, nonce, err := WrapUEK(uek, pdk)
if err != nil {
t.Fatalf("WrapUEK: %v", err)
}
recovered, err := UnwrapUEK(ciphertext, nonce, pdk)
if err != nil {
t.Fatalf("UnwrapUEK: %v", err)
}
if !bytes.Equal(uek, recovered) {
t.Fatal("UEK round-trip mismatch")
}
}
func TestUEKWrongPassword(t *testing.T) {
uek, _ := GenerateUEK()
salt, _ := GenerateSalt()
pdk := DeriveKeyFromPassword("correct-password", salt)
ciphertext, nonce, _ := WrapUEK(uek, pdk)
wrongPDK := DeriveKeyFromPassword("wrong-password", salt)
_, err := UnwrapUEK(ciphertext, nonce, wrongPDK)
if err != ErrDecryptionFailed {
t.Fatalf("expected ErrDecryptionFailed, got %v", err)
}
}
func TestAPIKeyRoundTrip(t *testing.T) {
key, _ := DeriveKeyFromEnv("test-encryption-key")
apiKey := "sk-ant-api03-very-secret-key-1234567890"
ciphertext, nonce, err := Encrypt(apiKey, key)
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
recovered, err := Decrypt(ciphertext, nonce, key)
if err != nil {
t.Fatalf("Decrypt: %v", err)
}
if recovered != apiKey {
t.Fatalf("API key round-trip mismatch: got %q", recovered)
}
}
func TestAPIKeyWrongKey(t *testing.T) {
key1, _ := DeriveKeyFromEnv("key-one")
key2, _ := DeriveKeyFromEnv("key-two")
ciphertext, nonce, _ := Encrypt("secret-api-key", key1)
_, err := Decrypt(ciphertext, nonce, key2)
if err != ErrDecryptionFailed {
t.Fatalf("expected ErrDecryptionFailed, got %v", err)
}
}
func TestAPIKeyCorruption(t *testing.T) {
key, _ := DeriveKeyFromEnv("test-key")
ciphertext, nonce, _ := Encrypt("my-key", key)
// Flip a byte in ciphertext
corrupted := make([]byte, len(ciphertext))
copy(corrupted, ciphertext)
corrupted[0] ^= 0xff
_, err := Decrypt(corrupted, nonce, key)
if err != ErrDecryptionFailed {
t.Fatalf("expected ErrDecryptionFailed for corrupted data, got %v", err)
}
}
func TestEncryptEmpty(t *testing.T) {
key, _ := DeriveKeyFromEnv("test-key")
ciphertext, nonce, err := Encrypt("", key)
if err != nil {
t.Fatalf("Encrypt empty: %v", err)
}
if ciphertext != nil || nonce != nil {
t.Fatal("encrypting empty string should return nil, nil")
}
result, err := Decrypt(nil, nil, key)
if err != nil {
t.Fatalf("Decrypt nil: %v", err)
}
if result != "" {
t.Fatalf("decrypting nil should return empty string, got %q", result)
}
}
func TestUEKUniqueness(t *testing.T) {
uek1, _ := GenerateUEK()
uek2, _ := GenerateUEK()
if bytes.Equal(uek1, uek2) {
t.Fatal("two generated UEKs should not be equal")
}
}
func TestFullPersonalKeyFlow(t *testing.T) {
// Simulate: user registers → UEK generated → UEK wrapped with password
// → user logs in → UEK unwrapped → personal API key encrypted with UEK
// → completion request decrypts API key with UEK
password := "user-secure-password-123"
apiKey := "sk-personal-byok-key-abc123"
// Registration: generate UEK and wrap with password
uek, _ := GenerateUEK()
salt, _ := GenerateSalt()
pdk := DeriveKeyFromPassword(password, salt)
wrappedUEK, uekNonce, _ := WrapUEK(uek, pdk)
// User adds a personal provider: encrypt API key with UEK
keyCiphertext, keyNonce, _ := Encrypt(apiKey, uek)
// --- simulate session boundary ---
// Login: derive PDK from password, unwrap UEK
loginPDK := DeriveKeyFromPassword(password, salt)
sessionUEK, err := UnwrapUEK(wrappedUEK, uekNonce, loginPDK)
if err != nil {
t.Fatalf("login UEK unwrap: %v", err)
}
// Completion request: decrypt API key with session UEK
recovered, err := Decrypt(keyCiphertext, keyNonce, sessionUEK)
if err != nil {
t.Fatalf("API key decrypt: %v", err)
}
if recovered != apiKey {
t.Fatalf("full flow mismatch: got %q", recovered)
}
}
func TestPasswordChangePreservesKeys(t *testing.T) {
// UEK stays the same — only the wrapping changes
oldPassword := "old-password"
newPassword := "new-password"
apiKey := "sk-my-key"
uek, _ := GenerateUEK()
salt, _ := GenerateSalt()
// Wrap with old password
oldPDK := DeriveKeyFromPassword(oldPassword, salt)
_, _, _ = WrapUEK(uek, oldPDK)
// Encrypt API key with UEK
keyCiphertext, keyNonce, _ := Encrypt(apiKey, uek)
// Password change: re-wrap UEK with new password (new salt too)
newSalt, _ := GenerateSalt()
newPDK := DeriveKeyFromPassword(newPassword, newSalt)
newWrappedUEK, newUEKNonce, _ := WrapUEK(uek, newPDK)
// Login with new password: unwrap UEK, decrypt API key
loginPDK := DeriveKeyFromPassword(newPassword, newSalt)
recoveredUEK, _ := UnwrapUEK(newWrappedUEK, newUEKNonce, loginPDK)
recovered, _ := Decrypt(keyCiphertext, keyNonce, recoveredUEK)
if recovered != apiKey {
t.Fatalf("password change broke key access: got %q", recovered)
}
}