Changeset 0.9.4 (#54)
This commit is contained in:
241
server/crypto/vault_test.go
Normal file
241
server/crypto/vault_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user