Skip to content

fix(vault): passphrase-independent node_id (FIX-01 security)#49

Open
ChelseaKR wants to merge 1 commit into
mainfrom
fix/fix01-node-id-hardening
Open

fix(vault): passphrase-independent node_id (FIX-01 security)#49
ChelseaKR wants to merge 1 commit into
mainfrom
fix/fix01-node-id-hardening

Conversation

@ChelseaKR

@ChelseaKR ChelseaKR commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Splits the FIX-01 security fix out of the uncommitted standards-conformance-2026-07-05 working tree into its own reviewable PR (per habitable-ROADMAP BF-1).

What & why: the device node_id used to be sha256(case_id + passphrase)[:16], written to plaintext config.toml and 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 makes node_id random 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). Config no longer carries node_id.

Verification: full suite green (206 passed), incl. a guard test that packet ids encode no passphrase-derived material.

⚠️ This is previously-unreviewed security code — please review the migration (one-way/idempotent) and the guard test before merging. Backed up on rescue/uncommitted-2026-07-06.

🤖 Generated with Claude Code

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).

@ChelseaKR ChelseaKR left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.encVaultError; 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)

  1. Minor — crash window can leave the plaintext leak in place permanently. _load_node_id writes node.enc then strips the plaintext line. If the process dies between the two, every later open returns early from the encrypted blob and the plaintext node_id line in config.toml is never stripped. Cheap hardening: when node.enc exists, 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).
  2. Minor — non-atomic config rewrite. _strip_plaintext_node_id rewrites config.toml with a plain write_text; a crash mid-write can corrupt the file and block vault open until hand-repair. Write-temp-then-rename removes the window.
  3. Note — PEP 758 syntax. except OSError, tomllib.TOMLDecodeError: (unparenthesized) is valid only on very recent Python; fine under the repo floor requires-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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant