Skip to content

Commit a58d432

Browse files
authored
fix: cache keychain retriever across browser profiles on macOS (#545)
Share a single KeyRetriever instance across all profiles of the same browser, and add sync.Once caching to GcoredumpRetriever and SecurityCmdRetriever. This avoids repeated keychain password prompts (or securityd memory dumps) when extracting multiple profiles. Closes #544
1 parent 92053b8 commit a58d432

2 files changed

Lines changed: 37 additions & 5 deletions

File tree

browser/chromium/chromium.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
type Browser struct {
1616
cfg types.BrowserConfig
1717
profileDir string // absolute path to profile directory
18+
retriever keyretriever.KeyRetriever // shared across profiles of the same browser
1819
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
1920
extractors map[types.Category]categoryExtractor // Category → custom extract function override
2021
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
@@ -32,6 +33,11 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
3233
return nil, nil
3334
}
3435

36+
// Create the key retriever once and share it across all profiles.
37+
// This avoids repeated keychain password prompts on macOS, where each
38+
// profile would otherwise trigger a separate `security` command dialog.
39+
retriever := keyretriever.DefaultRetriever(cfg.KeychainPassword)
40+
3541
var browsers []*Browser
3642
for _, profileDir := range profileDirs {
3743
sourcePaths := resolveSourcePaths(sources, profileDir)
@@ -41,6 +47,7 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
4147
browsers = append(browsers, &Browser{
4248
cfg: cfg,
4349
profileDir: profileDir,
50+
retriever: retriever,
4451
sources: sources,
4552
extractors: extractors,
4653
sourcePaths: sourcePaths,
@@ -126,8 +133,7 @@ func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) {
126133
}
127134
}
128135

129-
retriever := keyretriever.DefaultRetriever(b.cfg.KeychainPassword)
130-
return retriever.RetrieveKey(b.cfg.Storage, localStateDst)
136+
return b.retriever.RetrieveKey(b.cfg.Storage, localStateDst)
131137
}
132138

133139
// extractCategory calls the appropriate extract function for a category.

crypto/keyretriever/keyretriever_darwin.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,22 @@ const securityCmdTimeout = 30 * time.Second
2929

3030
// GcoredumpRetriever uses CVE-2025-24204 to extract keychain secrets
3131
// by dumping the securityd process memory. Requires root privileges.
32-
type GcoredumpRetriever struct{}
32+
// The result is cached via sync.Once to avoid repeated memory dumps
33+
// when multiple profiles share the same retriever instance.
34+
type GcoredumpRetriever struct {
35+
once sync.Once
36+
key []byte
37+
err error
38+
}
3339

3440
func (r *GcoredumpRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
41+
r.once.Do(func() {
42+
r.key, r.err = r.retrieveKeyOnce(storage)
43+
})
44+
return r.key, r.err
45+
}
46+
47+
func (r *GcoredumpRetriever) retrieveKeyOnce(storage string) ([]byte, error) {
3548
secret, err := DecryptKeychain(storage)
3649
if err != nil {
3750
return nil, fmt.Errorf("gcoredump: %w", err)
@@ -86,10 +99,23 @@ func (r *KeychainPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, erro
8699
}
87100

88101
// SecurityCmdRetriever uses macOS `security` CLI to query Keychain.
89-
// This may trigger a password dialog on macOS.
90-
type SecurityCmdRetriever struct{}
102+
// This may trigger a password dialog on macOS. The result is cached
103+
// via sync.Once so that multiple profiles sharing the same retriever
104+
// instance only prompt the user once.
105+
type SecurityCmdRetriever struct {
106+
once sync.Once
107+
key []byte
108+
err error
109+
}
91110

92111
func (r *SecurityCmdRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
112+
r.once.Do(func() {
113+
r.key, r.err = r.retrieveKeyOnce(storage)
114+
})
115+
return r.key, r.err
116+
}
117+
118+
func (r *SecurityCmdRetriever) retrieveKeyOnce(storage string) ([]byte, error) {
93119
ctx, cancel := context.WithTimeout(context.Background(), securityCmdTimeout)
94120
defer cancel()
95121

0 commit comments

Comments
 (0)