Skip to content

feat: keystore v3 — random-salt KEK, no machine/user identity - #95

Merged
StuartMeeks merged 2 commits into
mainfrom
feat/keystore-v3-random-salt-no-machine-binding
Sep 21, 2026
Merged

StuartMeeks merged 2 commits into
mainfrom
feat/keystore-v3-random-salt-no-machine-binding

Conversation

@StuartMeeks

Copy link
Copy Markdown
Owner

Follows the OSVersion fix (#90). That removed the volatile OS input from the KEK, but MachineName/UserName were still in it — same class of fragility (a machine rename rotates the KEK) and, more importantly, never a security boundary: those identifiers are discoverable, so binding to them was only a tripwire.

What changes

  • KEK from a random salt, no identity. v3 KEK = PBKDF2(fixed domain tag [+ AdditionalEntropy], random-salt). The 16-byte random salt is stored in the header: magic | version(3) | salt(16) | AES-GCM payload. A tampered salt → wrong KEK → the existing integrity error, so it's implicitly bound.
  • Transparent migration. v1 (and legacy headerless) and v2 keystores are read with their old KEK and re-sealed as v3 on first load (best-effort, in place — safe because the data key is unchanged).

Behaviour change (the trade-off we discussed)

A default-mode (no AdditionalEntropy) credentials directory is now portable — copied whole to another machine/user it still decrypts. This drops the "won't open elsewhere" tripwire, which was never a real control (an attacker who copies the keystore also gets the discoverable identity). For genuine binding:

  • supply AdditionalEntropy (a per-machine or per-deployment secret) — now the file backend's only real cryptographic boundary; or
  • use the DPAPI / platform-keychain backends, which bind to OS-managed secrets and survive renames and OS updates.

The default-mode boundary remains, as documented, the filesystem permissions on the credentials directory.

Tests

Added v2→v3 and entropy-v2→v3 migration tests and a random-salt-header test; retargeted the v1/headerless migration tests to v3. Full suite green locally on net8.0 and net10.0 (438 total, 0 failed, 120 platform-skipped). CHANGELOG and the CLAUDE.md security-boundary note updated.

🤖 Generated with Claude Code

StuartMeeks and others added 2 commits September 21, 2026 06:52
… identity (v3)

The file backend's KEK no longer folds in any ambient identity. After removing
OSVersion (which broke the store on a Windows feature update), MachineName and
UserName were still inputs — and they carry the same class of fragility (a
machine rename rotates the KEK) while never being a security boundary: they are
discoverable, so binding to them was only a tripwire, not protection.

v3 keystore: KEK = PBKDF2(fixed domain tag [+ AdditionalEntropy], random salt),
with the 16-byte random salt stored in the header
(magic | version(3) | salt(16) | AES-GCM payload). A tampered salt yields a
wrong KEK and surfaces as the existing integrity error, so it is implicitly
bound.

Migration: v1 (and legacy headerless) and v2 keystores are read with their old
KEK and transparently re-sealed as v3 on first load (best-effort, in place).

Behaviour change: a default-mode credentials directory is now portable across
machines/users. That drops the "won't open elsewhere" tripwire, which was never
a real control. Real binding is AdditionalEntropy (the file backend's only true
cryptographic boundary) or the DPAPI / platform-keychain backends, which bind to
OS-managed secrets and survive renames and OS updates.

Tests: add v2->v3 and entropy v2->v3 migration coverage and a random-salt header
test; retarget the v1/headerless migration tests to v3. Full suite green on
net8.0 and net10.0 (438 total, 0 failed). CHANGELOG and the CLAUDE.md
security-boundary note updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing)

Reflect the KEK redesign in prose:
- README "Moving credentials between machines": the default file backend is now
  portable (copy the directory, it decrypts) — reframe export/import as the
  passphrase-protected, backend-agnostic transfer, still required for the
  hardened/keychain-bound cases.
- README "Security model": KEK derives from a random header salt, no
  machine/user/OS identity. Drop the "casual attacker without machine knowledge"
  protection (no longer true) and state plainly that in default mode anyone with
  a copy of the files can derive the KEK — the boundary is filesystem
  permissions; AdditionalEntropy / DPAPI / keychain are the real bindings.
- SECURITY.md scope: same correction.

Historical CHANGELOG entries (1.0.0/2.0.0) are left as-is — they record what was
true at those releases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@StuartMeeks
StuartMeeks merged commit 477cd1a into main Sep 21, 2026
11 checks passed
@StuartMeeks
StuartMeeks deleted the feat/keystore-v3-random-salt-no-machine-binding branch September 21, 2026 07:14
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