package tool import ( "bytes" "crypto/sha256" "errors" "strings" "testing" ) // Why: round-trip is the bedrock — without it, every other test is // meaningless. What: encrypt then decrypt; assert plaintext returns. // Test: write a non-trivial plaintext and confirm exact byte equality. func TestEncryption_RoundTrip(t *testing.T) { t.Parallel() master := masterTestKey() key, err := DeriveSkillKey(master, "skill-abc") if err != nil { t.Fatalf("DeriveSkillKey: %v", err) } plaintext := []byte(`{"hello":"world","n":42,"arr":[1,2,3]}`) envelope, err := Encrypt(key, plaintext) if err != nil { t.Fatalf("Encrypt: %v", err) } if envelope[0] != envelopeFormatV1 { t.Fatalf("envelope[0] = %d, want %d", envelope[0], envelopeFormatV1) } got, err := Decrypt(key, envelope) if err != nil { t.Fatalf("Decrypt: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatalf("round-trip mismatch:\n got: %q\nwant: %q", got, plaintext) } } // Why: GCM is authenticated encryption — flipping any bit MUST be // detected. What: tamper with the ciphertext; assert ErrEncryptionTampered. // Test: flip one byte of the ciphertext suffix, decrypt, expect tamper error. func TestEncryption_TamperDetected(t *testing.T) { t.Parallel() master := masterTestKey() key, err := DeriveSkillKey(master, "skill-tamper") if err != nil { t.Fatalf("DeriveSkillKey: %v", err) } envelope, err := Encrypt(key, []byte("sensitive data")) if err != nil { t.Fatalf("Encrypt: %v", err) } // Flip a byte in the ciphertext (after version + nonce). envelope[1+gcmNonceSize] ^= 0x01 _, err = Decrypt(key, envelope) if !errors.Is(err, ErrEncryptionTampered) { t.Fatalf("Decrypt after tamper = %v, want ErrEncryptionTampered", err) } } // Why: nonce reuse under GCM is catastrophic — verify the impl uses // fresh randomness on every call. What: encrypt the same plaintext twice; // the envelopes must differ. func TestEncryption_FreshNoncePerCall(t *testing.T) { t.Parallel() master := masterTestKey() key, err := DeriveSkillKey(master, "skill-nonce") if err != nil { t.Fatalf("DeriveSkillKey: %v", err) } plaintext := []byte("fixed payload") a, err := Encrypt(key, plaintext) if err != nil { t.Fatalf("Encrypt #1: %v", err) } b, err := Encrypt(key, plaintext) if err != nil { t.Fatalf("Encrypt #2: %v", err) } if bytes.Equal(a, b) { t.Fatalf("two encryptions of the same plaintext produced identical envelopes (nonce not random)") } // Both must still decrypt to the same plaintext. for i, env := range [][]byte{a, b} { got, err := Decrypt(key, env) if err != nil { t.Fatalf("Decrypt #%d: %v", i, err) } if !bytes.Equal(got, plaintext) { t.Fatalf("Decrypt #%d mismatch", i) } } } // Why: per-skill key derivation MUST give different skills different // keys so a leaked skillkey doesn't cross-decrypt. What: derive keys // for skill A and skill B; encrypt under A; decrypt under B; expect // tamper error. func TestEncryption_PerSkillIsolation(t *testing.T) { t.Parallel() master := masterTestKey() keyA, _ := DeriveSkillKey(master, "skill-a") keyB, _ := DeriveSkillKey(master, "skill-b") if bytes.Equal(keyA, keyB) { t.Fatalf("derived keys for distinct skills are identical (HKDF salt not effective)") } envelope, err := Encrypt(keyA, []byte("only skill A may read")) if err != nil { t.Fatalf("Encrypt: %v", err) } _, err = Decrypt(keyB, envelope) if !errors.Is(err, ErrEncryptionTampered) { t.Fatalf("Decrypt under wrong skill key = %v, want ErrEncryptionTampered", err) } } // Why: a future format change must be detectable, not silently // corrupting reads. What: hand-craft an envelope with version byte // 0xFF; assert ErrEncryptionUnknownVersion. func TestEncryption_UnknownVersionRejected(t *testing.T) { t.Parallel() key := make([]byte, 32) envelope := make([]byte, 1+gcmNonceSize+16) envelope[0] = 0xFF _, err := Decrypt(key, envelope) if !errors.Is(err, ErrEncryptionUnknownVersion) { t.Fatalf("Decrypt with bad version = %v, want ErrEncryptionUnknownVersion", err) } } // Why: short inputs must not panic. What: feed a 5-byte envelope to // Decrypt; assert ErrEncryptionShortInput. func TestEncryption_ShortInputRejected(t *testing.T) { t.Parallel() key := make([]byte, 32) _, err := Decrypt(key, []byte{1, 2, 3, 4, 5}) if !errors.Is(err, ErrEncryptionShortInput) { t.Fatalf("Decrypt with short input = %v, want ErrEncryptionShortInput", err) } } // Why: empty master = encryption disabled. What: DeriveSkillKey with // empty master returns ErrEncryptionDisabled. func TestEncryption_EmptyMasterDisabled(t *testing.T) { t.Parallel() _, err := DeriveSkillKey(nil, "skill-x") if !errors.Is(err, ErrEncryptionDisabled) { t.Fatalf("DeriveSkillKey(nil) = %v, want ErrEncryptionDisabled", err) } } // Why: callers commonly paste random hex/base64 of varying length; // MasterKeyFromEnv should normalize to 32 bytes via SHA-256. What: // set the env var; assert returned bytes match SHA-256 of input. func TestEncryption_MasterKeyFromEnvNormalizesLength(t *testing.T) { // not parallel — we mutate process env const raw = "this-is-a-fake-master-key-for-testing-only-totally-not-secure" t.Setenv(EncryptionMasterKeyEnv, raw) got, present := MasterKeyFromEnv() if !present { t.Fatalf("MasterKeyFromEnv reported absent for non-empty env var") } if len(got) != 32 { t.Fatalf("len(masterKey) = %d, want 32", len(got)) } want := sha256.Sum256([]byte(raw)) if !bytes.Equal(got, want[:]) { t.Fatalf("masterKey does not match SHA-256 of env var") } } // Why: empty env var = encryption off (instance-wide). What: // MasterKeyFromEnv returns nil + present=false. func TestEncryption_MasterKeyFromEnvEmpty(t *testing.T) { t.Setenv(EncryptionMasterKeyEnv, "") got, present := MasterKeyFromEnv() if present { t.Fatalf("MasterKeyFromEnv reported present for empty env var") } if got != nil { t.Fatalf("MasterKeyFromEnv returned non-nil bytes for empty env var") } } // Why: defence in depth — explicit non-32 key sizes should error // rather than panic. func TestEncryption_BadKeySize(t *testing.T) { t.Parallel() _, err := Encrypt(make([]byte, 16), []byte("x")) if err == nil { t.Fatalf("Encrypt with 16-byte key did not error") } if !strings.Contains(err.Error(), "32-byte key") { t.Fatalf("error did not mention key size: %v", err) } _, err = Decrypt(make([]byte, 16), make([]byte, 32)) if err == nil { t.Fatalf("Decrypt with 16-byte key did not error") } } func masterTestKey() []byte { // fixed deterministic master so test runs are reproducible. sum := sha256.Sum256([]byte("test-master-do-not-use-in-prod")) return sum[:] }