Skip to content

Commit e35907d

Browse files
authored
refactor: remove dead code and rename V2 files (#541)
* refactor: remove V1 dead code and rename V2 files - Delete extractor/ package (V1 Extractor interface and registry) - Delete browserdata/ package (V1 orchestrator, outputter, 9 sub-packages) - Delete V1 browser implementations (chromium.go, chromium_{platform}.go, firefox.go) - Delete types/types.go (V1 DataType enum) and utils/byteutil/ - Remove gocsv and go-sqlmock dependencies, demote x/text to indirect - Upgrade keychainbreaker v0.1.0 → v0.2.5 - Rename chromium_new.go → chromium.go, firefox_new.go → firefox.go * refactor: remove unused V1 utility functions Remove functions no longer called by V2 code: - fileutil: IsDirExists, CopyDir, BrowserName, ReadFile, CopyFile, Filename, ParentDir, ParentBaseDir, BaseDir - typeutil: Keys, IntToBool
1 parent 0ace27c commit e35907d

33 files changed

Lines changed: 389 additions & 3407 deletions

browser/chromium/chromium.go

Lines changed: 189 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,183 +1,244 @@
11
package chromium
22

33
import (
4-
"io/fs"
54
"os"
65
"path/filepath"
7-
"strings"
86

9-
"github.com/moond4rk/hackbrowserdata/browserdata"
7+
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
8+
"github.com/moond4rk/hackbrowserdata/filemanager"
109
"github.com/moond4rk/hackbrowserdata/log"
1110
"github.com/moond4rk/hackbrowserdata/types"
1211
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
13-
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
1412
)
1513

16-
type Chromium struct {
17-
name string
18-
storage string
19-
profilePath string
20-
masterKey []byte
21-
dataTypes []types.DataType
22-
Paths map[types.DataType]string
14+
// Browser represents a single Chromium profile ready for extraction.
15+
type Browser struct {
16+
cfg types.BrowserConfig
17+
profileDir string // absolute path to profile directory
18+
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
19+
extractors map[types.Category]categoryExtractor // Category → custom extract function override
20+
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
2321
}
2422

25-
// New create instance of Chromium browser, fill item's path if item is existed.
26-
func New(name, storage, profilePath string, dataTypes []types.DataType) ([]*Chromium, error) {
27-
c := &Chromium{
28-
name: name,
29-
storage: storage,
30-
profilePath: profilePath,
31-
dataTypes: dataTypes,
32-
}
33-
multiDataTypePaths, err := c.userDataTypePaths(c.profilePath, c.dataTypes)
34-
if err != nil {
35-
return nil, err
36-
}
37-
chromiumList := make([]*Chromium, 0, len(multiDataTypePaths))
38-
for user, itemPaths := range multiDataTypePaths {
39-
chromiumList = append(chromiumList, &Chromium{
40-
name: fileutil.BrowserName(name, user),
41-
dataTypes: typeutil.Keys(itemPaths),
42-
Paths: itemPaths,
43-
storage: storage,
44-
})
45-
}
46-
return chromiumList, nil
47-
}
23+
// NewBrowsers discovers Chromium profiles under cfg.UserDataDir and returns
24+
// one Browser per profile. Uses ReadDir to find profile directories,
25+
// then Stat to check which data sources exist in each profile.
26+
func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
27+
sources := sourcesForKind(cfg.Kind)
28+
extractors := extractorsForKind(cfg.Kind)
4829

49-
func (c *Chromium) Name() string {
50-
return c.name
51-
}
30+
profileDirs := discoverProfiles(cfg.UserDataDir, sources)
31+
if len(profileDirs) == 0 {
32+
return nil, nil
33+
}
5234

53-
func (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
54-
// delete chromiumKey from dataTypes, doesn't need to export key
55-
var dataTypes []types.DataType
56-
for _, dt := range c.dataTypes {
57-
if dt != types.ChromiumKey {
58-
dataTypes = append(dataTypes, dt)
35+
var browsers []*Browser
36+
for _, profileDir := range profileDirs {
37+
sourcePaths := resolveSourcePaths(sources, profileDir)
38+
if len(sourcePaths) == 0 {
39+
continue
5940
}
41+
browsers = append(browsers, &Browser{
42+
cfg: cfg,
43+
profileDir: profileDir,
44+
sources: sources,
45+
extractors: extractors,
46+
sourcePaths: sourcePaths,
47+
})
6048
}
49+
return browsers, nil
50+
}
6151

62-
if !isFullExport {
63-
dataTypes = types.FilterSensitiveItems(c.dataTypes)
52+
func (b *Browser) BrowserName() string { return b.cfg.Name }
53+
func (b *Browser) ProfileName() string {
54+
if b.profileDir == "" {
55+
return ""
6456
}
57+
return filepath.Base(b.profileDir)
58+
}
6559

66-
data := browserdata.New(dataTypes)
67-
68-
if err := c.copyItemToLocal(); err != nil {
60+
// Extract copies browser files to a temp directory, retrieves the master key,
61+
// and extracts data for the requested categories.
62+
func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) {
63+
session, err := filemanager.NewSession()
64+
if err != nil {
6965
return nil, err
7066
}
67+
defer session.Cleanup()
68+
69+
tempPaths := b.acquireFiles(session, categories)
7170

72-
masterKey, err := c.GetMasterKey()
71+
masterKey, err := b.getMasterKey(session)
7372
if err != nil {
74-
return nil, err
73+
log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err)
7574
}
7675

77-
c.masterKey = masterKey
78-
if err := data.Recovery(c.masterKey); err != nil {
79-
return nil, err
76+
data := &types.BrowserData{}
77+
for _, cat := range categories {
78+
path, ok := tempPaths[cat]
79+
if !ok {
80+
continue
81+
}
82+
b.extractCategory(data, cat, masterKey, path)
8083
}
81-
8284
return data, nil
8385
}
8486

85-
func (c *Chromium) copyItemToLocal() error {
86-
for i, path := range c.Paths {
87-
filename := i.TempFilename()
88-
var err error
89-
switch {
90-
case fileutil.IsDirExists(path):
91-
if i == types.ChromiumLocalStorage {
92-
err = fileutil.CopyDir(path, filename, "lock")
93-
}
94-
if i == types.ChromiumSessionStorage {
95-
err = fileutil.CopyDir(path, filename, "lock")
96-
}
97-
default:
98-
err = fileutil.CopyFile(path, filename)
87+
// acquireFiles copies source files to the session temp directory.
88+
func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string {
89+
tempPaths := make(map[types.Category]string)
90+
for _, cat := range categories {
91+
rp, ok := b.sourcePaths[cat]
92+
if !ok {
93+
continue
9994
}
100-
if err != nil {
101-
log.Errorf("copy item to local, path %s, filename %s err %v", path, filename, err)
95+
dst := filepath.Join(session.TempDir(), cat.String())
96+
if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil {
97+
log.Debugf("acquire %s: %v", cat, err)
10298
continue
10399
}
100+
tempPaths[cat] = dst
104101
}
105-
return nil
102+
return tempPaths
106103
}
107104

108-
// userDataTypePaths return a map of user to item path, map[profile 1][item's name & path key pair]
109-
func (c *Chromium) userDataTypePaths(profilePath string, items []types.DataType) (map[string]map[types.DataType]string, error) {
110-
multiItemPaths := make(map[string]map[types.DataType]string)
111-
parentDir := fileutil.ParentDir(profilePath)
112-
err := filepath.Walk(parentDir, chromiumWalkFunc(items, multiItemPaths))
113-
if err != nil {
114-
return nil, err
115-
}
116-
var keyPath string
117-
var dir string
118-
for userDir, profiles := range multiItemPaths {
119-
for _, profile := range profiles {
120-
if strings.HasSuffix(profile, types.ChromiumKey.Filename()) {
121-
keyPath = profile
122-
dir = userDir
123-
break
105+
// getMasterKey retrieves the Chromium master encryption key.
106+
//
107+
// On Windows, the key is read from the Local State file and decrypted via DPAPI.
108+
// On macOS, the key is derived from Keychain (Local State is not needed).
109+
// On Linux, the key is derived from D-Bus Secret Service or a fallback password.
110+
//
111+
// The retriever is always called regardless of whether Local State exists,
112+
// because macOS/Linux retrievers don't need it.
113+
func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) {
114+
// Try to locate and copy Local State (needed on Windows, ignored on macOS/Linux).
115+
// Multi-profile layout: Local State is in the parent of profileDir.
116+
// Flat layout (Opera): Local State is alongside data files in profileDir.
117+
var localStateDst string
118+
for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} {
119+
candidate := filepath.Join(dir, "Local State")
120+
if fileutil.IsFileExists(candidate) {
121+
localStateDst = filepath.Join(session.TempDir(), "Local State")
122+
if err := session.Acquire(candidate, localStateDst, false); err != nil {
123+
return nil, err
124124
}
125+
break
125126
}
126127
}
127-
t := make(map[string]map[types.DataType]string)
128-
for userDir, v := range multiItemPaths {
129-
if userDir == dir {
128+
129+
retriever := keyretriever.DefaultRetriever(b.cfg.KeychainPassword)
130+
return retriever.RetrieveKey(b.cfg.Storage, localStateDst)
131+
}
132+
133+
// extractCategory calls the appropriate extract function for a category.
134+
// If a custom extractor is registered for this category (via extractorsForKind),
135+
// it is used instead of the default switch logic.
136+
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) {
137+
if ext, ok := b.extractors[cat]; ok {
138+
if err := ext.extract(masterKey, path, data); err != nil {
139+
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
140+
}
141+
return
142+
}
143+
144+
var err error
145+
switch cat {
146+
case types.Password:
147+
data.Passwords, err = extractPasswords(masterKey, path)
148+
case types.Cookie:
149+
data.Cookies, err = extractCookies(masterKey, path)
150+
case types.History:
151+
data.Histories, err = extractHistories(path)
152+
case types.Download:
153+
data.Downloads, err = extractDownloads(path)
154+
case types.Bookmark:
155+
data.Bookmarks, err = extractBookmarks(path)
156+
case types.CreditCard:
157+
data.CreditCards, err = extractCreditCards(masterKey, path)
158+
case types.Extension:
159+
data.Extensions, err = extractExtensions(path)
160+
case types.LocalStorage:
161+
data.LocalStorage, err = extractLocalStorage(path)
162+
case types.SessionStorage:
163+
data.SessionStorage, err = extractSessionStorage(path)
164+
}
165+
if err != nil {
166+
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
167+
}
168+
}
169+
170+
// discoverProfiles lists subdirectories of userDataDir that contain at least
171+
// one known data source. Each such directory is a browser profile.
172+
func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string {
173+
entries, err := os.ReadDir(userDataDir)
174+
if err != nil {
175+
log.Debugf("read user data dir %s: %v", userDataDir, err)
176+
return nil
177+
}
178+
179+
var profiles []string
180+
for _, e := range entries {
181+
if !e.IsDir() || isSkippedDir(e.Name()) {
130182
continue
131183
}
132-
t[userDir] = v
133-
t[userDir][types.ChromiumKey] = keyPath
134-
fillLocalStoragePath(t[userDir], types.ChromiumLocalStorage)
184+
dir := filepath.Join(userDataDir, e.Name())
185+
if hasAnySource(sources, dir) {
186+
profiles = append(profiles, dir)
187+
}
188+
}
189+
190+
// Flat layout fallback (older Opera): data files directly in userDataDir
191+
if len(profiles) == 0 && hasAnySource(sources, userDataDir) {
192+
profiles = append(profiles, userDataDir)
135193
}
136-
return t, nil
194+
return profiles
137195
}
138196

139-
// chromiumWalkFunc return a filepath.WalkFunc to find item's path
140-
func chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) filepath.WalkFunc {
141-
return func(path string, info fs.FileInfo, err error) error {
142-
if err != nil {
143-
if os.IsPermission(err) {
144-
log.Warnf("skipping walk chromium path permission error, path %s, err %v", path, err)
145-
return nil
197+
// hasAnySource checks if dir contains at least one source file or directory.
198+
func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool {
199+
for _, candidates := range sources {
200+
for _, sp := range candidates {
201+
abs := filepath.Join(dir, sp.rel)
202+
if _, err := os.Stat(abs); err == nil {
203+
return true
146204
}
147-
return err
148205
}
149-
for _, v := range items {
150-
if info.Name() != v.Filename() {
151-
continue
152-
}
153-
if strings.Contains(path, "System Profile") {
154-
continue
155-
}
156-
if strings.Contains(path, "Snapshot") {
157-
continue
158-
}
159-
if strings.Contains(path, "def") {
206+
}
207+
return false
208+
}
209+
210+
// resolvedPath holds the absolute path and type for a discovered source.
211+
type resolvedPath struct {
212+
absPath string
213+
isDir bool
214+
}
215+
216+
// resolveSourcePaths checks which sources actually exist in profileDir.
217+
// Candidates are tried in priority order; the first existing path wins.
218+
func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath {
219+
resolved := make(map[types.Category]resolvedPath)
220+
for cat, candidates := range sources {
221+
for _, sp := range candidates {
222+
abs := filepath.Join(profileDir, sp.rel)
223+
info, err := os.Stat(abs)
224+
if err != nil {
160225
continue
161226
}
162-
profileFolder := fileutil.ParentBaseDir(path)
163-
if strings.Contains(filepath.ToSlash(path), "/Network/Cookies") {
164-
profileFolder = fileutil.BaseDir(strings.ReplaceAll(filepath.ToSlash(path), "/Network/Cookies", ""))
165-
}
166-
if _, exist := multiItemPaths[profileFolder]; exist {
167-
multiItemPaths[profileFolder][v] = path
168-
} else {
169-
multiItemPaths[profileFolder] = map[types.DataType]string{v: path}
227+
if sp.isDir == info.IsDir() {
228+
resolved[cat] = resolvedPath{abs, sp.isDir}
229+
break
170230
}
171231
}
172-
return nil
173232
}
233+
return resolved
174234
}
175235

176-
func fillLocalStoragePath(itemPaths map[types.DataType]string, storage types.DataType) {
177-
if p, ok := itemPaths[types.ChromiumHistory]; ok {
178-
lsp := filepath.Join(filepath.Dir(p), storage.Filename())
179-
if fileutil.IsDirExists(lsp) {
180-
itemPaths[types.ChromiumLocalStorage] = lsp
181-
}
236+
// isSkippedDir returns true for directory names that should never be
237+
// treated as browser profiles.
238+
func isSkippedDir(name string) bool {
239+
switch name {
240+
case "System Profile", "Guest Profile", "Snapshot":
241+
return true
182242
}
243+
return false
183244
}

0 commit comments

Comments
 (0)