Reuven
|
cb9a9f346c
|
feat(encryption): ADR-096 Phase 1 — vault primitives + tests
Implements the cipher primitives portion of the encryption-at-rest
design (ADR-096). No call sites wired yet — that's a separate iteration
to keep blast radius small. With this commit alone, the env vars are
inert and the 1865-test baseline stays green.
src/encryption/vault.ts (new):
isEncryptionEnabled() — env-var gate (CLAUDE_FLOW_ENCRYPT_AT_REST).
Truthy: "1" / "true" / "yes" / "on" (case+whitespace tolerant).
Anything else, including unset, leaves the legacy plaintext path.
getKey() — resolves a 32-byte key from CLAUDE_FLOW_ENCRYPTION_KEY,
fail-closed when enabled-but-missing. Phase 1 supports env var only;
keychain (keytar) and passphrase-with-scrypt land in a follow-up.
decodeKey(raw) — accepts 64-char hex, 44-char padded base64, or
43-char unpadded base64. Anything else throws. Trims whitespace.
encryptBuffer(plain, key) — AES-256-GCM with a fresh random 12-byte
IV per call. Returns wire-format blob:
magic(4) || iv(12) || ciphertext(N) || tag(16)
where magic = "RFE1" (Ruflo File Encrypted v1). Validates inputs
(Buffer + 32-byte key) so a misuse fails loudly.
decryptBuffer(blob, key) — verifies magic with timingSafeEqual,
parses iv/ct/tag, runs GCM decrypt. Lets GCM auth failure
propagate so tampered ciphertext / wrong key / corrupted IV all
surface as a thrown error rather than silent garbage.
isEncryptedBlob(blob) — magic-byte sniff. True iff blob ≥ 32 bytes
AND starts with "RFE1". Used by readers during the incremental
migration so legacy plaintext .claude-flow/sessions/*.json files
pass through unchanged.
__tests__/encryption-vault.test.ts (new) — 45 cases pinning the
contract:
isEncryptionEnabled: env-var gate w/ truthy/falsy table
decodeKey: hex / padded b64 / unpadded b64 / wrong-length / garbage /
whitespace
getKey: missing-env error message; valid hex returns 32 bytes
round-trip: empty / small text / JSON / 1MB random / fresh-IV
wire format: starts with RFE1 / >= 32 bytes
isEncryptedBlob: detects fresh / rejects JSON / rejects SQLite header
/ rejects too-short / rejects non-Buffer
tamper detection: flipped ct byte / flipped tag / flipped IV /
flipped magic / too-short / wrong key
input validation: non-Buffer plaintext / wrong-size key (encrypt &
decrypt)
Verified:
tsc --noEmit clean
vault tests: 45/45 passing
full vitest suite: 1910/1910 passing, 46 skipped, 0 failures
(was 1865; +45 from this commit)
Phase 2 wires this into session/terminal/memory writers behind the env
gate. Phase 3 adds keychain + passphrase resolution.
|
2026-05-03 21:00:43 -04:00 |
|