Skip to content

Commit 7b9a973

Browse files
authored
fix: per-tier master-key retrievers for mixed-cipher profiles (#579)
* fix: per-tier master-key retrievers for mixed-cipher profiles
1 parent e50c623 commit 7b9a973

29 files changed

Lines changed: 700 additions & 228 deletions

browser/browser.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,11 @@ func pickFromConfigs(configs []types.BrowserConfig, opts PickOptions) ([]Browser
111111
return browsers, nil
112112
}
113113

114-
// retrieverSetter is an optional capability interface. Chromium variants
115-
// implement it to receive a master-key retriever chain; Firefox and Safari
116-
// do not.
117-
type retrieverSetter interface {
118-
SetRetriever(keyretriever.KeyRetriever)
114+
// keyRetrieversSetter is an optional capability interface. Chromium variants implement it to
115+
// receive the per-tier master-key retrievers (V10 / V11 / V20) as a single Retrievers struct;
116+
// Firefox and Safari do not.
117+
type keyRetrieversSetter interface {
118+
SetKeyRetrievers(keyretriever.Retrievers)
119119
}
120120

121121
// resolveGlobs expands glob patterns in browser configs' UserDataDir.

browser/browser_darwin.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,23 +171,23 @@ type keychainPasswordSetter interface {
171171
// no longer triggers a password prompt.
172172
func newPlatformInjector(opts PickOptions) func(Browser) {
173173
var (
174-
password string
175-
retriever keyretriever.KeyRetriever
176-
resolved bool
174+
password string
175+
retrievers keyretriever.Retrievers
176+
resolved bool
177177
)
178178
return func(b Browser) {
179-
rs, needsRetriever := b.(retrieverSetter)
179+
rs, needsRetrievers := b.(keyRetrieversSetter)
180180
kps, needsKeychainPassword := b.(keychainPasswordSetter)
181-
if !needsRetriever && !needsKeychainPassword {
181+
if !needsRetrievers && !needsKeychainPassword {
182182
return
183183
}
184184
if !resolved {
185185
password = resolveKeychainPassword(opts.KeychainPassword)
186-
retriever = keyretriever.DefaultRetriever(password)
186+
retrievers = keyretriever.DefaultRetrievers(password)
187187
resolved = true
188188
}
189-
if needsRetriever {
190-
rs.SetRetriever(retriever)
189+
if needsRetrievers {
190+
rs.SetKeyRetrievers(retrievers)
191191
}
192192
if needsKeychainPassword {
193193
kps.SetKeychainPassword(password)

browser/browser_linux.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,16 @@ func platformBrowsers() []types.BrowserConfig {
6767
}
6868
}
6969

70-
// newPlatformInjector returns a closure that injects the Chromium master-key
71-
// retriever chain into each Browser.
70+
// newPlatformInjector returns a closure that wires the Linux Chromium master-key retrievers into
71+
// each Browser. Linux has two tiers: V10 uses the "peanuts" hardcoded password (kV10Key); V11
72+
// uses the D-Bus Secret Service keyring (kV11Key). V20 is nil — App-Bound Encryption is Windows-
73+
// only. Both V10 and V11 run independently so a profile carrying mixed cipher prefixes decrypts
74+
// both tiers.
7275
func newPlatformInjector(_ PickOptions) func(Browser) {
73-
retriever := keyretriever.DefaultRetriever()
76+
retrievers := keyretriever.DefaultRetrievers()
7477
return func(b Browser) {
75-
if s, ok := b.(retrieverSetter); ok {
76-
s.SetRetriever(retriever)
78+
if s, ok := b.(keyRetrieversSetter); ok {
79+
s.SetKeyRetrievers(retrievers)
7780
}
7881
}
7982
}

browser/browser_windows.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ func platformBrowsers() []types.BrowserConfig {
125125
}
126126
}
127127

128-
// newPlatformInjector returns a closure that injects the Chromium master-key
129-
// retriever chain into each Browser.
128+
// newPlatformInjector returns a closure that wires the Windows v10 (DPAPI) and v20 (ABE) Chromium
129+
// master-key retrievers into each Browser. Per issue #578 the two tiers are orthogonal — a single
130+
// Chrome profile upgraded from pre-127 carries v20 cookies alongside v10 passwords — so both
131+
// retrievers run independently rather than as a first-success chain.
130132
func newPlatformInjector(_ PickOptions) func(Browser) {
131-
retriever := keyretriever.DefaultRetriever()
133+
retrievers := keyretriever.DefaultRetrievers()
132134
return func(b Browser) {
133-
if s, ok := b.(retrieverSetter); ok {
134-
s.SetRetriever(retriever)
135+
if s, ok := b.(keyRetrieversSetter); ok {
136+
s.SetKeyRetrievers(retrievers)
135137
}
136138
}
137139
}

browser/chromium/chromium.go

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package chromium
22

33
import (
4-
"fmt"
54
"os"
65
"path/filepath"
76
"time"
@@ -17,14 +16,14 @@ import (
1716
type Browser struct {
1817
cfg types.BrowserConfig
1918
profileDir string // absolute path to profile directory
20-
retriever keyretriever.KeyRetriever // set via SetRetriever after construction
19+
retrievers keyretriever.Retrievers // per-tier key sources (V10 / V11 / V20; unused tiers nil)
2120
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
2221
extractors map[types.Category]categoryExtractor // Category → custom extract function override
2322
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
2423
}
2524

2625
// NewBrowsers discovers Chromium profiles under cfg.UserDataDir and returns
27-
// one Browser per profile. Call SetRetriever on each returned browser before
26+
// one Browser per profile. Call SetKeyRetrievers on each returned browser before
2827
// Extract to enable decryption of sensitive data (passwords, cookies, etc.).
2928
func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
3029
sources := sourcesForKind(cfg.Kind)
@@ -52,11 +51,19 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
5251
return browsers, nil
5352
}
5453

55-
// SetRetriever sets the key retriever used by Extract to obtain the
56-
// master encryption key. Must be called before Extract if encrypted
57-
// data (passwords, cookies, credit cards) needs to be decrypted.
58-
func (b *Browser) SetRetriever(r keyretriever.KeyRetriever) {
59-
b.retriever = r
54+
// SetKeyRetrievers wires the per-tier master-key retrievers used by Extract. Each slot
55+
// (V10 / V11 / V20) is populated only on platforms where that cipher tier is used:
56+
//
57+
// - Windows: V10 (DPAPI) + V20 (ABE). V11 nil — Chromium does not emit v11 prefix on Windows.
58+
// - Linux: V10 ("peanuts" kV10Key) + V11 (D-Bus Secret Service kV11Key). V20 nil.
59+
// - macOS: V10 (Keychain chain). V11 and V20 nil.
60+
//
61+
// Slots are independent — a failure or absence in one tier does not affect others. A single
62+
// Chromium profile can carry mixed cipher-prefix ciphertexts (the motivation for issue #578), so
63+
// every configured retriever runs at extract time and decryptValue picks the matching key per
64+
// ciphertext.
65+
func (b *Browser) SetKeyRetrievers(r keyretriever.Retrievers) {
66+
b.retrievers = r
6067
}
6168

6269
func (b *Browser) BrowserName() string { return b.cfg.Name }
@@ -79,18 +86,15 @@ func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, erro
7986

8087
tempPaths := b.acquireFiles(session, categories)
8188

82-
masterKey, err := b.getMasterKey(session)
83-
if err != nil {
84-
log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err)
85-
}
89+
keys := b.getMasterKeys(session)
8690

8791
data := &types.BrowserData{}
8892
for _, cat := range categories {
8993
path, ok := tempPaths[cat]
9094
if !ok {
9195
continue
9296
}
93-
b.extractCategory(data, cat, masterKey, path)
97+
b.extractCategory(data, cat, keys, path)
9498
}
9599
return data, nil
96100
}
@@ -170,43 +174,46 @@ func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.
170174
return tempPaths
171175
}
172176

173-
// getMasterKey retrieves the Chromium master encryption key.
174-
//
175-
// On Windows, the key is read from the Local State file and decrypted via DPAPI.
176-
// On macOS, the key is derived from Keychain (Local State is not needed).
177-
// On Linux, the key is derived from D-Bus Secret Service or a fallback password.
178-
//
179-
// The retriever is always called regardless of whether Local State exists,
180-
// because macOS/Linux retrievers don't need it.
181-
func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) {
182-
if b.retriever == nil {
183-
return nil, fmt.Errorf("key retriever not set for %s", b.cfg.Name)
184-
}
185-
186-
// Try to locate and copy Local State (needed on Windows, ignored on macOS/Linux).
187-
// Multi-profile layout: Local State is in the parent of profileDir.
188-
// Flat layout (Opera): Local State is alongside data files in profileDir.
177+
// getMasterKeys retrieves the Chromium master keys for every configured tier. Chrome mixes
178+
// cipher tiers on the same profile — v20 for new cookies alongside v10 passwords on Windows; v10
179+
// (peanuts) alongside v11 (keyring) on Linux after session-mode changes — so every retriever in
180+
// b.retrievers runs independently and keyretriever.NewMasterKeys assembles the results. Any tier
181+
// key may be nil if its retriever failed or is not configured for this platform; decryptValue
182+
// treats a missing tier key as "that tier cannot decrypt" so partial success is still reported.
183+
func (b *Browser) getMasterKeys(session *filemanager.Session) keyretriever.MasterKeys {
184+
label := b.BrowserName() + "/" + b.ProfileName()
185+
186+
// Locate and copy Local State (needed on Windows, ignored on macOS/Linux). Multi-profile
187+
// layout: Local State is in the parent of profileDir. Flat layout (Opera): Local State is
188+
// alongside data files in profileDir.
189189
var localStateDst string
190190
for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} {
191191
candidate := filepath.Join(dir, "Local State")
192-
if fileutil.FileExists(candidate) {
193-
localStateDst = filepath.Join(session.TempDir(), "Local State")
194-
if err := session.Acquire(candidate, localStateDst, false); err != nil {
195-
return nil, err
196-
}
192+
if !fileutil.FileExists(candidate) {
193+
continue
194+
}
195+
dst := filepath.Join(session.TempDir(), "Local State")
196+
if err := session.Acquire(candidate, dst, false); err != nil {
197+
log.Debugf("acquire Local State for %s: %v", label, err)
197198
break
198199
}
200+
localStateDst = dst
201+
break
199202
}
200203

201-
return b.retriever.RetrieveKey(b.cfg.Storage, localStateDst)
204+
keys, err := keyretriever.NewMasterKeys(b.retrievers, b.cfg.Storage, localStateDst)
205+
if err != nil {
206+
log.Warnf("%s: master key retrieval: %v", label, err)
207+
}
208+
return keys
202209
}
203210

204211
// extractCategory calls the appropriate extract function for a category.
205212
// If a custom extractor is registered for this category (via extractorsForKind),
206213
// it is used instead of the default switch logic.
207-
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) {
214+
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, keys keyretriever.MasterKeys, path string) {
208215
if ext, ok := b.extractors[cat]; ok {
209-
if err := ext.extract(masterKey, path, data); err != nil {
216+
if err := ext.extract(keys, path, data); err != nil {
210217
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
211218
}
212219
return
@@ -215,17 +222,17 @@ func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, m
215222
var err error
216223
switch cat {
217224
case types.Password:
218-
data.Passwords, err = extractPasswords(masterKey, path)
225+
data.Passwords, err = extractPasswords(keys, path)
219226
case types.Cookie:
220-
data.Cookies, err = extractCookies(masterKey, path)
227+
data.Cookies, err = extractCookies(keys, path)
221228
case types.History:
222229
data.Histories, err = extractHistories(path)
223230
case types.Download:
224231
data.Downloads, err = extractDownloads(path)
225232
case types.Bookmark:
226233
data.Bookmarks, err = extractBookmarks(path)
227234
case types.CreditCard:
228-
data.CreditCards, err = extractCreditCards(masterKey, path)
235+
data.CreditCards, err = extractCreditCards(keys, path)
229236
case types.Extension:
230237
data.Extensions, err = extractExtensions(path)
231238
case types.LocalStorage:

0 commit comments

Comments
 (0)