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()