Skip to content

fix: local keystore KEK no longer includes OSVersion (survives Windows feature updates) - #90

Merged
StuartMeeks merged 1 commit into
mainfrom
fix/keystore-osversion-kek-rotation
Sep 21, 2026
Merged

StuartMeeks merged 1 commit into
mainfrom
fix/keystore-osversion-kek-rotation

Conversation

@StuartMeeks

Copy link
Copy Markdown
Owner

Problem

The file backend (LocalFileCredentialEncryption) derived its key-encryption key from {MachineName}:{UserName}:{OSVersion}. A Windows feature update (e.g. 25H2 → 26H2) changes Environment.OSVersion, which rotates the KEK and leaves every credential undecryptable:

System.Security.Cryptography.AuthenticationTagMismatchException:
  The computed authentication tag did not match the input authentication tag.

There was no migration path — the old build number is unrecoverable, so the keystore is effectively dead.

Fix

  • Drop OSVersion from 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).
  • Keystore format v1 → v2. A v1 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 v2 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 — a persistence failure never fails an otherwise-successful read.
  • The integrity-error message now names the OS-update cause.

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/UserName belong 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 2 and 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

…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>
@StuartMeeks
StuartMeeks merged commit ccf38f5 into main Sep 21, 2026
11 checks passed
@StuartMeeks
StuartMeeks deleted the fix/keystore-osversion-kek-rotation branch September 21, 2026 06:32
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