Skip to content

Zero the native plaintext TryGet loads before freeing it [patch] - #209

Open
matt-edmondson wants to merge 1 commit into
mainfrom
fix/173-zero-native-plaintext-on-load
Open

matt-edmondson wants to merge 1 commit into
mainfrom
fix/173-zero-native-plaintext-on-load

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #173

What was wrong

On a cache miss, TryGet calls Store.TryLoad. On Windows and macOS the store copied the decrypted credential out of the native buffer (CredRead's CredentialBlob, SecKeychainFindGenericPassword's passwordData) and scrubbed only that managed copy. CredFree / SecKeychainItemFreeContent then released the native block without zeroing it, so every load left a plaintext copy of the secret in freed heap.

Change

  • NativeSecretBuffer.ReadCredentialAndZero(pointer, length) copies the bytes, deserializes them through DeserializeAndScrub, and zeroes the native bytes in a finally. The memory is still allocated at that point, the unparseable path is covered, and the caller frees it afterwards.
  • WindowsCredentialStore.TryLoad and MacOsCredentialStore.TryLoad read through it. Linux is unchanged, because secret_password_free already wipes its copy.

The helper is shared, like the other primitives, so its behaviour runs under test on every OS.

Tests

  • SecretScrubbingTests: the helper returns the credential and zeroes the native copy, zeroes it for unparseable bytes too, and returns null for a null pointer or zero length without touching memory.
  • NativeStoreScrubbingWiringTests.TheWindowsAndMacOsStoresZeroTheNativePlaintextTheyLoad IL-pins both stores to the helper. It fails with the store changes reverted and passes with them. The existing load-wiring and second-hop assertions now also accept the new wrapper.
  • Full suite on Linux / net10.0: 107 passed, 5 skipped (platform-gated), 0 failed.

Generated by Claude Code

On Windows and macOS, TryLoad copied the decrypted credential out of the
buffer CredRead / SecKeychainFindGenericPassword returned and scrubbed only
that managed copy. CredFree and SecKeychainItemFreeContent then released the
native block unscrubbed, leaving a plaintext copy in freed heap on every load.

NativeSecretBuffer.ReadCredentialAndZero now reads, deserializes and scrubs
the managed copy, and zeroes the native bytes on every path out while they
are still allocated. Both stores read through it, and a wiring test pins
them to it, since neither store body runs on the Linux CI leg.

Fixes #173

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5TgsM2XbL48rHXNad
@sonarqubecloud

Copy link
Copy Markdown

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.

TryGet on Windows and macOS frees the OS-returned plaintext buffer without zeroing it, so every load leaves a copy of the secret in freed heap

1 participant