fix: local keystore KEK no longer includes OSVersion (survives Windows feature updates) - #90
Merged
Merged
Conversation
…break it
The file backend's key-encryption key was derived from
`{MachineName}:{UserName}:{OSVersion}`, so a Windows feature update (e.g.
25H2 -> 26H2) changed `Environment.OSVersion`, rotated the KEK, and left every
credential undecryptable with `AuthenticationTagMismatchException` and no
migration path.
Drop `OSVersion` from the KEK: it derives from stable machine/user identity
plus a fixed domain tag now, so later OS updates cannot rotate it. Machine and
user name still bind the keystore to this machine/user, and the security
boundary is unchanged (filesystem permissions on the credentials directory).
Bump the keystore format to version 2. A version-1 keystore -- and a legacy
headerless one, both sealed under the old OSVersion-based KEK -- is read with
the legacy KEK and then transparently re-sealed as version 2 on first load, so
an existing, still-readable store migrates itself on upgrade and becomes immune
to future OS updates. The re-seal overwrites in place (safe: the data key is
unchanged) and is best-effort, so a persistence failure never fails an
otherwise-successful read. A store already broken by an OS update performed
before this upgrade cannot be recovered by the library; the integrity-error
message now names that cause.
Tests: add v1->v2, legacy-headerless->v2, and entropy v1->v2 migration coverage
asserting the on-disk version flips to 2 and decryption still works; update the
format-header test. Full suite green on net8.0 and net10.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Problem
The file backend (
LocalFileCredentialEncryption) derived its key-encryption key from{MachineName}:{UserName}:{OSVersion}. A Windows feature update (e.g. 25H2 → 26H2) changesEnvironment.OSVersion, which rotates the KEK and leaves every credential undecryptable:There was no migration path — the old build number is unrecoverable, so the keystore is effectively dead.
Fix
OSVersionfrom the KEK. It now derives from stable machine/user identity plus a fixed domain tag (keystore/kek/v2), so a later OS update cannot rotate it. Machine and user name still bind the store to this machine/user; the security boundary is unchanged (filesystem permissions on the credentials directory).Scope / limitation
A store already broken by an OS update performed before this version is installed cannot be auto-recovered (the old OSVersion is gone). This PR protects every store still readable at upgrade time, and all future updates. Already-broken stores need out-of-band recovery or re-add.
This is the minimal, contained fix for the incident. A separate follow-up will revisit whether
MachineName/UserNamebelong in the KEK at all (they have the same class of fragility on rename, and are a tripwire rather than a boundary) and move the file backend to a random salt in the header — that changes default-mode portability semantics, so it's deliberately kept out of this PR.Tests
Added migration coverage (v1→v2, legacy-headerless→v2, entropy v1→v2) asserting the on-disk version flips to
2and decryption still works; updated the format-header test. Full suite green on net8.0 and net10.0 (312 passed / 0 failed; platform-guarded Windows/macOS backends skip locally and run on their CI legs).🤖 Generated with Claude Code