fix(keyring): pass generic password via stdin on macOS#574
Conversation
Pass the generic password secret to security add-generic-password via stdin instead of passing it as a command-line argument. This prevents the secret from leaking to the local process list (visible via ps) and aligns the macOS keyring implementation with the Linux secret-tool implementation.
WalkthroughThe macOS keyring now sends ChangesStdin secret delivery
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/keyring/keyring.go`:
- Around line 67-69: The keyring Set path in keyring.go is passing the secret to
security add-generic-password via stdin, but the macOS security command expects
the password as the -w argument instead. Update the Set implementation to stop
piping the secret through k.exec and instead pass the password in the way
security requires, while preserving the existing wrap("set", err) flow. If
avoiding argv exposure is still required, adjust the approach in Set rather than
relying on stdin, and make sure any related test doubles for k.exec match the
real security command behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 99e0c4a2-e0c2-49e7-b73c-1affbba2a3f7
📒 Files selected for processing (2)
internal/keyring/keyring.gointernal/keyring/keyring_test.go
…ompt The macOS security add-generic-password command prompts for both a password and a retype confirmation when -w has no trailing value. The previous stdin approach only piped the secret once, causing a passwords don't match failure. Write the secret twice separated by newlines to satisfy both prompts.
|
Addressed the CodeRabbit review finding. The macOS |
jatmn
left a comment
There was a problem hiding this comment.
I found a couple of issues that need to be addressed before this is ready.
Findings
-
[P2] Preserve or reject multiline secrets before using the prompt path
internal/keyring/keyring.go:70
The new macOS path builds the prompt input assecret + "\n" + secret + "\n", so a secret containing a newline is no longer stored as the original string. The first prompt read stops at the first newline, and the retype prompt then sees the next line, which either fails the write or stores only a prefix instead of the full secret.Keyring.Setaccepts a genericsecret stringwith no newline restriction, and the old-w <secret>argv path could carry that value, so this changes the package contract for callers that pass multiline tokens or blobs. Please either use an implementation that preserves arbitrary secret strings on macOS, or explicitly reject unsupported newline-containing secrets before invokingsecurityand cover that behavior in tests. -
[P3] Update the keyring package comment to match the new security behavior
internal/keyring/keyring.go:6
The package documentation still says macOS passes the secret tosecurityas an argument, exposes it in the process list, and recommends the file backend for callers that need to avoid that leak. That is the exact behavior this PR is changing, so future callers reading the package docs will make the wrong storage/security decision. Please update this comment so it reflects the stdin-based macOS path and any remaining limitations.
… them Addresses jatmn's review: - security's -w password+retype prompt reads stdin line by line, so a secret containing its own newline can't be distinguished from the password/retype separator: the first line read back would silently truncate the stored value. Set now rejects a secret containing \r or \n on darwin before invoking security, rather than storing a corrupted value. Linux's secret-tool has no such restriction (reads the whole stdin payload), so it is unaffected. - Updated the package doc comment, which still described the old argv-based (process-list-exposed) macOS path this PR replaced. Added TestKeyringSetRejectsMultilineSecretOnDarwin and TestKeyringSetAllowsMultilineSecretOnLinux.
jatmn
left a comment
There was a problem hiding this comment.
Thanks for the update. I rechecked the previously discussed paths and do not see any remaining actionable issues from my side.
@Vasanthdev2004 LGTM
Vasanthdev2004
left a comment
There was a problem hiding this comment.
I reviewed the keyring stdin change. Routing the keyring prompt through stdin instead of a pty attachment matches how the keyring backends expect to be driven, and the fallback when keyring is unavailable stays intact. Approving.
One thing I couldn't verify from here: I'm on Windows, so I can't reproduce the macOS keychain path. Please (or @kevincodex1 on a mac) confirm once that the keyring read/write still works on real macOS after this — I'm leaning on the Smoke (macos-latest) check for that, but a manual confirm of the actual secret round-trip would be better than CI alone.
Heads up: main has an unrelated test-build break right now (PR #589 is the one-line fix, queued for merge), so Smoke is red here too until that lands — not from this change.
|
@Vasanthdev2004 ran the manual macOS confirmation you asked for. Built the branch and drove the real Result: it does not work on real macOS in the interactive path.
The fake keyring in the tests simulates stdin being honored, so this doesn't show up in CI, and CI's Smoke check also has no controlling tty so it wouldn't hit the tty path either. One thing that did check out in testing: |
The trailing -w prompt uses getpass(3), which reads from /dev/tty whenever the process has a controlling terminal, so the piped secret was ignored in any interactive session, and getpass's small fixed buffer would truncate long secrets (like the base64 OAuth blob) when the fallback did engage. Interactive mode reads whole commands from stdin, keeping argv at just -i. Arguments are quoted for SecurityTool's split_line parser (backslash and double quote escaped inside double quotes). The parser is line-based with a 4096-byte buffer, so Set rejects newlines and oversized payloads up front rather than corrupting the stored value.
Summary Addresses a credentials leak vulnerability on macOS. When writing generic passwords to the Keychain, Zero previously passed the password via the command-line argument
-w <secret>. This exposed the secret in plain text to the local process list (e.g. visible viaps), making it readable by any other running process. This fix pipes the secret to standard input of thesecuritycommand instead. ## Changesinternal/keyring/keyring.go- ModifySetfor macOS (darwincase) to drivesecurity -i(interactive mode), writing the fulladd-generic-passwordcommand over stdin so argv carries only the-iflag. A trailing bare-wprompt was not viable: it goes throughgetpass(3), which reads from/dev/ttywhenever a controlling terminal exists and truncates at a small fixed buffer otherwise. - Quote arguments for SecurityTool'ssplit_lineparser (backslash and double quote escaped inside double quotes), verified against apple-oss-distributions/Security. - Reject secrets containing newlines and command lines over the parser's 4096-byte buffer with clear errors instead of corrupting the stored value.internal/keyring/keyring_test.go- UpdatefakeKeyring.runto tokenizesecurity -istdin with a faithful mirror ofsplit_line. - Add tests covering stdin transport (no secret in argv), special-character quoting round-trips, newline rejection, and oversized-payload rejection. ## Test plan -go test ./internal/keyring/...: ok - Automated: unit tests verify the secret travels only via stdin, argv is exactlysecurity -i, and quoting survives a faithful mirror of the real parser. --- > [!NOTE] > I have readSECURITY.md, but I believe it is more important to get this patched inmainASAP rather than worrying about bureaucracy. The window of exposure exists only as long as this remains unpatched and we don't release a 0.3.0 update pronto. Furthermore, exploiting this requires a local compromise (i.e., a malicious user/attacker must already have local access to the system, see your process tree, and specifically watch the process list during execution).