refactor(browser): simplify credential storage config (#593)

This commit is contained in:
Roger
2026-05-14 16:29:35 +08:00
committed by GitHub
parent 5d67d3c303
commit ecf8ba0585
22 changed files with 286 additions and 243 deletions
+55 -55
View File
@@ -17,81 +17,81 @@ import (
func platformBrowsers() []types.BrowserConfig {
return []types.BrowserConfig{
{
Key: "chrome",
Name: chromeName,
Kind: types.Chromium,
Storage: "Chrome",
UserDataDir: homeDir + "/Library/Application Support/Google/Chrome",
Key: "chrome",
Name: chromeName,
Kind: types.Chromium,
KeychainLabel: "Chrome",
UserDataDir: homeDir + "/Library/Application Support/Google/Chrome",
},
{
Key: "edge",
Name: edgeName,
Kind: types.Chromium,
Storage: "Microsoft Edge",
UserDataDir: homeDir + "/Library/Application Support/Microsoft Edge",
Key: "edge",
Name: edgeName,
Kind: types.Chromium,
KeychainLabel: "Microsoft Edge",
UserDataDir: homeDir + "/Library/Application Support/Microsoft Edge",
},
{
Key: "chromium",
Name: chromiumName,
Kind: types.Chromium,
Storage: "Chromium",
UserDataDir: homeDir + "/Library/Application Support/Chromium",
Key: "chromium",
Name: chromiumName,
Kind: types.Chromium,
KeychainLabel: "Chromium",
UserDataDir: homeDir + "/Library/Application Support/Chromium",
},
{
Key: "chrome-beta",
Name: chromeBetaName,
Kind: types.Chromium,
Storage: "Chrome",
UserDataDir: homeDir + "/Library/Application Support/Google/Chrome Beta",
Key: "chrome-beta",
Name: chromeBetaName,
Kind: types.Chromium,
KeychainLabel: "Chrome",
UserDataDir: homeDir + "/Library/Application Support/Google/Chrome Beta",
},
{
Key: "opera",
Name: operaName,
Kind: types.ChromiumOpera,
Storage: "Opera",
UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.Opera",
Key: "opera",
Name: operaName,
Kind: types.ChromiumOpera,
KeychainLabel: "Opera",
UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.Opera",
},
{
Key: "opera-gx",
Name: operaGXName,
Kind: types.ChromiumOpera,
Storage: "Opera",
UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.OperaGX",
Key: "opera-gx",
Name: operaGXName,
Kind: types.ChromiumOpera,
KeychainLabel: "Opera",
UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.OperaGX",
},
{
Key: "vivaldi",
Name: vivaldiName,
Kind: types.Chromium,
Storage: "Vivaldi",
UserDataDir: homeDir + "/Library/Application Support/Vivaldi",
Key: "vivaldi",
Name: vivaldiName,
Kind: types.Chromium,
KeychainLabel: "Vivaldi",
UserDataDir: homeDir + "/Library/Application Support/Vivaldi",
},
{
Key: "coccoc",
Name: coccocName,
Kind: types.Chromium,
Storage: "CocCoc",
UserDataDir: homeDir + "/Library/Application Support/Coccoc",
Key: "coccoc",
Name: coccocName,
Kind: types.Chromium,
KeychainLabel: "CocCoc",
UserDataDir: homeDir + "/Library/Application Support/Coccoc",
},
{
Key: "brave",
Name: braveName,
Kind: types.Chromium,
Storage: "Brave",
UserDataDir: homeDir + "/Library/Application Support/BraveSoftware/Brave-Browser",
Key: "brave",
Name: braveName,
Kind: types.Chromium,
KeychainLabel: "Brave",
UserDataDir: homeDir + "/Library/Application Support/BraveSoftware/Brave-Browser",
},
{
Key: "yandex",
Name: yandexName,
Kind: types.ChromiumYandex,
Storage: "Yandex",
UserDataDir: homeDir + "/Library/Application Support/Yandex/YandexBrowser",
Key: "yandex",
Name: yandexName,
Kind: types.ChromiumYandex,
KeychainLabel: "Yandex",
UserDataDir: homeDir + "/Library/Application Support/Yandex/YandexBrowser",
},
{
Key: "arc",
Name: arcName,
Kind: types.Chromium,
Storage: "Arc",
UserDataDir: homeDir + "/Library/Application Support/Arc/User Data",
Key: "arc",
Name: arcName,
Kind: types.Chromium,
KeychainLabel: "Arc",
UserDataDir: homeDir + "/Library/Application Support/Arc/User Data",
},
{
Key: "firefox",
+35 -35
View File
@@ -10,53 +10,53 @@ import (
func platformBrowsers() []types.BrowserConfig {
return []types.BrowserConfig{
{
Key: "chrome",
Name: chromeName,
Kind: types.Chromium,
Storage: "Chrome Safe Storage",
UserDataDir: homeDir + "/.config/google-chrome",
Key: "chrome",
Name: chromeName,
Kind: types.Chromium,
KeychainLabel: "Chrome Safe Storage",
UserDataDir: homeDir + "/.config/google-chrome",
},
{
Key: "edge",
Name: edgeName,
Kind: types.Chromium,
Storage: "Chromium Safe Storage",
UserDataDir: homeDir + "/.config/microsoft-edge",
Key: "edge",
Name: edgeName,
Kind: types.Chromium,
KeychainLabel: "Chromium Safe Storage",
UserDataDir: homeDir + "/.config/microsoft-edge",
},
{
Key: "chromium",
Name: chromiumName,
Kind: types.Chromium,
Storage: "Chromium Safe Storage",
UserDataDir: homeDir + "/.config/chromium",
Key: "chromium",
Name: chromiumName,
Kind: types.Chromium,
KeychainLabel: "Chromium Safe Storage",
UserDataDir: homeDir + "/.config/chromium",
},
{
Key: "chrome-beta",
Name: chromeBetaName,
Kind: types.Chromium,
Storage: "Chrome Safe Storage",
UserDataDir: homeDir + "/.config/google-chrome-beta",
Key: "chrome-beta",
Name: chromeBetaName,
Kind: types.Chromium,
KeychainLabel: "Chrome Safe Storage",
UserDataDir: homeDir + "/.config/google-chrome-beta",
},
{
Key: "opera",
Name: operaName,
Kind: types.ChromiumOpera,
Storage: "Chromium Safe Storage",
UserDataDir: homeDir + "/.config/opera",
Key: "opera",
Name: operaName,
Kind: types.ChromiumOpera,
KeychainLabel: "Chromium Safe Storage",
UserDataDir: homeDir + "/.config/opera",
},
{
Key: "vivaldi",
Name: vivaldiName,
Kind: types.Chromium,
Storage: "Chrome Safe Storage",
UserDataDir: homeDir + "/.config/vivaldi",
Key: "vivaldi",
Name: vivaldiName,
Kind: types.Chromium,
KeychainLabel: "Chrome Safe Storage",
UserDataDir: homeDir + "/.config/vivaldi",
},
{
Key: "brave",
Name: braveName,
Kind: types.Chromium,
Storage: "Brave Safe Storage",
UserDataDir: homeDir + "/.config/BraveSoftware/Brave-Browser",
Key: "brave",
Name: braveName,
Kind: types.Chromium,
KeychainLabel: "Brave Safe Storage",
UserDataDir: homeDir + "/.config/BraveSoftware/Brave-Browser",
},
{
Key: "firefox",
+5 -5
View File
@@ -13,14 +13,14 @@ func platformBrowsers() []types.BrowserConfig {
Key: "chrome",
Name: chromeName,
Kind: types.Chromium,
Storage: "chrome",
WindowsABE: true,
UserDataDir: homeDir + "/AppData/Local/Google/Chrome/User Data",
},
{
Key: "edge",
Name: edgeName,
Kind: types.Chromium,
Storage: "edge",
WindowsABE: true,
UserDataDir: homeDir + "/AppData/Local/Microsoft/Edge/User Data",
},
{
@@ -33,7 +33,7 @@ func platformBrowsers() []types.BrowserConfig {
Key: "chrome-beta",
Name: chromeBetaName,
Kind: types.Chromium,
Storage: "chrome-beta",
WindowsABE: true,
UserDataDir: homeDir + "/AppData/Local/Google/Chrome Beta/User Data",
},
{
@@ -58,14 +58,14 @@ func platformBrowsers() []types.BrowserConfig {
Key: "coccoc",
Name: coccocName,
Kind: types.Chromium,
Storage: "coccoc",
WindowsABE: true,
UserDataDir: homeDir + "/AppData/Local/CocCoc/Browser/User Data",
},
{
Key: "brave",
Name: braveName,
Kind: types.Chromium,
Storage: "brave",
WindowsABE: true,
UserDataDir: homeDir + "/AppData/Local/BraveSoftware/Brave-Browser/User Data",
},
{
+25 -12
View File
@@ -8,25 +8,38 @@ import (
"github.com/moond4rk/hackbrowserdata/utils/winutil"
)
// TestWinUtilTableCoversABEBrowsers verifies that every Windows browser
// with ABE support in winutil.Table has a matching Storage key in
// platformBrowsers(). A mismatch means adding a new Chromium fork was
// incomplete: either the BrowserConfig row lacks Storage: "<key>", or
// winutil.Table has a stale entry nobody retrieves keys for.
// TestWinUtilTableCoversABEBrowsers verifies that the set of Windows browsers
// with WindowsABE: true in platformBrowsers() exactly matches the set of
// winutil.Table entries that declare ABE support (keyed by BrowserConfig.Key ==
// winutil.Entry.Key). A mismatch means adding a new Chromium fork was
// incomplete: either a BrowserConfig row is missing WindowsABE: true, or
// winutil.Table has a stale/missing entry.
func TestWinUtilTableCoversABEBrowsers(t *testing.T) {
storages := make(map[string]struct{})
abeConfigs := make(map[string]struct{})
for _, b := range platformBrowsers() {
if b.Storage != "" {
storages[b.Storage] = struct{}{}
if b.WindowsABE {
abeConfigs[b.Key] = struct{}{}
}
}
abeTable := make(map[string]struct{})
for key, entry := range winutil.Table {
if entry.ABE == winutil.ABENone {
continue
if entry.Key != key {
t.Errorf("winutil.Table[%q].Key = %q; map key and Entry.Key must match (winutil.Entry doc invariant)", key, entry.Key)
}
if _, ok := storages[key]; !ok {
t.Errorf("winutil.Table[%q] declares ABE support but no BrowserConfig.Storage matches — either fix the table or set Storage: %q in platformBrowsers()", key, key)
if entry.ABE != winutil.ABENone {
abeTable[key] = struct{}{}
}
}
for key := range abeTable {
if _, ok := abeConfigs[key]; !ok {
t.Errorf("winutil.Table[%q] declares ABE support but no BrowserConfig with Key %q sets WindowsABE: true — either fix the table or set WindowsABE: true in platformBrowsers()", key, key)
}
}
for key := range abeConfigs {
if _, ok := abeTable[key]; !ok {
t.Errorf("BrowserConfig with Key %q sets WindowsABE: true but winutil.Table[%q] is missing or declares no ABE — add the table entry", key, key)
}
}
}
+10 -1
View File
@@ -195,7 +195,16 @@ func (b *Browser) getMasterKeys(session *filemanager.Session) keyretriever.Maste
break
}
keys, err := keyretriever.NewMasterKeys(b.retrievers, b.cfg.Storage, localStateDst)
abeKey := ""
if b.cfg.WindowsABE {
abeKey = b.cfg.Key
}
hints := keyretriever.Hints{
KeychainLabel: b.cfg.KeychainLabel,
WindowsABEKey: abeKey,
LocalStatePath: localStateDst,
}
keys, err := keyretriever.NewMasterKeys(b.retrievers, hints)
if err != nil {
installKey := b.BrowserName() + "|" + b.cfg.UserDataDir
if _, already := warnedMasterKeyFailure.LoadOrStore(installKey, struct{}{}); !already {
+70 -36
View File
@@ -448,17 +448,15 @@ func TestLocalStatePath(t *testing.T) {
// mockRetriever records the arguments passed to RetrieveKey.
type mockRetriever struct {
storage string
localState string
key []byte
err error
called bool
hints keyretriever.Hints
key []byte
err error
called bool
}
func (m *mockRetriever) RetrieveKey(storage, localStatePath string) ([]byte, error) {
func (m *mockRetriever) RetrieveKey(hints keyretriever.Hints) ([]byte, error) {
m.called = true
m.storage = storage
m.localState = localStatePath
m.hints = hints
return m.key, m.err
}
@@ -472,41 +470,41 @@ func TestGetMasterKeys(t *testing.T) {
mkFile(dirNoLocalState, "Default", "History")
tests := []struct {
name string
dir string
storage string
retriever keyretriever.KeyRetriever // nil → don't call SetKeyRetrievers
wantV10 []byte
wantStorage string
wantLocalState bool // whether localStatePath passed to retriever is non-empty
name string
dir string
keychainLabel string
retriever keyretriever.KeyRetriever // nil → don't call SetKeyRetrievers
wantV10 []byte
wantKeychainLabel string
wantLocalState bool // whether localStatePath passed to retriever is non-empty
}{
{
name: "nil retriever yields empty keys",
dir: fixture.chrome,
},
{
name: "with Local State passes path to retriever",
dir: fixture.chrome,
storage: "Chrome",
retriever: &mockRetriever{key: []byte("fake-master-key")},
wantV10: []byte("fake-master-key"),
wantStorage: "Chrome",
wantLocalState: true,
name: "with Local State passes path to retriever",
dir: fixture.chrome,
keychainLabel: "Chrome",
retriever: &mockRetriever{key: []byte("fake-master-key")},
wantV10: []byte("fake-master-key"),
wantKeychainLabel: "Chrome",
wantLocalState: true,
},
{
name: "without Local State passes empty path",
dir: dirNoLocalState,
storage: "Chromium",
retriever: &mockRetriever{key: []byte("derived-key")},
wantV10: []byte("derived-key"),
wantStorage: "Chromium",
name: "without Local State passes empty path",
dir: dirNoLocalState,
keychainLabel: "Chromium",
retriever: &mockRetriever{key: []byte("derived-key")},
wantV10: []byte("derived-key"),
wantKeychainLabel: "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,
Name: "Test", Kind: types.Chromium, UserDataDir: tt.dir, KeychainLabel: tt.keychainLabel,
})
require.NoError(t, err)
require.NotEmpty(t, browsers)
@@ -531,11 +529,11 @@ func TestGetMasterKeys(t *testing.T) {
mock, ok := tt.retriever.(*mockRetriever)
require.True(t, ok)
assert.True(t, mock.called)
assert.Equal(t, tt.wantStorage, mock.storage)
assert.Equal(t, tt.wantKeychainLabel, mock.hints.KeychainLabel)
if tt.wantLocalState {
assert.NotEmpty(t, mock.localState)
assert.NotEmpty(t, mock.hints.LocalStatePath)
} else {
assert.Empty(t, mock.localState)
assert.Empty(t, mock.hints.LocalStatePath)
}
})
}
@@ -553,7 +551,7 @@ func TestGetMasterKeys_AllTiersInvoked(t *testing.T) {
v20mock := &mockRetriever{key: []byte("fake-v20-key")}
browsers, err := NewBrowsers(types.BrowserConfig{
Name: "Test", Kind: types.Chromium, UserDataDir: fixture.chrome, Storage: "Chrome",
Name: "Test", Kind: types.Chromium, UserDataDir: fixture.chrome, KeychainLabel: "Chrome",
})
require.NoError(t, err)
require.NotEmpty(t, browsers)
@@ -573,8 +571,44 @@ func TestGetMasterKeys_AllTiersInvoked(t *testing.T) {
assert.True(t, v11mock.called, "V11 retriever must be called — no silent bypass")
assert.True(t, v20mock.called, "V20 retriever must be called — no silent bypass")
for _, m := range []*mockRetriever{v10mock, v11mock, v20mock} {
assert.Equal(t, "Chrome", m.storage)
assert.NotEmpty(t, m.localState, "Local State path must be passed to every retriever")
assert.Equal(t, "Chrome", m.hints.KeychainLabel)
assert.NotEmpty(t, m.hints.LocalStatePath, "Local State path must be passed to every retriever")
}
}
// TestGetMasterKeys_WindowsABEThreading pins cfg.WindowsABE → hints.WindowsABEKey threading. A
// regression here silently disables Windows ABE decryption with no dev-box-test signal — only the
// windows-tunnel sandbox 574-cookie regression would catch it — so it must be pinned at unit level.
func TestGetMasterKeys_WindowsABEThreading(t *testing.T) {
tests := []struct {
name string
key string
windowsABE bool
wantABEKey string
}{
{"WindowsABE=true threads cfg.Key into hints.WindowsABEKey", "chrome", true, "chrome"},
{"WindowsABE=false leaves hints.WindowsABEKey empty", "opera", false, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &mockRetriever{key: []byte("k")}
browsers, err := NewBrowsers(types.BrowserConfig{
Key: tt.key, Name: "Test", Kind: types.Chromium,
UserDataDir: fixture.chrome, WindowsABE: tt.windowsABE,
})
require.NoError(t, err)
require.NotEmpty(t, browsers)
b := browsers[0]
b.SetKeyRetrievers(keyretriever.Retrievers{V20: mock})
session, err := filemanager.NewSession()
require.NoError(t, err)
defer session.Cleanup()
b.getMasterKeys(session)
assert.Equal(t, tt.wantABEKey, mock.hints.WindowsABEKey)
})
}
}
@@ -605,7 +639,7 @@ func TestExtract(t *testing.T) {
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",
Name: "Test", Kind: types.Chromium, UserDataDir: dir, KeychainLabel: "Chrome",
})
require.NoError(t, err)
require.Len(t, browsers, 1)