Files
executus/tool/encryption_test.go
steve dc28b63ad8
executus CI / test (push) Successful in 36s
P1 (part 1): move skilltools core -> tool/ (clean, verbatim)
The tool registry core (registry, permission model, Invocation, gated-tool
wrapper, ssrf guard, hmac, encryption, argcoerce, helpers, rootrun,
session_tools, webhook_rate_limit) had zero mort coupling — it imports only
majordomo/llm + x/crypto/hkdf — so it moves verbatim with a package rename
(skilltools -> tool). All same-package tests came along and pass; the SSRF,
gated-wrapper, encryption and output-pattern invariants are re-anchored here.

majordomo re-enters the module graph (now pinned to the latest, incl. the
front-loaded-output fix). model/ + llmmeta + structured follow next.

Docs: CLAUDE.md now requires README/examples to stay in sync with changes in
the same commit; CI skips docs/example-only pushes via paths-ignore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:31:47 -04:00

206 lines
6.5 KiB
Go

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[:]
}