@@ -8,12 +8,14 @@ import (
88 "crypto/sha1"
99 "errors"
1010 "fmt"
11+ "os"
1112 "os/exec"
1213 "strings"
1314 "sync"
1415 "time"
1516
1617 "github.com/moond4rk/keychainbreaker"
18+ "golang.org/x/term"
1719)
1820
1921// https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
@@ -55,6 +57,30 @@ func (r *GcoredumpRetriever) retrieveKeyOnce(storage string) ([]byte, error) {
5557 return darwinParams .deriveKey ([]byte (secret )), nil
5658}
5759
60+ // loadKeychainRecords opens login.keychain-db and unlocks it with the given
61+ // password, returning all generic password records.
62+ func loadKeychainRecords (password string ) ([]keychainbreaker.GenericPassword , error ) {
63+ kc , err := keychainbreaker .Open ()
64+ if err != nil {
65+ return nil , fmt .Errorf ("open keychain: %w" , err )
66+ }
67+ if err := kc .Unlock (keychainbreaker .WithPassword (password )); err != nil {
68+ return nil , fmt .Errorf ("unlock keychain: %w" , err )
69+ }
70+ return kc .GenericPasswords ()
71+ }
72+
73+ // findStorageKey searches keychain records for the given storage account
74+ // and derives the encryption key.
75+ func findStorageKey (records []keychainbreaker.GenericPassword , storage string ) ([]byte , error ) {
76+ for _ , rec := range records {
77+ if rec .Account == storage {
78+ return darwinParams .deriveKey (rec .Password ), nil
79+ }
80+ }
81+ return nil , fmt .Errorf ("%q: %w" , storage , errStorageNotFound )
82+ }
83+
5884// KeychainPasswordRetriever unlocks login.keychain-db directly using the
5985// user's macOS login password. No root privileges required.
6086// The keychain is opened and decrypted only once; subsequent calls
@@ -67,35 +93,50 @@ type KeychainPasswordRetriever struct {
6793 err error
6894}
6995
70- func (r * KeychainPasswordRetriever ) loadRecords () {
71- kc , err := keychainbreaker .Open ()
72- if err != nil {
73- r .err = fmt .Errorf ("open keychain: %w" , err )
74- return
75- }
76- if err := kc .Unlock (keychainbreaker .WithPassword (r .Password )); err != nil {
77- r .err = fmt .Errorf ("unlock keychain: %w" , err )
78- return
79- }
80- r .records , r .err = kc .GenericPasswords ()
81- }
82-
8396func (r * KeychainPasswordRetriever ) RetrieveKey (storage , _ string ) ([]byte , error ) {
8497 if r .Password == "" {
8598 return nil , fmt .Errorf ("keychain password not provided" )
8699 }
87100
88- r .once .Do (r .loadRecords )
101+ r .once .Do (func () {
102+ r .records , r .err = loadKeychainRecords (r .Password )
103+ })
89104 if r .err != nil {
90105 return nil , r .err
91106 }
92107
93- for _ , rec := range r .records {
94- if rec .Account == storage {
95- return darwinParams .deriveKey (rec .Password ), nil
108+ return findStorageKey (r .records , storage )
109+ }
110+
111+ // TerminalPasswordRetriever prompts for the keychain password interactively
112+ // via the terminal using golang.org/x/term (with echo disabled).
113+ // Automatically skipped when stdin is not a TTY.
114+ type TerminalPasswordRetriever struct {
115+ once sync.Once
116+ records []keychainbreaker.GenericPassword
117+ err error
118+ }
119+
120+ func (r * TerminalPasswordRetriever ) RetrieveKey (storage , _ string ) ([]byte , error ) {
121+ if ! term .IsTerminal (int (os .Stdin .Fd ())) {
122+ return nil , nil
123+ }
124+
125+ r .once .Do (func () {
126+ fmt .Fprintf (os .Stderr , "Enter macOS login password for %s: " , storage )
127+ pwd , err := term .ReadPassword (int (os .Stdin .Fd ()))
128+ fmt .Fprintln (os .Stderr )
129+ if err != nil {
130+ r .err = fmt .Errorf ("terminal: read password: %w" , err )
131+ return
96132 }
133+ r .records , r .err = loadKeychainRecords (string (pwd ))
134+ })
135+ if r .err != nil {
136+ return nil , r .err
97137 }
98- return nil , fmt .Errorf ("storage %q not found in keychain" , storage )
138+
139+ return findStorageKey (r .records , storage )
99140}
100141
101142// SecurityCmdRetriever uses macOS `security` CLI to query Keychain.
@@ -143,14 +184,21 @@ func (r *SecurityCmdRetriever) retrieveKeyOnce(storage string) ([]byte, error) {
143184}
144185
145186// DefaultRetriever returns the macOS retriever chain.
146- // If keychainPassword is provided, the password-based retriever is included.
187+ // The chain tries each method in order until one succeeds:
188+ // 1. GcoredumpRetriever — CVE-2025-24204 exploit (root only, non-interactive)
189+ // 2. KeychainPasswordRetriever — direct unlock with --keychain-pw flag
190+ // 3. TerminalPasswordRetriever — interactive password prompt via terminal
191+ // 4. SecurityCmdRetriever — security CLI fallback (may trigger system dialog)
147192func DefaultRetriever (keychainPassword string ) KeyRetriever {
148193 retrievers := []KeyRetriever {
149194 & GcoredumpRetriever {},
150195 }
151196 if keychainPassword != "" {
152197 retrievers = append (retrievers , & KeychainPasswordRetriever {Password : keychainPassword })
153198 }
154- retrievers = append (retrievers , & SecurityCmdRetriever {})
199+ retrievers = append (retrievers ,
200+ & TerminalPasswordRetriever {},
201+ & SecurityCmdRetriever {},
202+ )
155203 return NewChain (retrievers ... )
156204}
0 commit comments