mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
fix: per-tier master-key retrievers for mixed-cipher profiles (#579)
* fix: per-tier master-key retrievers for mixed-cipher profiles
This commit is contained in:
+5
-5
@@ -111,11 +111,11 @@ func pickFromConfigs(configs []types.BrowserConfig, opts PickOptions) ([]Browser
|
||||
return browsers, nil
|
||||
}
|
||||
|
||||
// retrieverSetter is an optional capability interface. Chromium variants
|
||||
// implement it to receive a master-key retriever chain; Firefox and Safari
|
||||
// do not.
|
||||
type retrieverSetter interface {
|
||||
SetRetriever(keyretriever.KeyRetriever)
|
||||
// keyRetrieversSetter is an optional capability interface. Chromium variants implement it to
|
||||
// receive the per-tier master-key retrievers (V10 / V11 / V20) as a single Retrievers struct;
|
||||
// Firefox and Safari do not.
|
||||
type keyRetrieversSetter interface {
|
||||
SetKeyRetrievers(keyretriever.Retrievers)
|
||||
}
|
||||
|
||||
// resolveGlobs expands glob patterns in browser configs' UserDataDir.
|
||||
|
||||
@@ -171,23 +171,23 @@ type keychainPasswordSetter interface {
|
||||
// no longer triggers a password prompt.
|
||||
func newPlatformInjector(opts PickOptions) func(Browser) {
|
||||
var (
|
||||
password string
|
||||
retriever keyretriever.KeyRetriever
|
||||
resolved bool
|
||||
password string
|
||||
retrievers keyretriever.Retrievers
|
||||
resolved bool
|
||||
)
|
||||
return func(b Browser) {
|
||||
rs, needsRetriever := b.(retrieverSetter)
|
||||
rs, needsRetrievers := b.(keyRetrieversSetter)
|
||||
kps, needsKeychainPassword := b.(keychainPasswordSetter)
|
||||
if !needsRetriever && !needsKeychainPassword {
|
||||
if !needsRetrievers && !needsKeychainPassword {
|
||||
return
|
||||
}
|
||||
if !resolved {
|
||||
password = resolveKeychainPassword(opts.KeychainPassword)
|
||||
retriever = keyretriever.DefaultRetriever(password)
|
||||
retrievers = keyretriever.DefaultRetrievers(password)
|
||||
resolved = true
|
||||
}
|
||||
if needsRetriever {
|
||||
rs.SetRetriever(retriever)
|
||||
if needsRetrievers {
|
||||
rs.SetKeyRetrievers(retrievers)
|
||||
}
|
||||
if needsKeychainPassword {
|
||||
kps.SetKeychainPassword(password)
|
||||
|
||||
@@ -67,13 +67,16 @@ func platformBrowsers() []types.BrowserConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// newPlatformInjector returns a closure that injects the Chromium master-key
|
||||
// retriever chain into each Browser.
|
||||
// newPlatformInjector returns a closure that wires the Linux Chromium master-key retrievers into
|
||||
// each Browser. Linux has two tiers: V10 uses the "peanuts" hardcoded password (kV10Key); V11
|
||||
// uses the D-Bus Secret Service keyring (kV11Key). V20 is nil — App-Bound Encryption is Windows-
|
||||
// only. Both V10 and V11 run independently so a profile carrying mixed cipher prefixes decrypts
|
||||
// both tiers.
|
||||
func newPlatformInjector(_ PickOptions) func(Browser) {
|
||||
retriever := keyretriever.DefaultRetriever()
|
||||
retrievers := keyretriever.DefaultRetrievers()
|
||||
return func(b Browser) {
|
||||
if s, ok := b.(retrieverSetter); ok {
|
||||
s.SetRetriever(retriever)
|
||||
if s, ok := b.(keyRetrieversSetter); ok {
|
||||
s.SetKeyRetrievers(retrievers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,13 +125,15 @@ func platformBrowsers() []types.BrowserConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// newPlatformInjector returns a closure that injects the Chromium master-key
|
||||
// retriever chain into each Browser.
|
||||
// newPlatformInjector returns a closure that wires the Windows v10 (DPAPI) and v20 (ABE) Chromium
|
||||
// master-key retrievers into each Browser. Per issue #578 the two tiers are orthogonal — a single
|
||||
// Chrome profile upgraded from pre-127 carries v20 cookies alongside v10 passwords — so both
|
||||
// retrievers run independently rather than as a first-success chain.
|
||||
func newPlatformInjector(_ PickOptions) func(Browser) {
|
||||
retriever := keyretriever.DefaultRetriever()
|
||||
retrievers := keyretriever.DefaultRetrievers()
|
||||
return func(b Browser) {
|
||||
if s, ok := b.(retrieverSetter); ok {
|
||||
s.SetRetriever(retriever)
|
||||
if s, ok := b.(keyRetrieversSetter); ok {
|
||||
s.SetKeyRetrievers(retrievers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -17,14 +16,14 @@ import (
|
||||
type Browser struct {
|
||||
cfg types.BrowserConfig
|
||||
profileDir string // absolute path to profile directory
|
||||
retriever keyretriever.KeyRetriever // set via SetRetriever after construction
|
||||
retrievers keyretriever.Retrievers // per-tier key sources (V10 / V11 / V20; unused tiers nil)
|
||||
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. Call SetRetriever on each returned browser before
|
||||
// one Browser per profile. Call SetKeyRetrievers 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)
|
||||
@@ -52,11 +51,19 @@ 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
|
||||
// SetKeyRetrievers wires the per-tier master-key retrievers used by Extract. Each slot
|
||||
// (V10 / V11 / V20) is populated only on platforms where that cipher tier is used:
|
||||
//
|
||||
// - Windows: V10 (DPAPI) + V20 (ABE). V11 nil — Chromium does not emit v11 prefix on Windows.
|
||||
// - Linux: V10 ("peanuts" kV10Key) + V11 (D-Bus Secret Service kV11Key). V20 nil.
|
||||
// - macOS: V10 (Keychain chain). V11 and V20 nil.
|
||||
//
|
||||
// Slots are independent — a failure or absence in one tier does not affect others. A single
|
||||
// Chromium profile can carry mixed cipher-prefix ciphertexts (the motivation for issue #578), so
|
||||
// every configured retriever runs at extract time and decryptValue picks the matching key per
|
||||
// ciphertext.
|
||||
func (b *Browser) SetKeyRetrievers(r keyretriever.Retrievers) {
|
||||
b.retrievers = r
|
||||
}
|
||||
|
||||
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||
@@ -79,10 +86,7 @@ func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, erro
|
||||
|
||||
tempPaths := b.acquireFiles(session, categories)
|
||||
|
||||
masterKey, err := b.getMasterKey(session)
|
||||
if err != nil {
|
||||
log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
keys := b.getMasterKeys(session)
|
||||
|
||||
data := &types.BrowserData{}
|
||||
for _, cat := range categories {
|
||||
@@ -90,7 +94,7 @@ func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, erro
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
b.extractCategory(data, cat, masterKey, path)
|
||||
b.extractCategory(data, cat, keys, path)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -170,43 +174,46 @@ func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.
|
||||
return tempPaths
|
||||
}
|
||||
|
||||
// getMasterKey retrieves the Chromium master encryption key.
|
||||
//
|
||||
// On Windows, the key is read from the Local State file and decrypted via DPAPI.
|
||||
// On macOS, the key is derived from Keychain (Local State is not needed).
|
||||
// On Linux, the key is derived from D-Bus Secret Service or a fallback password.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
// getMasterKeys retrieves the Chromium master keys for every configured tier. Chrome mixes
|
||||
// cipher tiers on the same profile — v20 for new cookies alongside v10 passwords on Windows; v10
|
||||
// (peanuts) alongside v11 (keyring) on Linux after session-mode changes — so every retriever in
|
||||
// b.retrievers runs independently and keyretriever.NewMasterKeys assembles the results. Any tier
|
||||
// key may be nil if its retriever failed or is not configured for this platform; decryptValue
|
||||
// treats a missing tier key as "that tier cannot decrypt" so partial success is still reported.
|
||||
func (b *Browser) getMasterKeys(session *filemanager.Session) keyretriever.MasterKeys {
|
||||
label := b.BrowserName() + "/" + b.ProfileName()
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
var localStateDst string
|
||||
for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} {
|
||||
candidate := filepath.Join(dir, "Local State")
|
||||
if fileutil.FileExists(candidate) {
|
||||
localStateDst = filepath.Join(session.TempDir(), "Local State")
|
||||
if err := session.Acquire(candidate, localStateDst, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fileutil.FileExists(candidate) {
|
||||
continue
|
||||
}
|
||||
dst := filepath.Join(session.TempDir(), "Local State")
|
||||
if err := session.Acquire(candidate, dst, false); err != nil {
|
||||
log.Debugf("acquire Local State for %s: %v", label, err)
|
||||
break
|
||||
}
|
||||
localStateDst = dst
|
||||
break
|
||||
}
|
||||
|
||||
return b.retriever.RetrieveKey(b.cfg.Storage, localStateDst)
|
||||
keys, err := keyretriever.NewMasterKeys(b.retrievers, b.cfg.Storage, localStateDst)
|
||||
if err != nil {
|
||||
log.Warnf("%s: master key retrieval: %v", label, err)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// extractCategory calls the appropriate extract function for a category.
|
||||
// If a custom extractor is registered for this category (via extractorsForKind),
|
||||
// it is used instead of the default switch logic.
|
||||
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) {
|
||||
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, keys keyretriever.MasterKeys, path string) {
|
||||
if ext, ok := b.extractors[cat]; ok {
|
||||
if err := ext.extract(masterKey, path, data); err != nil {
|
||||
if err := ext.extract(keys, path, data); err != nil {
|
||||
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
return
|
||||
@@ -215,9 +222,9 @@ func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, m
|
||||
var err error
|
||||
switch cat {
|
||||
case types.Password:
|
||||
data.Passwords, err = extractPasswords(masterKey, path)
|
||||
data.Passwords, err = extractPasswords(keys, path)
|
||||
case types.Cookie:
|
||||
data.Cookies, err = extractCookies(masterKey, path)
|
||||
data.Cookies, err = extractCookies(keys, path)
|
||||
case types.History:
|
||||
data.Histories, err = extractHistories(path)
|
||||
case types.Download:
|
||||
@@ -225,7 +232,7 @@ func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, m
|
||||
case types.Bookmark:
|
||||
data.Bookmarks, err = extractBookmarks(path)
|
||||
case types.CreditCard:
|
||||
data.CreditCards, err = extractCreditCards(masterKey, path)
|
||||
data.CreditCards, err = extractCreditCards(keys, path)
|
||||
case types.Extension:
|
||||
data.Extensions, err = extractExtensions(path)
|
||||
case types.LocalStorage:
|
||||
|
||||
@@ -362,7 +362,7 @@ func TestExtractCategory_CustomExtractor(t *testing.T) {
|
||||
}
|
||||
|
||||
data := &types.BrowserData{}
|
||||
b.extractCategory(data, types.Extension, nil, "unused-path")
|
||||
b.extractCategory(data, types.Extension, keyretriever.MasterKeys{}, "unused-path")
|
||||
|
||||
assert.True(t, called, "custom extractor should be called")
|
||||
require.Len(t, data.Extensions, 1)
|
||||
@@ -381,7 +381,7 @@ func TestExtractCategory_DefaultFallback(t *testing.T) {
|
||||
}
|
||||
|
||||
data := &types.BrowserData{}
|
||||
b.extractCategory(data, types.History, nil, path)
|
||||
b.extractCategory(data, types.History, keyretriever.MasterKeys{}, path)
|
||||
|
||||
require.Len(t, data.Histories, 1)
|
||||
assert.Equal(t, "Example", data.Histories[0].Title)
|
||||
@@ -441,7 +441,7 @@ func TestLocalStatePath(t *testing.T) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getMasterKey
|
||||
// getMasterKeys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// mockRetriever records the arguments passed to RetrieveKey.
|
||||
@@ -460,7 +460,10 @@ func (m *mockRetriever) RetrieveKey(storage, localStatePath string) ([]byte, err
|
||||
return m.key, m.err
|
||||
}
|
||||
|
||||
func TestGetMasterKey(t *testing.T) {
|
||||
func TestGetMasterKeys(t *testing.T) {
|
||||
// getMasterKeys routes through keyretriever.NewMasterKeys on every platform — the V10 mock
|
||||
// wired via SetKeyRetrievers(Retrievers{V10: mock}) is consulted cross-platform.
|
||||
|
||||
// Profile directory without Local State file.
|
||||
dirNoLocalState := t.TempDir()
|
||||
mkFile(dirNoLocalState, "Default", "Preferences")
|
||||
@@ -470,23 +473,21 @@ func TestGetMasterKey(t *testing.T) {
|
||||
name string
|
||||
dir string
|
||||
storage string
|
||||
retriever keyretriever.KeyRetriever // nil → don't call SetRetriever
|
||||
wantKey []byte
|
||||
wantErr string
|
||||
retriever keyretriever.KeyRetriever // nil → don't call SetKeyRetrievers
|
||||
wantV10 []byte
|
||||
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: "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")},
|
||||
wantKey: []byte("fake-master-key"),
|
||||
wantV10: []byte("fake-master-key"),
|
||||
wantStorage: "Chrome",
|
||||
wantLocalState: true,
|
||||
},
|
||||
@@ -495,7 +496,7 @@ func TestGetMasterKey(t *testing.T) {
|
||||
dir: dirNoLocalState,
|
||||
storage: "Chromium",
|
||||
retriever: &mockRetriever{key: []byte("derived-key")},
|
||||
wantKey: []byte("derived-key"),
|
||||
wantV10: []byte("derived-key"),
|
||||
wantStorage: "Chromium",
|
||||
},
|
||||
}
|
||||
@@ -510,22 +511,21 @@ func TestGetMasterKey(t *testing.T) {
|
||||
|
||||
b := browsers[0]
|
||||
if tt.retriever != nil {
|
||||
b.SetRetriever(tt.retriever)
|
||||
b.SetKeyRetrievers(keyretriever.Retrievers{V10: 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)
|
||||
keys := b.getMasterKeys(session)
|
||||
assert.Equal(t, tt.wantV10, keys.V10)
|
||||
assert.Nil(t, keys.V11, "V11 stays nil when no v11 retriever is wired")
|
||||
assert.Nil(t, keys.V20, "V20 stays nil when no v20 retriever is wired")
|
||||
|
||||
if tt.retriever == nil {
|
||||
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)
|
||||
@@ -539,6 +539,43 @@ func TestGetMasterKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetMasterKeys_AllTiersInvoked is the mixed-tier regression test at the getMasterKeys layer.
|
||||
// Before the refactor a Windows-only bypass meant only one tier's retriever was consulted, so a
|
||||
// profile mixing prefixes silently lost the un-retrieved tier. After the refactor every
|
||||
// configured tier must be called exactly once and its key must land in the matching MasterKeys
|
||||
// slot. This catches any future "bypass keyretriever for a faster path" regression and covers the
|
||||
// analogous Linux v10/v11 case — no platform silently drops a tier any more.
|
||||
func TestGetMasterKeys_AllTiersInvoked(t *testing.T) {
|
||||
v10mock := &mockRetriever{key: []byte("fake-v10-key")}
|
||||
v11mock := &mockRetriever{key: []byte("fake-v11-key")}
|
||||
v20mock := &mockRetriever{key: []byte("fake-v20-key")}
|
||||
|
||||
browsers, err := NewBrowsers(types.BrowserConfig{
|
||||
Name: "Test", Kind: types.Chromium, UserDataDir: fixture.chrome, Storage: "Chrome",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, browsers)
|
||||
|
||||
b := browsers[0]
|
||||
b.SetKeyRetrievers(keyretriever.Retrievers{V10: v10mock, V11: v11mock, V20: v20mock})
|
||||
|
||||
session, err := filemanager.NewSession()
|
||||
require.NoError(t, err)
|
||||
defer session.Cleanup()
|
||||
|
||||
keys := b.getMasterKeys(session)
|
||||
assert.Equal(t, []byte("fake-v10-key"), keys.V10, "V10 slot must be populated")
|
||||
assert.Equal(t, []byte("fake-v11-key"), keys.V11, "V11 slot must be populated")
|
||||
assert.Equal(t, []byte("fake-v20-key"), keys.V20, "V20 slot must be populated")
|
||||
assert.True(t, v10mock.called, "V10 retriever must be called — no silent bypass")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extract
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -572,7 +609,7 @@ func TestExtract(t *testing.T) {
|
||||
require.Len(t, browsers, 1)
|
||||
|
||||
if tt.retriever != nil {
|
||||
browsers[0].SetRetriever(tt.retriever)
|
||||
browsers[0].SetKeyRetrievers(keyretriever.Retrievers{V10: tt.retriever})
|
||||
}
|
||||
|
||||
result, err := browsers[0].Extract([]types.Category{types.History})
|
||||
@@ -673,12 +710,12 @@ func TestCountCategory(t *testing.T) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SetRetriever: verify *Browser satisfies the interface used by
|
||||
// SetKeyRetrievers: verify *Browser satisfies the interface used by
|
||||
// browser.pickFromConfigs for post-construction retriever injection.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSetRetriever_SatisfiesInterface(t *testing.T) {
|
||||
func TestSetKeyRetrievers_SatisfiesInterface(t *testing.T) {
|
||||
var _ interface {
|
||||
SetRetriever(keyretriever.KeyRetriever)
|
||||
SetKeyRetrievers(keyretriever.Retrievers)
|
||||
} = (*Browser)(nil)
|
||||
}
|
||||
|
||||
@@ -4,24 +4,43 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
)
|
||||
|
||||
// decryptValue decrypts a Chromium-encrypted value using the master key. It detects the cipher version
|
||||
// from the ciphertext prefix and routes to the appropriate decryption function.
|
||||
func decryptValue(masterKey, ciphertext []byte) ([]byte, error) {
|
||||
// decryptValue decrypts a Chromium-encrypted value by dispatching on the ciphertext's version
|
||||
// prefix to the matching tier in keys:
|
||||
//
|
||||
// - v10 → keys.V10 (Windows DPAPI / macOS Keychain / Linux peanuts kV10Key)
|
||||
// - v11 → keys.V11 (Linux keyring kV11Key; nil on Windows/macOS — Chromium doesn't emit v11 there)
|
||||
// - v20 → keys.V20 (Windows ABE; nil on non-Windows — Chromium doesn't emit v20 there)
|
||||
//
|
||||
// A single profile can carry mixed prefixes (Chrome 127+ upgrades on Windows; Linux session-mode
|
||||
// changes), so every applicable key must be populated upstream for lossless extraction. Missing
|
||||
// tier keys surface as decrypt errors at the ciphertext level; the extract layer treats those as
|
||||
// empty plaintexts rather than fatal errors.
|
||||
func decryptValue(keys keyretriever.MasterKeys, ciphertext []byte) ([]byte, error) {
|
||||
if len(ciphertext) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
version := crypto.DetectVersion(ciphertext)
|
||||
switch version {
|
||||
case crypto.CipherV10, crypto.CipherV11:
|
||||
// v11 is Linux-only and shares v10's AES-CBC path; only the key source differs.
|
||||
return crypto.DecryptChromium(masterKey, ciphertext)
|
||||
case crypto.CipherV10:
|
||||
return crypto.DecryptChromium(keys.V10, ciphertext)
|
||||
case crypto.CipherV11:
|
||||
// v11 is Linux-only and shares v10's AES-CBC path, but uses the keyring-derived kV11Key
|
||||
// rather than the peanuts-derived kV10Key — so a Linux profile with both prefixes needs
|
||||
// distinct per-tier keys to decrypt everything.
|
||||
return crypto.DecryptChromium(keys.V11, ciphertext)
|
||||
case crypto.CipherV20:
|
||||
// v20 is cross-platform AES-GCM; routed through a dedicated function so Linux/macOS CI can
|
||||
// exercise the same decryption path as Windows.
|
||||
return crypto.DecryptChromiumV20(masterKey, ciphertext)
|
||||
return crypto.DecryptChromiumV20(keys.V20, ciphertext)
|
||||
case crypto.CipherV12:
|
||||
// Chromium's SecretPortalKeyProvider (Flatpak / xdg-desktop-portal) — HKDF-SHA256 +
|
||||
// AES-256-GCM with a secret retrieved via org.freedesktop.portal.Desktop. Recognized here
|
||||
// to surface an actionable "known gap" error rather than the generic "unsupported" one.
|
||||
return nil, fmt.Errorf("unsupported cipher version v12 (Chromium SecretPortal / Flatpak; not yet implemented)")
|
||||
case crypto.CipherDPAPI:
|
||||
return crypto.DecryptDPAPI(ciphertext)
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
)
|
||||
|
||||
// TestDecryptValue_MixedTier is the regression test for mixed-cipher profiles (issue #578 on
|
||||
// Windows; the analogous Linux v10/v11 gap). A single MasterKeys struct must carry distinct keys
|
||||
// for each tier, and decryptValue must dispatch each ciphertext to the matching tier's key.
|
||||
// Before the refactor the master-key retriever returned only one tier, so a profile mixing
|
||||
// cipher prefixes silently lost whichever tier wasn't retrieved.
|
||||
//
|
||||
// Uses v20 (cross-platform AES-GCM) to cover the prefix→slot routing property without depending
|
||||
// on platform-specific v10/v11 cipher primitives (AES-CBC on darwin/linux, AES-GCM on Windows).
|
||||
// The per-platform v10/v11 formats are covered by decrypt_test.go and decrypt_windows_test.go.
|
||||
func TestDecryptValue_MixedTier(t *testing.T) {
|
||||
k10 := bytes.Repeat([]byte{0x10}, 16) // V10 slot key (wrong for v20 payload)
|
||||
k11 := bytes.Repeat([]byte{0x11}, 16) // V11 slot key (wrong for v20 payload)
|
||||
k20 := bytes.Repeat([]byte{0x20}, 16) // V20 slot key (correct for v20 payload)
|
||||
|
||||
plaintext := []byte("cookie-value-encrypted-with-k20")
|
||||
nonce := []byte("v20_nonce_12") // 12-byte AES-GCM nonce
|
||||
|
||||
gcmEnc, err := crypto.AESGCMEncrypt(k20, nonce, plaintext)
|
||||
require.NoError(t, err)
|
||||
v20Ciphertext := append([]byte("v20"), append(nonce, gcmEnc...)...)
|
||||
|
||||
t.Run("all tiers populated: v20 picks V20, decrypts", func(t *testing.T) {
|
||||
got, err := decryptValue(keyretriever.MasterKeys{V10: k10, V11: k11, V20: k20}, v20Ciphertext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, plaintext, got)
|
||||
})
|
||||
|
||||
t.Run("V20 holds wrong key: v20 still picks V20 slot (not V10/V11), errors", func(t *testing.T) {
|
||||
// If the dispatcher incorrectly fell back to V10 or V11 when V20 had a wrong key, this
|
||||
// would succeed. Proves the router uses prefix-based selection, not first-usable-key.
|
||||
_, err := decryptValue(keyretriever.MasterKeys{V10: k20, V11: k20, V20: k10}, v20Ciphertext)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("only V20 populated: v20 still decrypts", func(t *testing.T) {
|
||||
// The pre-#578 symmetric regression: when DPAPI/keyring failed and only V20 was retrieved,
|
||||
// v20 cookies had to still decrypt. This asserts V10 and V11 being nil doesn't block v20.
|
||||
got, err := decryptValue(keyretriever.MasterKeys{V20: k20}, v20Ciphertext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, plaintext, got)
|
||||
})
|
||||
|
||||
t.Run("V20 slot unpopulated: v20 errors (no key to use)", func(t *testing.T) {
|
||||
_, err := decryptValue(keyretriever.MasterKeys{V10: k10, V11: k11}, v20Ciphertext)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
)
|
||||
|
||||
func TestDecryptValue_V10(t *testing.T) {
|
||||
@@ -39,7 +40,7 @@ func TestDecryptValue_V10(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := decryptValue(tt.key, v10Ciphertext)
|
||||
got, err := decryptValue(keyretriever.MasterKeys{V10: tt.key}, v10Ciphertext)
|
||||
if tt.wantErrMsg != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErrMsg)
|
||||
@@ -59,7 +60,52 @@ func TestDecryptValue_V11(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
v11Ciphertext := append([]byte("v11"), cbcEncrypted...)
|
||||
|
||||
got, err := decryptValue(testAESKey, v11Ciphertext)
|
||||
// v11 ciphertexts route to the V11 slot (Linux's keyring-derived kV11Key) — not V10 (peanuts).
|
||||
got, err := decryptValue(keyretriever.MasterKeys{V11: testAESKey}, v11Ciphertext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, plaintext, got)
|
||||
}
|
||||
|
||||
// TestDecryptValue_V10_V11_SlotSeparation is the Linux analog of the #578 regression test: a
|
||||
// profile carrying both v10 (peanuts) and v11 (keyring) ciphertexts must route each prefix to
|
||||
// its own slot, not share a single key. Build-tag scoped to darwin/linux because v10/v11 use
|
||||
// AES-CBC on these platforms; Windows uses AES-GCM for v10 and is covered separately by
|
||||
// decrypt_windows_test.go.
|
||||
func TestDecryptValue_V10_V11_SlotSeparation(t *testing.T) {
|
||||
k10 := bytes.Repeat([]byte{0x10}, 16) // V10 slot key (peanuts-derived kV10Key)
|
||||
k11 := bytes.Repeat([]byte{0x11}, 16) // V11 slot key (keyring-derived kV11Key)
|
||||
|
||||
iv := bytes.Repeat([]byte{0x20}, 16) // matches crypto.chromiumCBCIV on darwin/linux
|
||||
v10plain := []byte("password-from-v10-era")
|
||||
v11plain := []byte("password-from-v11-era")
|
||||
|
||||
v10Enc, err := crypto.AESCBCEncrypt(k10, iv, v10plain)
|
||||
require.NoError(t, err)
|
||||
v10Ciphertext := append([]byte("v10"), v10Enc...)
|
||||
|
||||
v11Enc, err := crypto.AESCBCEncrypt(k11, iv, v11plain)
|
||||
require.NoError(t, err)
|
||||
v11Ciphertext := append([]byte("v11"), v11Enc...)
|
||||
|
||||
keys := keyretriever.MasterKeys{V10: k10, V11: k11}
|
||||
|
||||
t.Run("v10 ciphertext decrypts via V10 slot", func(t *testing.T) {
|
||||
got, err := decryptValue(keys, v10Ciphertext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, v10plain, got)
|
||||
})
|
||||
|
||||
t.Run("v11 ciphertext decrypts via V11 slot", func(t *testing.T) {
|
||||
got, err := decryptValue(keys, v11Ciphertext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, v11plain, got)
|
||||
})
|
||||
|
||||
t.Run("swapped keys fail both directions", func(t *testing.T) {
|
||||
swapped := keyretriever.MasterKeys{V10: k11, V11: k10}
|
||||
_, err := decryptValue(swapped, v10Ciphertext)
|
||||
require.Error(t, err, "v10 with V11's key must fail")
|
||||
_, err = decryptValue(swapped, v11Ciphertext)
|
||||
require.Error(t, err, "v11 with V10's key must fail")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
)
|
||||
|
||||
// TestDecryptValue_V20 is cross-platform because v20's ciphertext format
|
||||
@@ -23,13 +24,13 @@ func TestDecryptValue_V20(t *testing.T) {
|
||||
// v20 layout: "v20" (3B) + nonce (12B) + ciphertext+tag
|
||||
ciphertext := append([]byte("v20"), append(nonce, gcm...)...)
|
||||
|
||||
got, err := decryptValue(testAESKey, ciphertext)
|
||||
got, err := decryptValue(keyretriever.MasterKeys{V20: testAESKey}, ciphertext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, plaintext, got)
|
||||
}
|
||||
|
||||
func TestDecryptValue_V20_ShortCiphertext(t *testing.T) {
|
||||
// Missing nonce (prefix only) must error, not panic.
|
||||
_, err := decryptValue(testAESKey, []byte("v20"))
|
||||
_, err := decryptValue(keyretriever.MasterKeys{V20: testAESKey}, []byte("v20"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
)
|
||||
|
||||
// encryptWithDPAPI encrypts data using Windows DPAPI (CryptProtectData).
|
||||
@@ -63,7 +64,7 @@ func TestDecryptValue_V10_Windows(t *testing.T) {
|
||||
// v10 format on Windows: "v10" + nonce(12) + encrypted
|
||||
ciphertext := append([]byte("v10"), append(nonce, gcmEncrypted...)...)
|
||||
|
||||
got, err := decryptValue(testAESKey, ciphertext)
|
||||
got, err := decryptValue(keyretriever.MasterKeys{V10: testAESKey}, ciphertext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, plaintext, got)
|
||||
}
|
||||
@@ -76,8 +77,8 @@ func TestDecryptValue_DPAPI_Windows(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, encrypted)
|
||||
|
||||
// No v10/v20 prefix → decryptValue routes to DPAPI path
|
||||
got, err := decryptValue(nil, encrypted)
|
||||
// No v10/v20 prefix → decryptValue routes to DPAPI path; no per-tier key needed.
|
||||
got, err := decryptValue(keyretriever.MasterKeys{}, encrypted)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, plaintext, got)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||
)
|
||||
@@ -18,9 +18,7 @@ const (
|
||||
countCookieQuery = `SELECT COUNT(*) FROM cookies`
|
||||
)
|
||||
|
||||
func extractCookies(masterKey []byte, path string) ([]types.CookieEntry, error) {
|
||||
var decryptFails int
|
||||
var lastErr error
|
||||
func extractCookies(keys keyretriever.MasterKeys, path string) ([]types.CookieEntry, error) {
|
||||
cookies, err := sqliteutil.QueryRows(path, false, defaultCookieQuery,
|
||||
func(rows *sql.Rows) (types.CookieEntry, error) {
|
||||
var (
|
||||
@@ -36,11 +34,7 @@ func extractCookies(masterKey []byte, path string) ([]types.CookieEntry, error)
|
||||
return types.CookieEntry{}, err
|
||||
}
|
||||
|
||||
value, err := decryptValue(masterKey, encryptedValue)
|
||||
if err != nil {
|
||||
decryptFails++
|
||||
lastErr = err
|
||||
}
|
||||
value, _ := decryptValue(keys, encryptedValue)
|
||||
value = stripCookieHash(value, host)
|
||||
return types.CookieEntry{
|
||||
Name: name,
|
||||
@@ -58,9 +52,6 @@ func extractCookies(masterKey []byte, path string) ([]types.CookieEntry, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if decryptFails > 0 {
|
||||
log.Warnf("cookies: total=%d decrypt_failed=%d last_err=%v", len(cookies), decryptFails, lastErr)
|
||||
}
|
||||
|
||||
sort.Slice(cookies, func(i, j int) bool {
|
||||
return cookies[i].CreatedAt.After(cookies[j].CreatedAt)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
)
|
||||
|
||||
func setupCookieDB(t *testing.T) string {
|
||||
@@ -19,7 +21,7 @@ func setupCookieDB(t *testing.T) string {
|
||||
func TestExtractCookies(t *testing.T) {
|
||||
path := setupCookieDB(t)
|
||||
|
||||
got, err := extractCookies(nil, path)
|
||||
got, err := extractCookies(keyretriever.MasterKeys{}, path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package chromium
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||
)
|
||||
@@ -14,9 +14,7 @@ const (
|
||||
countCreditCardQuery = `SELECT COUNT(*) FROM credit_cards`
|
||||
)
|
||||
|
||||
func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry, error) {
|
||||
var decryptFails int
|
||||
var lastErr error
|
||||
func extractCreditCards(keys keyretriever.MasterKeys, path string) ([]types.CreditCardEntry, error) {
|
||||
cards, err := sqliteutil.QueryRows(path, false, defaultCreditCardQuery,
|
||||
func(rows *sql.Rows) (types.CreditCardEntry, error) {
|
||||
var guid, name, month, year, nickname, address string
|
||||
@@ -24,11 +22,7 @@ func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry,
|
||||
if err := rows.Scan(&guid, &name, &month, &year, &encNumber, &nickname, &address); err != nil {
|
||||
return types.CreditCardEntry{}, err
|
||||
}
|
||||
number, err := decryptValue(masterKey, encNumber)
|
||||
if err != nil {
|
||||
decryptFails++
|
||||
lastErr = err
|
||||
}
|
||||
number, _ := decryptValue(keys, encNumber)
|
||||
return types.CreditCardEntry{
|
||||
GUID: guid,
|
||||
Name: name,
|
||||
@@ -42,9 +36,6 @@ func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if decryptFails > 0 {
|
||||
log.Debugf("decrypt credit cards: %d failed: %v", decryptFails, lastErr)
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
)
|
||||
|
||||
func setupCreditCardDB(t *testing.T) string {
|
||||
@@ -18,7 +20,7 @@ func setupCreditCardDB(t *testing.T) string {
|
||||
func TestExtractCreditCards(t *testing.T) {
|
||||
path := setupCreditCardDB(t)
|
||||
|
||||
got, err := extractCreditCards(nil, path)
|
||||
got, err := extractCreditCards(keyretriever.MasterKeys{}, path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||
)
|
||||
@@ -14,13 +14,11 @@ const (
|
||||
countLoginQuery = `SELECT COUNT(*) FROM logins`
|
||||
)
|
||||
|
||||
func extractPasswords(masterKey []byte, path string) ([]types.LoginEntry, error) {
|
||||
return extractPasswordsWithQuery(masterKey, path, defaultLoginQuery)
|
||||
func extractPasswords(keys keyretriever.MasterKeys, path string) ([]types.LoginEntry, error) {
|
||||
return extractPasswordsWithQuery(keys, path, defaultLoginQuery)
|
||||
}
|
||||
|
||||
func extractPasswordsWithQuery(masterKey []byte, path, query string) ([]types.LoginEntry, error) {
|
||||
var decryptFails int
|
||||
var lastErr error
|
||||
func extractPasswordsWithQuery(keys keyretriever.MasterKeys, path, query string) ([]types.LoginEntry, error) {
|
||||
logins, err := sqliteutil.QueryRows(path, false, query,
|
||||
func(rows *sql.Rows) (types.LoginEntry, error) {
|
||||
var url, username string
|
||||
@@ -29,11 +27,7 @@ func extractPasswordsWithQuery(masterKey []byte, path, query string) ([]types.Lo
|
||||
if err := rows.Scan(&url, &username, &pwd, &created); err != nil {
|
||||
return types.LoginEntry{}, err
|
||||
}
|
||||
password, err := decryptValue(masterKey, pwd)
|
||||
if err != nil {
|
||||
decryptFails++
|
||||
lastErr = err
|
||||
}
|
||||
password, _ := decryptValue(keys, pwd)
|
||||
return types.LoginEntry{
|
||||
URL: url,
|
||||
Username: username,
|
||||
@@ -44,9 +38,6 @@ func extractPasswordsWithQuery(masterKey []byte, path, query string) ([]types.Lo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if decryptFails > 0 {
|
||||
log.Warnf("passwords: total=%d decrypt_failed=%d last_err=%v", len(logins), decryptFails, lastErr)
|
||||
}
|
||||
|
||||
sort.Slice(logins, func(i, j int) bool {
|
||||
return logins[i].CreatedAt.After(logins[j].CreatedAt)
|
||||
@@ -56,9 +47,9 @@ func extractPasswordsWithQuery(masterKey []byte, path, query string) ([]types.Lo
|
||||
|
||||
// extractYandexPasswords extracts passwords from Yandex's Ya Passman Data, which stores the URL in
|
||||
// action_url instead of origin_url.
|
||||
func extractYandexPasswords(masterKey []byte, path string) ([]types.LoginEntry, error) {
|
||||
func extractYandexPasswords(keys keyretriever.MasterKeys, path string) ([]types.LoginEntry, error) {
|
||||
const yandexLoginQuery = `SELECT action_url, username_value, password_value, date_created FROM logins`
|
||||
return extractPasswordsWithQuery(masterKey, path, yandexLoginQuery)
|
||||
return extractPasswordsWithQuery(keys, path, yandexLoginQuery)
|
||||
}
|
||||
|
||||
func countPasswords(path string) (int, error) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
)
|
||||
|
||||
func setupLoginDB(t *testing.T) string {
|
||||
@@ -18,7 +20,7 @@ func setupLoginDB(t *testing.T) string {
|
||||
func TestExtractPasswords(t *testing.T) {
|
||||
path := setupLoginDB(t)
|
||||
|
||||
got, err := extractPasswords(nil, path)
|
||||
got, err := extractPasswords(keyretriever.MasterKeys{}, path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
@@ -54,7 +56,7 @@ func TestExtractYandexPasswords(t *testing.T) {
|
||||
insertLogin("https://origin.yandex.ru", "https://action.yandex.ru/submit", "user", "", 13350000000000000),
|
||||
)
|
||||
|
||||
got, err := extractYandexPasswords(nil, path)
|
||||
got, err := extractYandexPasswords(keyretriever.MasterKeys{}, path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1)
|
||||
assert.Equal(t, "https://action.yandex.ru/submit", got[0].URL) // action_url, not origin_url
|
||||
|
||||
@@ -3,6 +3,7 @@ package chromium
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
@@ -68,17 +69,17 @@ func sourcesForKind(kind types.BrowserKind) map[types.Category][]sourcePath {
|
||||
// switch logic, enabling browser-specific parsing (e.g. Opera's opsettings
|
||||
// for extensions, Yandex's credit card table, QBCI-encrypted bookmarks).
|
||||
type categoryExtractor interface {
|
||||
extract(masterKey []byte, path string, data *types.BrowserData) error
|
||||
extract(keys keyretriever.MasterKeys, path string, data *types.BrowserData) error
|
||||
}
|
||||
|
||||
// passwordExtractor wraps a custom password extract function.
|
||||
type passwordExtractor struct {
|
||||
fn func(masterKey []byte, path string) ([]types.LoginEntry, error)
|
||||
fn func(keys keyretriever.MasterKeys, path string) ([]types.LoginEntry, error)
|
||||
}
|
||||
|
||||
func (e passwordExtractor) extract(masterKey []byte, path string, data *types.BrowserData) error {
|
||||
func (e passwordExtractor) extract(keys keyretriever.MasterKeys, path string, data *types.BrowserData) error {
|
||||
var err error
|
||||
data.Passwords, err = e.fn(masterKey, path)
|
||||
data.Passwords, err = e.fn(keys, path)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -87,7 +88,7 @@ type extensionExtractor struct {
|
||||
fn func(path string) ([]types.ExtensionEntry, error)
|
||||
}
|
||||
|
||||
func (e extensionExtractor) extract(_ []byte, path string, data *types.BrowserData) error {
|
||||
func (e extensionExtractor) extract(_ keyretriever.MasterKeys, path string, data *types.BrowserData) error {
|
||||
var err error
|
||||
data.Extensions, err = e.fn(path)
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user