11package chromium
22
33import (
4- "fmt"
54 "os"
65 "path/filepath"
76 "time"
@@ -17,14 +16,14 @@ import (
1716type 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.).
2928func 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
6269func (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