Skip to content

macOS Save and Remove decrypt the existing secret into process memory without using it, and free it unscrubbed #166

Description

@matt-edmondson

What's wrong

In CredentialCache/Storage/MacOsCredentialStore.cs, both Save (~line 81) and Remove (~line 139) call SecKeychainFindGenericPasswordWithRef only to get itemRef. Save then passes it to SecKeychainItemModifyAttributesAndData, and Remove passes it to SecKeychainItemDelete. Both calls still request the password data, though:

passwordLength: out _, passwordData: out IntPtr existingPtr,   // Save
passwordLength: out _, passwordData: out IntPtr passwordPtr,   // Remove
itemRef: out IntPtr itemRef);

Apple's SecKeychainFindGenericPassword documentation says to pass NULL for passwordLength and passwordData when you only want the item reference. Passing non-null pointers makes the Keychain decrypt the old secret and copy it into the process. The code then releases that buffer with SecKeychainItemFreeContent, which does not zero it.

Why it matters

  • It breaks the scrubbing guarantee. Every overwrite or delete puts the previous plaintext credential into process memory and leaves it there unscrubbed. The read path, by contrast, goes to some length to scrub. A secret that is being rotated or deleted is exactly the one you'd least want lingering in memory.
  • It can prompt the user. Reading item password data is the operation the Keychain gates behind its "allow access" prompt when the calling binary isn't on the item's ACL, for example after an app update changes its signature. So a plain Remove, or an overwrite, can raise an authorization dialog it doesn't need. Deleting or modifying through the item ref does not need the secret.

Suggested fix

Add a P/Invoke overload, or change the existing signature, so the password parameters are IntPtr passwordLength, IntPtr passwordData. Pass IntPtr.Zero for both in Save and Remove, and remove the matching SecKeychainItemFreeContent(IntPtr.Zero, existingPtr/passwordPtr) calls.

Acceptance criteria

  • Save (the update branch) and Remove no longer request password data from the Keychain.
  • TryGet stays the only macOS path that decrypts a secret, and it keeps its existing scrub-and-free handling.

How this was verified

By comparing the code with Apple's documented parameter semantics. It was not run on macOS. That the old secret is decrypted without being needed is certain from the code. That this can trigger a prompt is documented Keychain behaviour, not something observed here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    readyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions