mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-21 19:06:47 +02:00
fix: share key retriever across all browsers to avoid repeated prompts (#560)
* fix: share key retriever across all browsers to avoid repeated password prompts
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -16,15 +17,15 @@ import (
|
||||
type Browser struct {
|
||||
cfg types.BrowserConfig
|
||||
profileDir string // absolute path to profile directory
|
||||
retriever keyretriever.KeyRetriever // shared across profiles of the same browser
|
||||
retriever keyretriever.KeyRetriever // set via SetRetriever after construction
|
||||
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
|
||||
}
|
||||
|
||||
// NewBrowsers discovers Chromium profiles under cfg.UserDataDir and returns
|
||||
// one Browser per profile. Uses ReadDir to find profile directories,
|
||||
// then Stat to check which data sources exist in each profile.
|
||||
// one Browser per profile. Call SetRetriever on each returned browser before
|
||||
// Extract to enable decryption of sensitive data (passwords, cookies, etc.).
|
||||
func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
sources := sourcesForKind(cfg.Kind)
|
||||
extractors := extractorsForKind(cfg.Kind)
|
||||
@@ -34,11 +35,6 @@ 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)
|
||||
@@ -48,7 +44,6 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
browsers = append(browsers, &Browser{
|
||||
cfg: cfg,
|
||||
profileDir: profileDir,
|
||||
retriever: retriever,
|
||||
sources: sources,
|
||||
extractors: extractors,
|
||||
sourcePaths: sourcePaths,
|
||||
@@ -57,6 +52,13 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
return browsers, nil
|
||||
}
|
||||
|
||||
// SetRetriever sets the key retriever used by Extract to obtain the
|
||||
// master encryption key. Must be called before Extract if encrypted
|
||||
// data (passwords, cookies, credit cards) needs to be decrypted.
|
||||
func (b *Browser) SetRetriever(r keyretriever.KeyRetriever) {
|
||||
b.retriever = r
|
||||
}
|
||||
|
||||
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||
func (b *Browser) ProfileDir() string { return b.profileDir }
|
||||
func (b *Browser) ProfileName() string {
|
||||
@@ -120,6 +122,10 @@ func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.
|
||||
// The retriever is always called regardless of whether Local State exists,
|
||||
// because macOS/Linux retrievers don't need it.
|
||||
func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) {
|
||||
if b.retriever == nil {
|
||||
return nil, fmt.Errorf("key retriever not set for %s", b.cfg.Name)
|
||||
}
|
||||
|
||||
// Try to locate and copy Local State (needed on Windows, ignored on macOS/Linux).
|
||||
// Multi-profile layout: Local State is in the parent of profileDir.
|
||||
// Flat layout (Opera): Local State is alongside data files in profileDir.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
"github.com/moond4rk/hackbrowserdata/filemanager"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
@@ -438,3 +439,172 @@ func TestLocalStatePath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getMasterKey
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// mockRetriever records the arguments passed to RetrieveKey.
|
||||
type mockRetriever struct {
|
||||
storage string
|
||||
localState string
|
||||
key []byte
|
||||
err error
|
||||
called bool
|
||||
}
|
||||
|
||||
func (m *mockRetriever) RetrieveKey(storage, localStatePath string) ([]byte, error) {
|
||||
m.called = true
|
||||
m.storage = storage
|
||||
m.localState = localStatePath
|
||||
return m.key, m.err
|
||||
}
|
||||
|
||||
func TestGetMasterKey(t *testing.T) {
|
||||
// Profile directory without Local State file.
|
||||
dirNoLocalState := t.TempDir()
|
||||
mkFile(dirNoLocalState, "Default", "Preferences")
|
||||
mkFile(dirNoLocalState, "Default", "History")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
storage string
|
||||
retriever keyretriever.KeyRetriever // nil → don't call SetRetriever
|
||||
wantKey []byte
|
||||
wantErr string
|
||||
wantStorage string
|
||||
wantLocalState bool // whether localStatePath passed to retriever is non-empty
|
||||
}{
|
||||
{
|
||||
name: "nil retriever returns error",
|
||||
dir: fixture.chrome,
|
||||
wantErr: "key retriever not set",
|
||||
},
|
||||
{
|
||||
name: "with Local State passes path to retriever",
|
||||
dir: fixture.chrome,
|
||||
storage: "Chrome",
|
||||
retriever: &mockRetriever{key: []byte("fake-master-key")},
|
||||
wantKey: []byte("fake-master-key"),
|
||||
wantStorage: "Chrome",
|
||||
wantLocalState: true,
|
||||
},
|
||||
{
|
||||
name: "without Local State passes empty path",
|
||||
dir: dirNoLocalState,
|
||||
storage: "Chromium",
|
||||
retriever: &mockRetriever{key: []byte("derived-key")},
|
||||
wantKey: []byte("derived-key"),
|
||||
wantStorage: "Chromium",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
browsers, err := NewBrowsers(types.BrowserConfig{
|
||||
Name: "Test", Kind: types.Chromium, UserDataDir: tt.dir, Storage: tt.storage,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, browsers)
|
||||
|
||||
b := browsers[0]
|
||||
if tt.retriever != nil {
|
||||
b.SetRetriever(tt.retriever)
|
||||
}
|
||||
|
||||
session, err := filemanager.NewSession()
|
||||
require.NoError(t, err)
|
||||
defer session.Cleanup()
|
||||
|
||||
key, err := b.getMasterKey(session)
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantKey, key)
|
||||
|
||||
mock, ok := tt.retriever.(*mockRetriever)
|
||||
require.True(t, ok)
|
||||
assert.True(t, mock.called)
|
||||
assert.Equal(t, tt.wantStorage, mock.storage)
|
||||
if tt.wantLocalState {
|
||||
assert.NotEmpty(t, mock.localState)
|
||||
} else {
|
||||
assert.Empty(t, mock.localState)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestExtract(t *testing.T) {
|
||||
// Shared fixture: profile with a real History database.
|
||||
dir := t.TempDir()
|
||||
mkFile(dir, "Default", "Preferences")
|
||||
|
||||
historyDB := createTestDB(t, "History", urlsSchema,
|
||||
insertURL("https://example.com", "Example", 5, 13350000000000000),
|
||||
)
|
||||
profileDir := filepath.Join(dir, "Default")
|
||||
data, err := os.ReadFile(historyDB)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profileDir, "History"), data, 0o644))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
retriever keyretriever.KeyRetriever // nil → don't call SetRetriever
|
||||
wantRetriever bool // whether retriever should be called
|
||||
}{
|
||||
{
|
||||
name: "without retriever extracts unencrypted data",
|
||||
},
|
||||
{
|
||||
name: "with mock retriever",
|
||||
retriever: &mockRetriever{key: []byte("test-key-16bytes")},
|
||||
wantRetriever: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
browsers, err := NewBrowsers(types.BrowserConfig{
|
||||
Name: "Test", Kind: types.Chromium, UserDataDir: dir, Storage: "Chrome",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, browsers, 1)
|
||||
|
||||
if tt.retriever != nil {
|
||||
browsers[0].SetRetriever(tt.retriever)
|
||||
}
|
||||
|
||||
result, err := browsers[0].Extract([]types.Category{types.History})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, result.Histories, 1)
|
||||
assert.Equal(t, "Example", result.Histories[0].Title)
|
||||
|
||||
if tt.wantRetriever {
|
||||
mock, ok := tt.retriever.(*mockRetriever)
|
||||
require.True(t, ok)
|
||||
assert.True(t, mock.called)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SetRetriever: verify *Browser satisfies the interface used by
|
||||
// browser.pickFromConfigs for post-construction retriever injection.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSetRetriever_SatisfiesInterface(t *testing.T) {
|
||||
var _ interface {
|
||||
SetRetriever(keyretriever.KeyRetriever)
|
||||
} = (*Browser)(nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user