From a58d4326886eb7520b42b362528f2edc881b619e Mon Sep 17 00:00:00 2001 From: Roger Date: Sat, 4 Apr 2026 18:36:49 +0800 Subject: [PATCH] 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 --- browser/chromium/chromium.go | 10 +++++-- crypto/keyretriever/keyretriever_darwin.go | 32 ++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/browser/chromium/chromium.go b/browser/chromium/chromium.go index 904c3bc..06f5bf2 100644 --- a/browser/chromium/chromium.go +++ b/browser/chromium/chromium.go @@ -15,6 +15,7 @@ import ( type Browser struct { cfg types.BrowserConfig profileDir string // absolute path to profile directory + retriever keyretriever.KeyRetriever // shared across profiles of the same browser sources map[types.Category][]sourcePath // Category → candidate paths (priority order) extractors map[types.Category]categoryExtractor // Category → custom extract function override sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path @@ -32,6 +33,11 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) { return nil, nil } + // Create the key retriever once and share it across all profiles. + // This avoids repeated keychain password prompts on macOS, where each + // profile would otherwise trigger a separate `security` command dialog. + retriever := keyretriever.DefaultRetriever(cfg.KeychainPassword) + var browsers []*Browser for _, profileDir := range profileDirs { sourcePaths := resolveSourcePaths(sources, profileDir) @@ -41,6 +47,7 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) { browsers = append(browsers, &Browser{ cfg: cfg, profileDir: profileDir, + retriever: retriever, sources: sources, extractors: extractors, sourcePaths: sourcePaths, @@ -126,8 +133,7 @@ func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) { } } - retriever := keyretriever.DefaultRetriever(b.cfg.KeychainPassword) - return retriever.RetrieveKey(b.cfg.Storage, localStateDst) + return b.retriever.RetrieveKey(b.cfg.Storage, localStateDst) } // extractCategory calls the appropriate extract function for a category. diff --git a/crypto/keyretriever/keyretriever_darwin.go b/crypto/keyretriever/keyretriever_darwin.go index c162603..a0881a7 100644 --- a/crypto/keyretriever/keyretriever_darwin.go +++ b/crypto/keyretriever/keyretriever_darwin.go @@ -29,9 +29,22 @@ const securityCmdTimeout = 30 * time.Second // GcoredumpRetriever uses CVE-2025-24204 to extract keychain secrets // by dumping the securityd process memory. Requires root privileges. -type GcoredumpRetriever struct{} +// The result is cached via sync.Once to avoid repeated memory dumps +// when multiple profiles share the same retriever instance. +type GcoredumpRetriever struct { + once sync.Once + key []byte + err error +} func (r *GcoredumpRetriever) RetrieveKey(storage, _ string) ([]byte, error) { + r.once.Do(func() { + r.key, r.err = r.retrieveKeyOnce(storage) + }) + return r.key, r.err +} + +func (r *GcoredumpRetriever) retrieveKeyOnce(storage string) ([]byte, error) { secret, err := DecryptKeychain(storage) if err != nil { return nil, fmt.Errorf("gcoredump: %w", err) @@ -86,10 +99,23 @@ func (r *KeychainPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, erro } // SecurityCmdRetriever uses macOS `security` CLI to query Keychain. -// This may trigger a password dialog on macOS. -type SecurityCmdRetriever struct{} +// This may trigger a password dialog on macOS. The result is cached +// via sync.Once so that multiple profiles sharing the same retriever +// instance only prompt the user once. +type SecurityCmdRetriever struct { + once sync.Once + key []byte + err error +} func (r *SecurityCmdRetriever) RetrieveKey(storage, _ string) ([]byte, error) { + r.once.Do(func() { + r.key, r.err = r.retrieveKeyOnce(storage) + }) + return r.key, r.err +} + +func (r *SecurityCmdRetriever) retrieveKeyOnce(storage string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), securityCmdTimeout) defer cancel()