feat: keystore v3 — random-salt KEK, no machine/user identity - #95
Merged
StuartMeeks merged 2 commits intoSep 21, 2026
Merged
Conversation
… 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
deleted the
feat/keystore-v3-random-salt-no-machine-binding
branch
September 21, 2026 07:14
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.
Follows the
OSVersionfix (#90). That removed the volatile OS input from the KEK, butMachineName/UserNamewere 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
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.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:AdditionalEntropy(a per-machine or per-deployment secret) — now the file backend's only real cryptographic boundary; orThe 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