mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
refactor(browser): simplify credential storage config (#593)
This commit is contained in:
+55
-55
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -25,17 +25,15 @@ var errNoABEKey = errors.New("abe: Local State has no app_bound_encrypted_key")
|
||||
|
||||
type ABERetriever struct{}
|
||||
|
||||
func (r *ABERetriever) RetrieveKey(storage, localStatePath string) ([]byte, error) {
|
||||
// Non-ABE Chromium forks (Opera/Vivaldi/Yandex/...) call this with an empty storage key; pre-v20
|
||||
// Chrome profiles have no app_bound_encrypted_key in Local State. Both are "ABE not applicable" —
|
||||
// return (nil, nil) so ChainRetriever falls through to DPAPI silently instead of emitting a Warnf
|
||||
// for every non-ABE browser.
|
||||
browserKey := strings.TrimSpace(storage)
|
||||
func (r *ABERetriever) RetrieveKey(hints Hints) ([]byte, error) {
|
||||
// Non-ABE forks (Opera/Vivaldi/Yandex) supply no WindowsABEKey — treat as "not applicable".
|
||||
// (Pre-v20 Chrome takes the errNoABEKey path below.)
|
||||
browserKey := strings.TrimSpace(hints.WindowsABEKey)
|
||||
if browserKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
encKey, err := loadEncryptedKey(localStatePath)
|
||||
encKey, err := loadEncryptedKey(hints.LocalStatePath)
|
||||
if errors.Is(err, errNoABEKey) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -20,13 +20,16 @@ import (
|
||||
// has no storage lookup.
|
||||
var errStorageNotFound = errors.New("not found in credential store") //nolint:unused // only used on darwin and linux
|
||||
|
||||
// KeyRetriever retrieves the master encryption key for a Chromium-based browser. Each platform has
|
||||
// different implementations:
|
||||
// - macOS: Keychain access (security command) or gcoredump exploit
|
||||
// - Windows: DPAPI decryption of Local State file
|
||||
// - Linux: D-Bus Secret Service or fallback to "peanuts" password
|
||||
// Hints bundles inputs for KeyRetriever; each retriever reads only the field that applies to it.
|
||||
type Hints struct {
|
||||
KeychainLabel string // macOS Keychain account / Linux D-Bus Secret Service label
|
||||
WindowsABEKey string // Windows ABE browser key (e.g. "chrome"); "" → ABE not applicable
|
||||
LocalStatePath string // path to (temp-copied) Local State JSON; only used on Windows
|
||||
}
|
||||
|
||||
// KeyRetriever retrieves the master encryption key for a Chromium-based browser.
|
||||
type KeyRetriever interface {
|
||||
RetrieveKey(storage, localStatePath string) ([]byte, error)
|
||||
RetrieveKey(hints Hints) ([]byte, error)
|
||||
}
|
||||
|
||||
// ChainRetriever tries multiple retrievers in order, returning the first success. Used on macOS
|
||||
@@ -40,10 +43,10 @@ func NewChain(retrievers ...KeyRetriever) KeyRetriever {
|
||||
return &ChainRetriever{retrievers: retrievers}
|
||||
}
|
||||
|
||||
func (c *ChainRetriever) RetrieveKey(storage, localStatePath string) ([]byte, error) {
|
||||
func (c *ChainRetriever) RetrieveKey(hints Hints) ([]byte, error) {
|
||||
var errs []error
|
||||
for _, r := range c.retrievers {
|
||||
key, err := r.RetrieveKey(storage, localStatePath)
|
||||
key, err := r.RetrieveKey(hints)
|
||||
if err == nil && len(key) > 0 {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ type GcoredumpRetriever struct {
|
||||
// through to the next retriever silently. The most common failure ("requires root privileges")
|
||||
// is documented expected behavior, not a warning-worthy condition; surfacing it on every profile
|
||||
// would drown out genuine warnings. The same pattern is used by ABERetriever (see abe_windows.go).
|
||||
func (r *GcoredumpRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
func (r *GcoredumpRetriever) RetrieveKey(hints Hints) ([]byte, error) {
|
||||
r.once.Do(func() {
|
||||
r.records, r.err = DecryptKeychainRecords()
|
||||
})
|
||||
@@ -52,7 +52,7 @@ func (r *GcoredumpRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
return nil, nil //nolint:nilerr // intentional silent fallthrough
|
||||
}
|
||||
|
||||
key, err := findStorageKey(r.records, storage)
|
||||
key, err := findStorageKey(r.records, hints.KeychainLabel)
|
||||
if err != nil {
|
||||
log.Debugf("gcoredump: %v", err)
|
||||
return nil, nil //nolint:nilerr // intentional silent fallthrough
|
||||
@@ -96,7 +96,7 @@ type KeychainPasswordRetriever struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *KeychainPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
func (r *KeychainPasswordRetriever) RetrieveKey(hints Hints) ([]byte, error) {
|
||||
if r.Password == "" {
|
||||
return nil, fmt.Errorf("keychain password not provided")
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func (r *KeychainPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, erro
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
return findStorageKey(r.records, storage)
|
||||
return findStorageKey(r.records, hints.KeychainLabel)
|
||||
}
|
||||
|
||||
// SecurityCmdRetriever uses macOS `security` CLI to query Keychain.
|
||||
@@ -124,7 +124,8 @@ type securityResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *SecurityCmdRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
func (r *SecurityCmdRetriever) RetrieveKey(hints Hints) ([]byte, error) {
|
||||
storage := hints.KeychainLabel
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestFindStorageKey_NotFound(t *testing.T) {
|
||||
|
||||
func TestKeychainPasswordRetriever_EmptyPassword(t *testing.T) {
|
||||
r := &KeychainPasswordRetriever{Password: ""}
|
||||
key, err := r.RetrieveKey("Chrome", "")
|
||||
key, err := r.RetrieveKey(Hints{KeychainLabel: "Chrome"})
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, key)
|
||||
assert.Contains(t, err.Error(), "keychain password not provided")
|
||||
|
||||
@@ -21,7 +21,8 @@ var linuxParams = pbkdf2Params{
|
||||
// DBusRetriever queries GNOME Keyring / KDE Wallet via D-Bus Secret Service.
|
||||
type DBusRetriever struct{}
|
||||
|
||||
func (r *DBusRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
func (r *DBusRetriever) RetrieveKey(hints Hints) ([]byte, error) {
|
||||
storage := hints.KeychainLabel
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dbus session: %w", err)
|
||||
@@ -74,7 +75,7 @@ func (r *DBusRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
// the "v10" prefix when no keyring is available (headless servers, Docker, CI).
|
||||
type PosixRetriever struct{}
|
||||
|
||||
func (r *PosixRetriever) RetrieveKey(_, _ string) ([]byte, error) {
|
||||
func (r *PosixRetriever) RetrieveKey(_ Hints) ([]byte, error) {
|
||||
return linuxParams.deriveKey([]byte("peanuts")), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func TestPosixRetriever(t *testing.T) {
|
||||
r := &PosixRetriever{}
|
||||
|
||||
key, err := r.RetrieveKey("Chrome", "")
|
||||
key, err := r.RetrieveKey(Hints{KeychainLabel: "Chrome"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, linuxParams.deriveKey([]byte("peanuts")), key)
|
||||
assert.Len(t, key, linuxParams.keySize)
|
||||
@@ -27,9 +27,9 @@ func TestPosixRetriever(t *testing.T) {
|
||||
}
|
||||
assert.False(t, allZero, "derived key should not be all zeros")
|
||||
|
||||
// "peanuts" is a hardcoded password, so the result should be the same regardless of storage
|
||||
// name or number of calls.
|
||||
key2, err := r.RetrieveKey("Brave", "")
|
||||
// "peanuts" is a hardcoded password, so the result should be the same regardless of the hints
|
||||
// or number of calls.
|
||||
key2, err := r.RetrieveKey(Hints{KeychainLabel: "Brave"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, key, key2, "kV10Key should be constant across any storage label")
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func TestPosixRetriever_MatchesChromiumKV10Key(t *testing.T) {
|
||||
0x9d, 0xfa, 0x14, 0x7c, 0xa9, 0x27, 0x27, 0x78,
|
||||
}
|
||||
r := &PosixRetriever{}
|
||||
key, err := r.RetrieveKey("", "")
|
||||
key, err := r.RetrieveKey(Hints{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, key)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type mockRetriever struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockRetriever) RetrieveKey(_, _ string) ([]byte, error) {
|
||||
func (m *mockRetriever) RetrieveKey(_ Hints) ([]byte, error) {
|
||||
return m.key, m.err
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestChainRetriever_FirstSuccess(t *testing.T) {
|
||||
&mockRetriever{key: []byte("first-key")},
|
||||
&mockRetriever{key: []byte("second-key")},
|
||||
)
|
||||
key, err := chain.RetrieveKey("Chrome", "")
|
||||
key, err := chain.RetrieveKey(Hints{KeychainLabel: "Chrome"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("first-key"), key)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestChainRetriever_FallbackOnError(t *testing.T) {
|
||||
&mockRetriever{err: errors.New("first failed")},
|
||||
&mockRetriever{key: []byte("fallback-key")},
|
||||
)
|
||||
key, err := chain.RetrieveKey("Chrome", "")
|
||||
key, err := chain.RetrieveKey(Hints{KeychainLabel: "Chrome"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("fallback-key"), key)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func TestChainRetriever_AllFail(t *testing.T) {
|
||||
&mockRetriever{err: errors.New("first failed")},
|
||||
&mockRetriever{err: errors.New("second failed")},
|
||||
)
|
||||
key, err := chain.RetrieveKey("Chrome", "")
|
||||
key, err := chain.RetrieveKey(Hints{KeychainLabel: "Chrome"})
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, key)
|
||||
assert.Contains(t, err.Error(), "all retrievers failed")
|
||||
@@ -56,14 +56,14 @@ func TestChainRetriever_SkipEmptyKey(t *testing.T) {
|
||||
&mockRetriever{key: nil, err: nil},
|
||||
&mockRetriever{key: []byte("real-key")},
|
||||
)
|
||||
key, err := chain.RetrieveKey("Chrome", "")
|
||||
key, err := chain.RetrieveKey(Hints{KeychainLabel: "Chrome"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("real-key"), key)
|
||||
}
|
||||
|
||||
func TestChainRetriever_Empty(t *testing.T) {
|
||||
chain := NewChain()
|
||||
key, err := chain.RetrieveKey("Chrome", "")
|
||||
key, err := chain.RetrieveKey(Hints{KeychainLabel: "Chrome"})
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
// and decrypts it using Windows DPAPI.
|
||||
type DPAPIRetriever struct{}
|
||||
|
||||
func (r *DPAPIRetriever) RetrieveKey(_, localStatePath string) ([]byte, error) {
|
||||
data, err := os.ReadFile(localStatePath)
|
||||
func (r *DPAPIRetriever) RetrieveKey(hints Hints) ([]byte, error) {
|
||||
data, err := os.ReadFile(hints.LocalStatePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read Local State: %w", err)
|
||||
}
|
||||
|
||||
@@ -5,47 +5,26 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MasterKeys bundles the per-cipher-version Chromium master keys used to decrypt data from a
|
||||
// single profile. decryptValue dispatches on the ciphertext's version prefix and picks the
|
||||
// matching key; a missing (nil) key for a tier means "that cipher version cannot be decrypted",
|
||||
// but the other tiers remain usable — a Chrome 127+ profile upgraded from pre-127 carries mixed
|
||||
// v10+v20 ciphertexts, and Linux profiles may carry mixed v10+v11 for analogous reasons.
|
||||
//
|
||||
// - V10: Chrome 80+ key with "v10" cipher prefix.
|
||||
// - Windows: os_crypt.encrypted_key decrypted by user-level DPAPI (AES-GCM ciphertexts).
|
||||
// - macOS: derived from Keychain via PBKDF2(1003, SHA-1) (AES-CBC ciphertexts).
|
||||
// - Linux: derived from "peanuts" hardcoded password (Chromium's kV10Key, AES-CBC).
|
||||
// - V11: Chrome Linux key with "v11" cipher prefix, derived from D-Bus Secret Service
|
||||
// (KWallet / GNOME Keyring) via PBKDF2. Nil on Windows and macOS (v11 prefix not used there).
|
||||
// - V20: Chrome 127+ Windows key with "v20" cipher prefix, retrieved via reflective injection
|
||||
// into the browser's elevation service. Nil on non-Windows platforms.
|
||||
// MasterKeys holds per-cipher-version Chromium master keys. A profile may carry mixed prefixes
|
||||
// (Chrome 127+ on Windows mixes v10+v20; Linux can mix v10+v11), so each tier must be populated
|
||||
// independently for lossless decryption. A nil tier means that cipher version cannot be decrypted.
|
||||
type MasterKeys struct {
|
||||
V10 []byte
|
||||
V11 []byte
|
||||
V20 []byte
|
||||
}
|
||||
|
||||
// Retrievers is the per-tier retriever configuration passed to NewMasterKeys. Each slot runs
|
||||
// independently — failure or absence of one tier does not affect others. Platform injectors set
|
||||
// only the slots that apply to their platform and leave the rest nil (e.g. Linux populates
|
||||
// V10+V11, leaves V20 nil).
|
||||
// Retrievers is the per-tier retriever configuration; unused slots are nil.
|
||||
type Retrievers struct {
|
||||
V10 KeyRetriever
|
||||
V11 KeyRetriever
|
||||
V20 KeyRetriever
|
||||
}
|
||||
|
||||
// NewMasterKeys fetches every configured tier in r independently and returns the assembled
|
||||
// MasterKeys together with any per-tier errors joined into one. Nil retrievers and retrievers
|
||||
// returning (nil, nil) (the "not applicable" signal — e.g. ABERetriever on a non-ABE fork)
|
||||
// contribute nil keys silently; only non-nil errors propagate.
|
||||
//
|
||||
// The returned error, when non-nil, is an errors.Join of per-tier failures formatted as
|
||||
// "<tier>: <err>" (e.g. "v10: dpapi decrypt: ..."). Callers are expected to log it at whatever
|
||||
// severity fits their context — this function itself never logs, leaving logging policy to its
|
||||
// callers. Other pieces of the keyretriever package (e.g. ChainRetriever) may still log on their
|
||||
// own failures; the "no-logging" guarantee is scoped to NewMasterKeys.
|
||||
func NewMasterKeys(r Retrievers, storage, localStatePath string) (MasterKeys, error) {
|
||||
// NewMasterKeys fetches each non-nil tier in r and returns the assembled MasterKeys with per-tier
|
||||
// errors joined. A retriever returning (nil, nil) signals "not applicable" and contributes no key
|
||||
// silently. This function never logs; the caller decides severity.
|
||||
func NewMasterKeys(r Retrievers, hints Hints) (MasterKeys, error) {
|
||||
var keys MasterKeys
|
||||
var errs []error
|
||||
|
||||
@@ -61,7 +40,7 @@ func NewMasterKeys(r Retrievers, storage, localStatePath string) (MasterKeys, er
|
||||
if t.r == nil {
|
||||
continue
|
||||
}
|
||||
k, err := t.r.RetrieveKey(storage, localStatePath)
|
||||
k, err := t.r.RetrieveKey(hints)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", t.name, err))
|
||||
continue
|
||||
|
||||
@@ -10,20 +10,18 @@ import (
|
||||
)
|
||||
|
||||
// recordingRetriever captures call count and arguments so tests can verify each tier's retriever
|
||||
// is invoked exactly once with the expected storage and localStatePath.
|
||||
// is invoked exactly once with the expected hints.
|
||||
type recordingRetriever struct {
|
||||
key []byte
|
||||
err error
|
||||
|
||||
calls int
|
||||
gotStorage string
|
||||
gotPath string
|
||||
calls int
|
||||
gotHints Hints
|
||||
}
|
||||
|
||||
func (r *recordingRetriever) RetrieveKey(storage, localStatePath string) ([]byte, error) {
|
||||
func (r *recordingRetriever) RetrieveKey(hints Hints) ([]byte, error) {
|
||||
r.calls++
|
||||
r.gotStorage = storage
|
||||
r.gotPath = localStatePath
|
||||
r.gotHints = hints
|
||||
return r.key, r.err
|
||||
}
|
||||
|
||||
@@ -115,7 +113,7 @@ func TestNewMasterKeys_Matrix(t *testing.T) {
|
||||
r.V20 = tt.v20
|
||||
}
|
||||
|
||||
keys, err := NewMasterKeys(r, "chrome", "/tmp/Local State")
|
||||
keys, err := NewMasterKeys(r, Hints{KeychainLabel: "chrome", LocalStatePath: "/tmp/Local State"})
|
||||
assert.Equal(t, tt.wantV10, keys.V10)
|
||||
assert.Equal(t, tt.wantV11, keys.V11)
|
||||
assert.Equal(t, tt.wantV20, keys.V20)
|
||||
@@ -136,8 +134,7 @@ func TestNewMasterKeys_Matrix(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, 1, mock.calls, "%s retriever should be called exactly once", name)
|
||||
assert.Equal(t, "chrome", mock.gotStorage)
|
||||
assert.Equal(t, "/tmp/Local State", mock.gotPath)
|
||||
assert.Equal(t, Hints{KeychainLabel: "chrome", LocalStatePath: "/tmp/Local State"}, mock.gotHints)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -145,7 +142,7 @@ func TestNewMasterKeys_Matrix(t *testing.T) {
|
||||
|
||||
func TestNewMasterKeys_AllNilRetrievers(t *testing.T) {
|
||||
// All slots nil — macOS/Linux with no retriever wiring, or Windows with neither tier set up.
|
||||
keys, err := NewMasterKeys(Retrievers{}, "chrome", "/tmp/Local State")
|
||||
keys, err := NewMasterKeys(Retrievers{}, Hints{KeychainLabel: "chrome", LocalStatePath: "/tmp/Local State"})
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, keys.V10)
|
||||
assert.Nil(t, keys.V11)
|
||||
@@ -156,14 +153,14 @@ func TestNewMasterKeys_PartialNil(t *testing.T) {
|
||||
// Only V10 wired — typical macOS shape. V11/V20 left nil.
|
||||
k10 := []byte("v10-key-bytes-for-testing")
|
||||
r := &recordingRetriever{key: k10}
|
||||
keys, err := NewMasterKeys(Retrievers{V10: r}, "Chrome", "")
|
||||
keys, err := NewMasterKeys(Retrievers{V10: r}, Hints{KeychainLabel: "Chrome"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, k10, keys.V10)
|
||||
assert.Nil(t, keys.V11)
|
||||
assert.Nil(t, keys.V20)
|
||||
assert.Equal(t, 1, r.calls)
|
||||
assert.Equal(t, "Chrome", r.gotStorage)
|
||||
assert.Equal(t, Hints{KeychainLabel: "Chrome"}, r.gotHints)
|
||||
}
|
||||
|
||||
func TestNewMasterKeys_ErrorWrapping(t *testing.T) {
|
||||
@@ -172,7 +169,7 @@ func TestNewMasterKeys_ErrorWrapping(t *testing.T) {
|
||||
sentinel := errors.New("sentinel")
|
||||
r := Retrievers{V20: &recordingRetriever{err: sentinel}}
|
||||
|
||||
_, err := NewMasterKeys(r, "chrome", "")
|
||||
_, err := NewMasterKeys(r, Hints{KeychainLabel: "chrome"})
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, sentinel, "errors.Is should find wrapped sentinel error")
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ See `types/category.go` for the authoritative enum definition.
|
||||
|
||||
### 4.2 BrowserConfig
|
||||
|
||||
`BrowserConfig` is the declarative, platform-specific browser definition containing: Key (CLI matching), Name (display), Kind (engine), Storage (keychain label), UserDataDir (data path).
|
||||
`BrowserConfig` is the declarative, platform-specific browser definition containing: Key (CLI matching; also the Windows ABE / winutil.Table identifier when WindowsABE is true), Name (display), Kind (engine), KeychainLabel (macOS Keychain / Linux D-Bus Secret Service label), WindowsABE (bool — enable Windows App-Bound Encryption v20 path), UserDataDir (data path).
|
||||
|
||||
### 4.3 Browser Selection Flow
|
||||
|
||||
|
||||
@@ -20,10 +20,17 @@ For Chromium encryption details (cipher versions, AES-CBC/GCM), see [RFC-003](00
|
||||
|
||||
## 2. KeyRetriever Interface
|
||||
|
||||
The interface takes two parameters:
|
||||
The interface takes a single `Hints` struct so caller intent is explicit rather than positional:
|
||||
|
||||
- **`storage`** — platform-dependent identifier. On macOS it's the Keychain account label (e.g. `"Chrome"`); on Linux it's the D-Bus collection label (e.g. `"Chrome Safe Storage"`); on Windows it's the browser key used by `ABERetriever` to locate the elevation-service COM interface (e.g. `"chrome"`, `"edge"`). Ignored by `DPAPIRetriever`.
|
||||
- **`localStatePath`** — path to `Local State` JSON file. Only used on Windows (DPAPI + ABE both read it).
|
||||
```go
|
||||
type Hints struct {
|
||||
KeychainLabel string // macOS Keychain account / Linux D-Bus Secret Service item label (e.g. "Chrome", "Chrome Safe Storage")
|
||||
WindowsABEKey string // Windows ABE browser key used by ABERetriever to locate the elevation-service COM interface (e.g. "chrome", "edge"). "" → ABE not applicable; ABERetriever returns (nil, nil) silently.
|
||||
LocalStatePath string // path to Local State JSON. Only used on Windows (DPAPI + ABE both read it).
|
||||
}
|
||||
```
|
||||
|
||||
Each retriever reads only the field that applies to its platform; the others are ignored. Callers populate `Hints` from `BrowserConfig`: `KeychainLabel` is copied directly, `WindowsABEKey` is set to `cfg.Key` when `cfg.WindowsABE` is true.
|
||||
|
||||
The return value is the **ready-to-use decryption key** — either the raw AES key (Windows) or the PBKDF2-derived key (macOS/Linux).
|
||||
|
||||
@@ -67,11 +74,11 @@ All macOS strategies produce a raw password string from the keychain. This is de
|
||||
| Key length | 16 bytes (AES-128) | |
|
||||
| Hash | HMAC-SHA1 | |
|
||||
|
||||
### 3.4 Storage Labels
|
||||
### 3.4 Keychain Labels
|
||||
|
||||
Each browser identifies its Keychain entry with a short account string — typically the browser's base name (`"Chrome"`, `"Brave"`, `"Arc"`). Edge uses `"Microsoft Edge"`. Related variants share labels rather than defining their own: Chrome Beta aliases onto `"Chrome"`, Opera GX aliases onto `"Opera"`.
|
||||
|
||||
The authoritative mapping lives in the `Storage` field of each entry in `platformBrowsers()` (`browser/browser_darwin.go`).
|
||||
The authoritative mapping lives in the `KeychainLabel` field of each entry in `platformBrowsers()` (`browser/browser_darwin.go`).
|
||||
|
||||
## 4. Windows Key Retrieval
|
||||
|
||||
@@ -119,7 +126,7 @@ Windows populates two slots of the `keyretriever.Retrievers` struct — V10 (leg
|
||||
|
||||
**Why not a ChainRetriever?** `ChainRetriever` has first-success semantics: once ABE returns a key, DPAPI is never called. That semantics is wrong for orthogonal tiers — it was the root cause of issue #578, where upgraded profiles' v10-encrypted passwords silently failed because only the v20 key was retrieved. `NewMasterKeys` evaluates each tier independently and returns an `errors.Join` of per-tier failures; log severity is a caller-side decision. `browser/chromium::getMasterKeys` currently logs all tier errors uniformly at `Warnf` — the distinction between "partial" and "total" failure was judged low-value for a short-lived CLI where all warn lines are visible in the default output.
|
||||
|
||||
**Non-ABE Chromium forks** (Opera, Vivaldi, Yandex, 360, QQ, Sogou) have `Storage: ""` in `platformBrowsers()`. `ABERetriever` returns `(nil, nil)` for empty storage, which `NewMasterKeys` treats silently as "not applicable" — so attempting ABE on these forks is a no-op, not a failure. Their V10 DPAPI key continues to work unchanged.
|
||||
**Non-ABE Chromium forks** (Opera, Vivaldi, Yandex, 360, QQ, Sogou) omit `WindowsABE` in `platformBrowsers()` (default false). The caller leaves `Hints.WindowsABEKey` empty, and `ABERetriever` returns `(nil, nil)` for empty `WindowsABEKey`, which `NewMasterKeys` treats silently as "not applicable" — so attempting ABE on these forks is a no-op, not a failure. Their V10 DPAPI key continues to work unchanged.
|
||||
|
||||
## 5. Linux Key Retrieval
|
||||
|
||||
@@ -157,11 +164,11 @@ Both strategies produce a password, derived via PBKDF2 with notably weaker param
|
||||
|
||||
A single iteration makes PBKDF2 essentially a keyed HMAC — no real key-stretching. Combined with the well-known fallback password `"peanuts"`, Linux Chromium encryption is trivial to break without the keyring.
|
||||
|
||||
### 5.4 Storage Labels
|
||||
### 5.4 Keychain Labels
|
||||
|
||||
Linux D-Bus labels follow a `"<name> Safe Storage"` convention, but many browsers alias onto a small shared set rather than defining their own. The three distinct labels are `"Chrome Safe Storage"`, `"Chromium Safe Storage"`, and `"Brave Safe Storage"` — everything else maps onto one of these.
|
||||
|
||||
The authoritative mapping lives in the `Storage` field of each entry in `platformBrowsers()` (`browser/browser_linux.go`).
|
||||
The authoritative mapping lives in the `KeychainLabel` field of each entry in `platformBrowsers()` (`browser/browser_linux.go`).
|
||||
|
||||
## 6. Platform Summary
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ Three steps.
|
||||
2. **Mine IIDs from TypeLib** — the interface IIDs live in the TypeLib resource of `<InstallDir>\Application\<version>\elevation_service.exe`. PowerShell + `ITypeLib.GetTypeInfo` enumerates them. Map `IElevator<Vendor>` → v1 IID, `IElevator2<Vendor>` → v2 IID (absent for older vendors).
|
||||
3. **Determine vtable slot** — count `IElevator` methods in the TypeLib. Chrome-family has 3 methods (slot 5). Edge prepends 3 placeholders (slot 8). Avast extends the interface further (slot 13).
|
||||
|
||||
Edit `crypto/windows/abe_native/com_iid.c` (add the entry), `utils/winutil/browser_meta_windows.go` (add a matching `winutil.Entry` with the right `ABEKind` and install-path fallbacks), `browser/browser_windows.go` (set `Storage: "<key>"` for the new `BrowserConfig`), then `make payload-clean && make build-windows` and redeploy.
|
||||
Edit `crypto/windows/abe_native/com_iid.c` (add the entry), `utils/winutil/browser_meta_windows.go` (add a matching `winutil.Entry` with the right `ABEKind` and install-path fallbacks; `Entry.Key` must equal the `BrowserConfig.Key` of the matching ABE-enabled config), `browser/browser_windows.go` (set `WindowsABE: true` on the new `BrowserConfig` so its `Key` doubles as the ABE / winutil.Table identifier), then `make payload-clean && make build-windows` and redeploy.
|
||||
|
||||
## 12. Known issues & future work
|
||||
|
||||
|
||||
+6
-5
@@ -83,11 +83,12 @@ const (
|
||||
|
||||
// BrowserConfig holds the declarative configuration for a browser installation.
|
||||
type BrowserConfig struct {
|
||||
Key string // lookup key: "chrome", "edge", "firefox"
|
||||
Name string // display name: "Chrome", "Edge", "Firefox"
|
||||
Kind BrowserKind // engine type
|
||||
Storage string // macOS/Linux: keychain/GNOME label. Windows: ABE browser key (triggers reflective injection when populated).
|
||||
UserDataDir string // base browser directory
|
||||
Key string // lookup key; doubles as the Windows ABE / winutil.Table key when WindowsABE is true
|
||||
Name string // display name
|
||||
Kind BrowserKind // engine type
|
||||
KeychainLabel string // macOS Keychain account / Linux D-Bus Secret Service label; "" = none
|
||||
WindowsABE bool // enable Windows App-Bound Encryption v20 (reflective injection)
|
||||
UserDataDir string // base browser directory
|
||||
}
|
||||
|
||||
// BrowserData holds all extracted browser data with typed slices.
|
||||
|
||||
@@ -35,9 +35,9 @@ const (
|
||||
|
||||
// Entry is the per-browser Windows metadata record.
|
||||
//
|
||||
// Key must match browser.BrowserConfig.Storage so retrievers and path
|
||||
// resolvers share a single lookup identifier. CLSID/IID bytes are *not*
|
||||
// stored here; see the package doc for why.
|
||||
// Key must match browser.BrowserConfig.Key for every config that sets
|
||||
// WindowsABE: true, so retrievers and path resolvers share a single lookup
|
||||
// identifier. CLSID/IID bytes are *not* stored here; see the package doc for why.
|
||||
type Entry struct {
|
||||
Key string
|
||||
ExeName string
|
||||
|
||||
@@ -29,7 +29,7 @@ var ErrExecutableNotFound = errors.New("browser executable not found")
|
||||
// is not running and the registry is missing the entry).
|
||||
//
|
||||
// browserKey must match an Entry in Table; keys align with
|
||||
// browser.BrowserConfig.Storage.
|
||||
// browser.BrowserConfig.Key (for configs that set WindowsABE: true).
|
||||
func ExecutablePath(browserKey string) (string, error) {
|
||||
entry, ok := Table[browserKey]
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user