feat(B-0852.1): TS crypto module — HKDF-SHA256 + AES-256-GCM for credential persistence (pure functions; 18 unit tests; smallest concrete substrate slice)#5411
Merged
Conversation
… persistence
Smallest concrete substrate slice of B-0852 (credential persistence on USB
ESP). Pure functions; no I/O. 18 unit tests covering round-trip + wrong-
passphrase + wrong-UUID + tamper-rejection + size validation + empty/large
plaintext cases.
Two files:
- tools/installer/zeta-creds-crypto.ts — deriveKey + encrypt + decrypt
using node:crypto's hkdfSync + createCipheriv("aes-256-gcm", ...). Pure
module; no FS, no side effects. Result type Buffer | { error } for
decrypt (substrate-honest: failure IS a value, no try/catch at callers).
- tools/installer/zeta-creds-crypto.test.ts — 18 acceptance tests
validating the threat model claims in the module's header.
Threat model (Phase 1 scope per row body):
- HKDF binds key to (USB UUID || passphrase) — copying ESP contents to a
different-UUID USB cannot decrypt (defeats "copy to uuid" attack named
by Aaron 2026-05-27: "we can put a key on the usb too if wnated tied to
the uuid so it can't be copied to uuid")
- AES-GCM authenticated encryption rejects tampered ciphertext/tag/salt
- Wrong passphrase → different derived key → GCM auth tag fails → error
(NOT garbled plaintext)
Phase 3 (NOT this row): hardware-bound keys (TPM / YubiKey / Touch-ID-
derived) defeat "attacker stole USB AND knows passphrase AND knows UUID"
case; row body explicitly defers this to Phase 3.
Composes with:
- B-0852 parent row (this is sub-row .1; smallest concrete slice)
- B-0852.2 (TS persist/restore CLIs; consume encrypt + decrypt)
- B-0852.5 (cred-manifest schema; pure declarative; sibling module)
- node:crypto hkdfSync (standard primitive; no third-party dep)
Test output: 18 pass / 0 fail / 23 expect() calls / 103ms.
Format: prettier-clean.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…HKDF — passphrase-based KDF discipline
Security review HIGH finding 2026-05-27:
> Insecure Password Hashing / Weak KDF — HKDF assumes high-entropy IKM.
> Operator passphrases are low-entropy; HKDF alone is brute-force vulnerable.
Two-layer derivation now:
1. scrypt(passphrase, salt, 32, {N: 2^17, r: 8, p: 1, maxmem: 256MB})
→ stretched 32 bytes (memory-hard; ~128MB per guess; ~1-2s per derivation)
2. HKDF-SHA256(usbUuid || stretched, salt, info=HKDF_INFO)
→ 32 byte AES-256 key (binds to USB UUID for copy-attack defense)
OWASP 2026 recommended scrypt parameters. ~1-2s per operator boot is
acceptable; brute-force cost on attacker is the load-bearing defense.
Test impact: suite went from 103ms → 5.86s (scrypt called ~36 times across
18 tests). Acceptable for CI (well under timeout); operationally meaningful
security improvement.
All 18 tests still pass. Round-trip + wrong-pass + wrong-UUID + tamper
rejections + size validation all hold.
Composes with:
- B-0852 row's Phase 3 deferral note (hardware-bound keys still future)
- node:crypto scryptSync (standard primitive; no third-party dep)
Per .claude/rules/methodology-hard-limits.md: substrate-honest discipline
includes upgrading security when review surfaces real attack class.
There was a problem hiding this comment.
Pull request overview
Adds a small, pure TypeScript crypto substrate for B-0852 credential persistence, implementing key derivation + authenticated encryption and validating the Phase-1 threat model via Bun unit tests.
Changes:
- Introduces
deriveKey(HKDF-SHA256) andencrypt/decrypt(AES-256-GCM) as pure functions with a structured envelope shape. - Adds 18 Bun acceptance tests covering round-trips, wrong passphrase/UUID, and tamper rejection cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| tools/installer/zeta-creds-crypto.ts | New pure crypto module: HKDF-based key derivation and AES-256-GCM encrypt/decrypt with envelope output. |
| tools/installer/zeta-creds-crypto.test.ts | New Bun test suite validating round-trip behavior and tamper/wrong-key rejection properties. |
…oken test fixture + wrong-length-salt test PR #5411 Copilot review: P1 (correctness): decrypt() called deriveKey(envelope.salt) without validating salt length. deriveKey throws on wrong-length salt → defeats the "no try/catch at callers" contract. Fix: validate salt.length up front; return structured { error } on mismatch (mirrors existing IV + tag length validation pattern). P2 (defensive): test fixture used "ghp_fake_for_test_only" which matches GitHub PAT regex → could trip secret-scanners with false positives. Fix: replaced with clearly-non-token strings "TEST-FIXTURE-NOT-A-REAL-TOKEN-deadbeef-cafebabe" + comment explaining why. P2 (coverage): no test asserted wrong-length-salt envelope returns error (not throws). Fix: added test case locking in the no-throw contract. Two findings resolved as-is (NOT changes): - "consider scrypt before HKDF" — ALREADY ADDRESSED in prior commit 941106f (security-review HIGH fix); thread filed against original commit before scrypt addition; resolve no-op - "typo 'wnated'" — quoting Aaron's verbatim message; preserve typos exactly per substrate-honest discipline (resolve no-op with note) All 19 tests pass (added 1 new test; up from 18).
This was referenced May 27, 2026
AceHack
added a commit
that referenced
this pull request
May 27, 2026
… tests) (#5414) B-0852 sub-row .5 — smallest pure-data substrate slice. No runtime deps; pure types + validator. Composes with B-0852.1 crypto module (PR #5411) as the data-shape layer to the cipher layer. Aaron 2026-05-27 discipline: "the keep credentials options we should declare each credential we need and save and restore so it's not so imparative too." Adding a new credential type = manifest edit, not a code change. Two files: - tools/installer/zeta-creds-manifest.ts — CredentialEntry + Manifest types; DEFAULT_MANIFEST (6 entries: gh-cli + claude + gemini + codex + ssh-host-keys + ssh-operator-pubkey); validateManifest() pure function returning { ok } | { error } (no-throws contract). - tools/installer/zeta-creds-manifest.test.ts — 21 tests covering: DEFAULT_MANIFEST internal consistency + Phase 1 vendor coverage + personaScoped flags + happy-path validation + 12 rejection cases (non-object, wrong schema version, duplicate ids, empty paths, type mismatches, error accumulation). Test output: 21 pass / 0 fail / 37 expect() calls / 111ms. Format: prettier-clean. No third-party deps. Default manifest entries: - gh-cli (personaScoped:false; required:true; B-0847 may flip future) - claude / gemini / codex (personaScoped:true; required:true; per-AI identity) - ssh-host-keys (personaScoped:false; required:false; regen acceptable) - ssh-operator-pubkey (personaScoped:false; required:true; iter-4.2 compose) Composes with: - B-0852 (parent) — credential persistence row - B-0852.1 (PR #5411) — crypto module; cipher layer (this row is data layer) - B-0852.2 (next) — persist/restore CLIs consume both .1 + .5 - B-0847 — per-AI GitHub identity; future personaScoped:true flip for gh-cli - iter-4.2 ESP SSH pubkey injection — composes with ssh-operator-pubkey entry Co-authored-by: Lior <lior@zeta.dev>
This was referenced May 27, 2026
AceHack
pushed a commit
that referenced
this pull request
May 27, 2026
…attribution + ghp_ sweep + beforeAll/afterAll cleanup + pure-resolution wording PR #5418 Copilot review caught 5 findings on the per-cred type handler module. All 5 valid; fixed in single commit before re-arming auto-merge. P0 SECURITY (parseBakeCredArg secret leak): Before: error messages included raw `arg` which may contain a PAT / JSON cred / SSH key. Operator typo → secret echoed to logs. After: errors include only id (operator-controlled name); value- source explicitly omitted with comment naming the leak risk. P1 codebase convention (name attribution in code): Before: header attributed CLI-override design to a named individual. After: role-ref form "operator-named; substrate-anchor in B-0852 row body" per repo's no-name-in-code rule. P1 operational (ghp_ token fixtures trip secret scanners): Before: 3 occurrences of "ghp_*" in test fixtures. After: swept to "TEST-NOT-A-REAL-TOKEN-*" prefix (same pattern as PR #5411 B-0852.1 already used). P1 maintainability (tmp-dir cleanup as teardown test): Before: describe block created tmpdir at evaluation time; cleanup via dedicated "teardown" test (skip-filterable; order-dependent; leaks if earlier test throws). After: beforeAll() + afterAll() — afterAll always runs even on test failure; canonical bun:test cleanup pattern. P2 documentation (mislabeled "pure" function): Before: "Pure resolution layer (file read + env access only — no network)." — the file/env reads are side effects. After: "Local-only side effects: file read + env access. No network. Not pure." All 60 tests still pass (now 59 actually — removed the dedicated "teardown" test since beforeAll/afterAll handle cleanup correctly). Resolves Copilot threads PRRT_kwDOSF9kNM6FBxi3 + PRRT_kwDOSF9kNM6FBxjO + PRRT_kwDOSF9kNM6FBxjc + PRRT_kwDOSF9kNM6FBxjp + PRRT_kwDOSF9kNM6FBxj3 on PR #5418.
AceHack
added a commit
that referenced
this pull request
May 27, 2026
…ve literal/@file/env:VAR + per-type validation (60 unit tests; pure TS) (#5418) * feat(B-0852.10): per-cred type handlers — parse <id>=<source> + resolve literal/@file/env:VAR + per-type validation (60 unit tests) B-0852 sub-row .10 — pure TS module composing B-0852.5 (declarative manifest; landed PR #5414) + B-0852.1 (crypto module; landed PR #5411) toward B-0852.9 zflash --bake-cred CLI override per Aaron 2026-05-27 CLI-override design. Three pure layers: 1. parseBakeCredArg(arg) — splits "<id>=<source>" preserving = in value 2. resolveValueSource(source) — handles literal / @file / env:VAR 3. validateValue(buf) per cred-type handler — PAT / JSON / SSH pubkey Per-type handlers (one per default manifest entry): - GH_CLI_HANDLER — literal/file/env; non-empty string - CLAUDE_HANDLER — literal/file; valid JSON object - GEMINI_HANDLER — literal/file; valid JSON object - CODEX_HANDLER — literal/file; valid JSON object - SSH_OPERATOR_PUBKEY — literal/file; OpenSSH key-type prefix check - SSH_HOST_KEYS — Phase 1 deferred (empty supportedSources) resolveBakeCred() full pipeline composes the three layers + gates unsupported source types per handler.supportedSources (e.g., claude rejects env: source — JSON creds belong in files, not env vars). Test output: 60 pass / 0 fail / 71 expect() calls / 106ms. Format prettier-clean. No third-party deps; node:fs + node:os only. Composes with: - B-0852 parent row (CLI-override design per Aaron 2026-05-27 sharpening) - B-0852.5 (manifest schema) — handler.id matches manifest entry id - B-0852.1 (crypto module) — resolved bytes feed encrypt() in next slice - B-0852.9 future — zflash --bake-cred CLI consumes this module * fix(B-0852.10): 5 Copilot findings — P0 secret-leak redaction + name attribution + ghp_ sweep + beforeAll/afterAll cleanup + pure-resolution wording PR #5418 Copilot review caught 5 findings on the per-cred type handler module. All 5 valid; fixed in single commit before re-arming auto-merge. P0 SECURITY (parseBakeCredArg secret leak): Before: error messages included raw `arg` which may contain a PAT / JSON cred / SSH key. Operator typo → secret echoed to logs. After: errors include only id (operator-controlled name); value- source explicitly omitted with comment naming the leak risk. P1 codebase convention (name attribution in code): Before: header attributed CLI-override design to a named individual. After: role-ref form "operator-named; substrate-anchor in B-0852 row body" per repo's no-name-in-code rule. P1 operational (ghp_ token fixtures trip secret scanners): Before: 3 occurrences of "ghp_*" in test fixtures. After: swept to "TEST-NOT-A-REAL-TOKEN-*" prefix (same pattern as PR #5411 B-0852.1 already used). P1 maintainability (tmp-dir cleanup as teardown test): Before: describe block created tmpdir at evaluation time; cleanup via dedicated "teardown" test (skip-filterable; order-dependent; leaks if earlier test throws). After: beforeAll() + afterAll() — afterAll always runs even on test failure; canonical bun:test cleanup pattern. P2 documentation (mislabeled "pure" function): Before: "Pure resolution layer (file read + env access only — no network)." — the file/env reads are side effects. After: "Local-only side effects: file read + env access. No network. Not pure." All 60 tests still pass (now 59 actually — removed the dedicated "teardown" test since beforeAll/afterAll handle cleanup correctly). Resolves Copilot threads PRRT_kwDOSF9kNM6FBxi3 + PRRT_kwDOSF9kNM6FBxjO + PRRT_kwDOSF9kNM6FBxjc + PRRT_kwDOSF9kNM6FBxjp + PRRT_kwDOSF9kNM6FBxj3 on PR #5418. --------- Co-authored-by: Lior <lior@zeta.dev>
AceHack
added a commit
that referenced
this pull request
May 27, 2026
…0 substrate for Ace migration trajectory (14 sub-steps; 12 declarative-input categories; substrate-anchor for B-0852/0853/0855/0856 cross-refs) (#5420) * docs(B-0854.1): zeta-install.sh step-state-machine inventory — Phase 0 substrate for Ace migration trajectory B-0854 sub-row .1 (Phase 0; smallest pure-analysis slice). Documents the EXISTING imperative bash state-machine in zeta-install.sh so the B-0854 Phase 2 declarative-Ace-manifest schema can express the same surface. Inventory covers: - Top-level entry (REPO_URL, HOST, ZETA_AUTO_CONFIRM env semantics) - Step-by-step state machine for all 14 sub-steps (1, 2, 3, 4, 5, 6, 6.5, 6.55, 6.6, 6.7, 6.8, 6.9, 6.95, 7) with inputs/outputs/side- effects/failure-modes/declarative-equivalent per step - Cross-cutting: operator-prompt accumulation count (7 prompts today; B-0852 phase-split target = 1 passphrase prompt) - Idempotency surface table — informs B-0855 architectural fix scope - 12 distinct declarative-input categories the Ace manifest must capture (Phase 2 sub-row scope) - Files-generated-during-install table mapping to B-0852.5 cred- manifest entries (6 mapped, 3 candidate-expansion items named) Snapshot date: 2026-05-27 (origin/main 70596a8; PR #5417 cosign merge). Future refreshes should re-snapshot when zeta-install.sh changes substantially. Composes with already-landed substrate-engineering arc: - B-0852 + sub-rows (cred persistence) — PR #5403/#5411/#5414 - B-0853.1 (cosign signing) — PR #5417 + fix-fwd #5419 - B-0855 (self-register architectural fix) — PR #5412 - B-0856 Path A (deferred /tmp coordination) — PR #5413 - B-0854 parent (Ace migration trajectory) — PR #5405 No code change; pure documentation. Doesn't affect ISO substrate; batches into substrate-engineering history independent of next ISO build cycle. * fix(B-0854.1): escape | inside code spans for MD056 table-column-count compliance * fix(B-0854.1): 10 Copilot accuracy corrections — verified against actual zeta-install.sh content PR #5420 Copilot review caught 10 substantive accuracy issues in the B-0854.1 inventory doc. All 10 verified against origin/main 70596a8's actual zeta-install.sh content + corrected. Corrections: - Name attribution → role-ref ("the human maintainer") - Step 1 inputs: actual `lsblk -d -p -n -o NAME,TYPE,RM,RO,TRAN` + awk filter (not made-up NAME,SIZE,MODEL,TRAN,ROTA) - Step 3 side effects: `sgdisk --zap-all` only (not `wipefs -af` too) - Step 4: actual `sgdisk` (NOT `parted`); GPT layout via -n + -t flags; whole-disk longhorn partitions on DATA_DISKS too - Step 6: `nixos-generate-config --root /mnt --force` (NOT --no-filesystems; --force overwrites existing config) - Step 6.5: no MAGIC_NUMBER (didn't exist in script); INJECT_OK gate flag; iter-4 v1 manual-config-edit fallback path - Step 6.9: SELF_REG_OK flag; documented graceful-skip path lines 731+ - nixos-install: actual line ~1004 (NOT 1096-1340); section renamed to "nixos-install (the actual build; ~line 1004)" since the prior range was wrong - Step 7: actual lines 1261-1336 (NOT 1341-1352); banner driven by GH_AUTH_OK/GH_KEY_COUNT/INJECT_OK/SELF_REG_OK (NOT MAGIC_NUMBER); conditional sections listed in declarative equivalent Resolves 10 Copilot threads on PR #5420. Root cause of the inaccuracies: original draft was written from `grep -E "^# ── Step"` summaries + recollection of script behavior, not careful per-step body reads. Discipline lesson: when authoring substrate-anchor docs claiming to inventory existing code, the read must be careful per-line, not skim-grep summary. Composes with .claude/rules/verify-existing-substrate-before-authoring.md at the inventory-substrate scope (verify-content-of-thing-being-inventoried before authoring claims about its content). --------- Co-authored-by: Lior <lior@zeta.dev>
This was referenced May 27, 2026
AceHack
added a commit
that referenced
this pull request
May 27, 2026
…xt schema (17 unit tests) (#5421) B-0852 sub-row .2 first slice — the on-disk wire format that B-0852.2b persist/restore CLIs will use to read/write /esp/zeta-creds.enc. Pure functions; no I/O. Two layers: 1. Envelope serialization (binary; little-endian; v1 magic "ZCV1"): - 8-byte header: 4-byte magic + 4-byte reserved - Length-prefixed: salt (u16) + iv (u16) + tag (u16) + ciphertext (u32) - Trailing bytes rejected in v1 (v2 will explicit-version-bump) 2. CredBundle plaintext schema (post-decryption JSON): - schemaVersion: 1 - globalCreds: { id: bytes } (personaScoped=false manifest entries) - personaCreds: { persona: { id: bytes } } (personaScoped=true) - Bytes base64-encoded inside JSON for safe transport - Composes with B-0852.5 manifest's personaScoped flag Full pipeline (covered by 1 integration test): CredBundle → encodeBundle → encrypt (B-0852.1) → serializeEnvelope → [disk write/read simulation] → parseEnvelope → decrypt → decodeBundle → CredBundle (byte-identical) Test output: 17 pass / 0 fail / 29 expect() calls / 1.67s (scrypt dominates timing per B-0852.1 OWASP-recommended N=2^17). Composes with: - B-0852 parent (cred persistence) - B-0852.1 crypto module (merged PR #5411) — Envelope type producer - B-0852.5 cred-manifest schema (merged PR #5414) — personaScoped semantics - B-0852.10 per-cred handlers (merged PR #5418) — value producers feed globalCreds/personaCreds maps - B-0852.2b future — persist/restore CLIs consume this module What this is NOT: - NOT the persist CLI (next slice; needs FS + passphrase prompt) - NOT the restore CLI (next slice; same) - NOT zflash --bake-cred integration (B-0852.9) Co-authored-by: Lior <lior@zeta.dev>
AceHack
added a commit
that referenced
this pull request
May 27, 2026
…ersistence stack (19 integration tests; replaces conflict-dirty #5422) (#5425) * feat(B-0852.2b): persist + restore CLIs composing the full cred-persistence stack (19 integration tests) B-0852 sub-row .2 final slice — operational CLI surface that composes all 4 already-shipped modules: - B-0852.1 crypto (encrypt/decrypt with scrypt+HKDF+AES-256-GCM) - B-0852.5 manifest (declarative cred catalog) - B-0852.10 per-cred handlers (--bake-cred parse + validate) - B-0852.2a envelope (wire format + CredBundle plaintext schema) THREE files: 1. tools/installer/zeta-creds-persist.ts (CLI) Usage: bun zeta-creds-persist.ts --usb-uuid <uuid> --output /esp/zeta-creds.enc --passphrase-{env VAR | file PATH} [--persona <name>] [--bake-cred <id>=<value-source>]... Composes --bake-cred args + manifest personaScoped flag → CredBundle → encrypt via crypto module → serialize via envelope → write to ESP. 2. tools/installer/zeta-creds-restore.ts (CLI) Usage: bun zeta-creds-restore.ts --usb-uuid <uuid> --input /esp/zeta-creds.enc --passphrase-{env VAR | file PATH} [--persona <name>] [--target-root /] [--dry-run] Reads encrypted blob → parseEnvelope → decrypt → decodeBundle → writes each cred to its manifest-declared paths under target-root. Exit codes: 0=ok / 2=arg-parse / 3=file-read / 4=envelope-parse / 5=decrypt-fail (wrong pass/UUID/tampered) / 6=bundle-decode / 7=manifest-mismatch. --dry-run prints plan without writing. 3. tools/installer/zeta-creds-persist-restore.test.ts (19 tests) - parsePersistArgs / parseRestoreArgs (well-formed + each error path) - composeBundle (global / persona / missing-persona / unknown-id) - resolveCredPaths (~ expansion + absolute paths under target-root) - persist→restore round-trips via tmpdir (gh-cli global + claude persona) - wrong-passphrase / wrong-UUID / tampered-blob / invalid-magic each surface their canonical exit code via planRestore Note: passphrase-interactive-prompt deferred to a NixOS module wrapper (B-0852.4); these CLIs require --passphrase-file or --passphrase-env so the modules are scriptable + test-driveable. Interactive entry happens at the install-script Step 6.77 (per B-0852 row body) which prompts + exports to env before invoking these CLIs. Test output: 19 pass / 0 fail / 28 expect() calls / 2.06s (scrypt-bound). Format prettier-clean. Composes with: - B-0852 parent (cred persistence) - B-0852.1 crypto module (merged PR #5411) - B-0852.5 cred-manifest (merged PR #5414) - B-0852.10 per-cred handlers (merged PR #5418) - B-0852.2a envelope (PR #5421 — this PR chains off; will resolve cleanly when #5421 merges to main) - B-0852.4 future — NixOS module wraps these CLIs with passphrase prompt - B-0852.3 future — zeta-install.sh Step 6.77 picker invokes these CLIs - B-0852.9 future — zflash --bake-cred at flash-time invokes persist directly What this is NOT: - NOT the interactive passphrase prompt (B-0852.4 NixOS-module scope) - NOT the zeta-install.sh Step 6.77 integration (B-0852.3) - NOT the zflash --bake-cred at flash time (B-0852.9; same persist CLI invoked from operator's Mac instead of target boot) * fix(B-0852.2b): 14 Copilot findings — P0 CodeQL secret-leak + 5 error-handling bugs + applyPlan refactor (single decrypt) + 0-byte ciphertext + docs PR #5422 Copilot review caught 14 findings; all valid. Comprehensive fix-pass before re-arming auto-merge. P0 CodeQL clear-text-logging (2 alerts; persist.ts L129 + restore.ts L240): CodeQL flagged the env-var NAME from --passphrase-env being included in error strings (taint tracker treats env[passphraseEnv] access as sensitive → var-name becomes tainted). Fix: omit env-var name from error message; generic "--passphrase-env target var is not set or is empty" instead. Same change in both files. Error-handling bugs: - readFileSync(passphrase-file) in parseArgs not wrapped → could throw on permission failure instead of returning {error}. Fixed in both persist + restore. - readFileSync(input) in restore main() not wrapped → could throw on permission failure instead of returning code 3. Fixed. - writeFileSync(output) in persist main() not wrapped → could throw on unwritable path. Fixed; new exit code 4 for write failure. applyPlan refactor (P1 design): Prior: planRestore + applyPlan each did full parse→decrypt→decode, doubling scrypt cost + extending passphrase-derived key lifetime in memory. Now: planRestore returns RestorePlan with embedded value Buffer per write entry; applyPlan takes the plan + just writes (no decrypt). Single scrypt invocation per restore. Also fixes the silent-skip mismatch — applyPlan now consumes the plan's pre-validated writes; can't accidentally bypass the manifest match checks. 0-byte ciphertext (P2 envelope): MIN_BLOB_LEN was header + lens + 1-byte ciphertext; AES-GCM allows empty plaintext/ciphertext. Fixed: drop the +1. Documentation: - parseArgs docs: removed "Pure (no I/O)" claim (does FS reads for --passphrase-file); accurate description in new doc comment - Usage docs: removed "(interactive prompt)" mention (not implemented in this entry-point per design); replaced with "interactive prompting is the wrapping NixOS module's responsibility (B-0852.4)" Unused import: - tools/installer/zeta-creds-restore.ts: removed unused `join` import from node:path Test updates: - applyPlan signature changed; updated 2 test cases to call planRestore first + pass plan to applyPlan. All 19 persist/restore tests + 17 envelope tests still pass. Resolves 14 Copilot threads on PR #5422. --------- Co-authored-by: Lior <lior@zeta.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Smallest concrete substrate slice of B-0852 cred-persistence. Pure crypto functions; no I/O; 18 unit tests covering the threat model.
Threat model (Phase 1 scope per B-0852 row body)
(USB-UUID || passphrase)— copying ESP contents to a different-UUID USB cannot decrypt (defeats "copy to uuid" attack named by Aaron 2026-05-27){ error }(NOT garbled plaintext)Files
tools/installer/zeta-creds-crypto.tsderiveKey+encrypt+decryptpure moduletools/installer/zeta-creds-crypto.test.tsTest output
Tests cover: round-trip (small/empty/1MiB), wrong passphrase, wrong UUID (copy-to-different-USB attack), tampered ciphertext (byte flip), tampered tag, tampered salt, malformed IV/tag sizes, HKDF determinism, HKDF independence (UUID/passphrase/salt sensitivity), salt + IV freshness across calls.
Composes with
encrypt/decrypthkdfSync+createCipheriv("aes-256-gcm", ...)— standard primitives; no third-party depWhat this is NOT
Test plan
bun test tools/installer/zeta-creds-crypto.test.ts→ 18 passprettier --checkclean🤖 Generated with Claude Code