fix(vault): passphrase-independent node_id (FIX-01 security)#49
fix(vault): passphrase-independent node_id (FIX-01 security)#49ChelseaKR wants to merge 1 commit into
Conversation
Pre-FIX-01 the device node_id was derived from the vault passphrase and written to plaintext config.toml, then embedded in every exported packet id — an offline brute-force oracle for the passphrase on a seized device (the ideation pass's highest-severity security finding). Generate a random, passphrase-independent node_id kept inside the encrypted vault instead; a one-way, idempotent migration moves any legacy plaintext node_id into the vault unchanged so existing packet ids and the custody chain stay stable. Config no longer carries node_id. Verified: full test suite green (206 passed), including a guard that packet ids encode no passphrase-derived material. Split out of the uncommitted standards-conformance working tree as its own reviewable PR (habitable BF-1).
357672c to
72aafed
Compare
ChelseaKR
left a comment
There was a problem hiding this comment.
Conformance review — APPROVE
FIX-01 verified effective. The passphrase-derived node_id (sha256(case_id+passphrase)[:16]) no longer exists anywhere: new vaults get secrets.token_hex(8) stored only in the encrypted node.enc blob; plaintext config.toml no longer carries node_id (asserted in tests); and the guard test reproduces the pre-fix derivation and asserts it appears in neither config.toml, keyfile.json, nor an exported bundle.json (tests/test_guards.py::test_packet_ids_do_not_encode_passphrase_derived_material).
- Fails closed: corrupt/empty
node.enc→VaultError; missing blob with no legacy value →VaultError(src/habitable/vault.py::_load_node_id). - Idempotent + one-way: second open reads the encrypted blob; the migration preserves the legacy value byte-for-byte, so packet ids and the append-only custody chain stay stable (
tests/test_vault_capture.py::test_legacy_plaintext_node_id_migrates_on_open, including a second re-open).
Findings (none blocking)
- Minor — crash window can leave the plaintext leak in place permanently.
_load_node_idwritesnode.encthen strips the plaintext line. If the process dies between the two, every later open returns early from the encrypted blob and the plaintextnode_idline inconfig.tomlis never stripped. Cheap hardening: whennode.encexists, still call_strip_plaintext_node_id(path / _CONFIG)(a no-op when the line is absent).src/habitable/vault.py(_load_node_id, ~line 330). - Minor — non-atomic config rewrite.
_strip_plaintext_node_idrewritesconfig.tomlwith a plainwrite_text; a crash mid-write can corrupt the file and block vault open until hand-repair. Write-temp-then-rename removes the window. - Note — PEP 758 syntax.
except OSError, tomllib.TOMLDecodeError:(unparenthesized) is valid only on very recent Python; fine under the repo floorrequires-python = ">=3.14", but keep this construct out of the verifier subset that PR #26 wants cross-Python-compilable.
CodeQL check status — false attribution, confirmed and on the record
The failing "CodeQL" check on this PR reports "1 high in code changed by this pull request", but the only alert on refs/pull/49/merge is alert #1 (py/weak-sensitive-data-hashing, src/habitable/canonical.py:49), open on main since 2026-06-17. This PR does not touch canonical.py. The check's own output says "1 configuration present on refs/heads/main was not found" — i.e. the baseline comparison failed, so the pre-existing main alert was misclassified as new to this PR. The actual analyses ("CodeQL (python)", "CodeQL (actions)") both pass. The failing check is not caused by this PR; alert #1 remains a pre-existing main-branch finding to triage separately.
Claims vs. diff / CI
PR body matches the change; migration, randomness, and guard tests are present as claimed. Merge gate (lint/types/tests, gitleaks, i18n parity, axe, Trivy, build, dep audit) all green.
Conformance: conventional-commit title, honest security framing with no overclaim, no telemetry surface touched — conforms to the repo's CODE-QUALITY and privacy standards.
Splits the FIX-01 security fix out of the uncommitted
standards-conformance-2026-07-05working tree into its own reviewable PR (per habitable-ROADMAP BF-1).What & why: the device
node_idused to besha256(case_id + passphrase)[:16], written to plaintextconfig.tomland embedded in every exported packet id. On a seized device that is an offline brute-force oracle for the vault passphrase — the ideation pass's highest-severity finding. This makesnode_idrandom and passphrase-independent, stored inside the encrypted vault, with a one-way idempotent migration that moves any legacy plaintext id into the vault unchanged (packet ids + custody chain stay stable).Configno longer carriesnode_id.Verification: full suite green (206 passed), incl. a guard test that packet ids encode no passphrase-derived material.
rescue/uncommitted-2026-07-06.🤖 Generated with Claude Code