From e35907de6f145f53209dbf39054b7bc1449d5d60 Mon Sep 17 00:00:00 2001 From: Roger Date: Sat, 4 Apr 2026 15:51:54 +0800 Subject: [PATCH] refactor: remove dead code and rename V2 files (#541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- browser/chromium/chromium.go | 341 +++++++++++------- browser/chromium/chromium_darwin.go | 77 ---- browser/chromium/chromium_linux.go | 76 ---- browser/chromium/chromium_new.go | 244 ------------- ...{chromium_new_test.go => chromium_test.go} | 0 browser/chromium/chromium_windows.go | 43 --- browser/firefox/firefox.go | 288 ++++++++++----- browser/firefox/firefox_new.go | 237 ------------ .../{firefox_new_test.go => firefox_test.go} | 0 browserdata/bookmark/bookmark.go | 159 -------- browserdata/browser_data.go | 18 - browserdata/browserdata.go | 67 ---- browserdata/cookie/cookie.go | 165 --------- browserdata/creditcard/creditcard.go | 147 -------- browserdata/download/download.go | 146 -------- browserdata/extension/extension.go | 187 ---------- browserdata/history/history.go | 137 ------- browserdata/imports.go | 20 - browserdata/localstorage/localstorage.go | 234 ------------ browserdata/localstorage/localstorage_test.go | 219 ----------- browserdata/outputter.go | 79 ---- browserdata/outputter_test.go | 23 -- browserdata/password/password.go | 259 ------------- browserdata/sessionstorage/sessionstorage.go | 175 --------- extractor/extractor.go | 10 - extractor/registration.go | 20 - go.mod | 7 +- go.sum | 12 +- types/types.go | 221 ------------ types/types_test.go | 130 ------- utils/byteutil/byteutil.go | 8 - utils/fileutil/filetutil.go | 69 ---- utils/typeutil/typeutil.go | 24 -- 33 files changed, 412 insertions(+), 3430 deletions(-) delete mode 100644 browser/chromium/chromium_darwin.go delete mode 100644 browser/chromium/chromium_linux.go delete mode 100644 browser/chromium/chromium_new.go rename browser/chromium/{chromium_new_test.go => chromium_test.go} (100%) delete mode 100644 browser/chromium/chromium_windows.go delete mode 100644 browser/firefox/firefox_new.go rename browser/firefox/{firefox_new_test.go => firefox_test.go} (100%) delete mode 100644 browserdata/bookmark/bookmark.go delete mode 100644 browserdata/browser_data.go delete mode 100644 browserdata/browserdata.go delete mode 100644 browserdata/cookie/cookie.go delete mode 100644 browserdata/creditcard/creditcard.go delete mode 100644 browserdata/download/download.go delete mode 100644 browserdata/extension/extension.go delete mode 100644 browserdata/history/history.go delete mode 100644 browserdata/imports.go delete mode 100644 browserdata/localstorage/localstorage.go delete mode 100644 browserdata/localstorage/localstorage_test.go delete mode 100644 browserdata/outputter.go delete mode 100644 browserdata/outputter_test.go delete mode 100644 browserdata/password/password.go delete mode 100644 browserdata/sessionstorage/sessionstorage.go delete mode 100644 extractor/extractor.go delete mode 100644 extractor/registration.go delete mode 100644 types/types.go delete mode 100644 types/types_test.go delete mode 100644 utils/byteutil/byteutil.go diff --git a/browser/chromium/chromium.go b/browser/chromium/chromium.go index b0de100..904c3bc 100644 --- a/browser/chromium/chromium.go +++ b/browser/chromium/chromium.go @@ -1,183 +1,244 @@ package chromium import ( - "io/fs" "os" "path/filepath" - "strings" - "github.com/moond4rk/hackbrowserdata/browserdata" + "github.com/moond4rk/hackbrowserdata/crypto/keyretriever" + "github.com/moond4rk/hackbrowserdata/filemanager" "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/fileutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) -type Chromium struct { - name string - storage string - profilePath string - masterKey []byte - dataTypes []types.DataType - Paths map[types.DataType]string +// Browser represents a single Chromium profile ready for extraction. +type Browser struct { + cfg types.BrowserConfig + profileDir string // absolute path to profile directory + 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 } -// New create instance of Chromium browser, fill item's path if item is existed. -func New(name, storage, profilePath string, dataTypes []types.DataType) ([]*Chromium, error) { - c := &Chromium{ - name: name, - storage: storage, - profilePath: profilePath, - dataTypes: dataTypes, +// NewBrowsers discovers Chromium profiles under cfg.UserDataDir and returns +// one Browser per profile. Uses ReadDir to find profile directories, +// then Stat to check which data sources exist in each profile. +func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) { + sources := sourcesForKind(cfg.Kind) + extractors := extractorsForKind(cfg.Kind) + + profileDirs := discoverProfiles(cfg.UserDataDir, sources) + if len(profileDirs) == 0 { + return nil, nil } - multiDataTypePaths, err := c.userDataTypePaths(c.profilePath, c.dataTypes) - if err != nil { - return nil, err - } - chromiumList := make([]*Chromium, 0, len(multiDataTypePaths)) - for user, itemPaths := range multiDataTypePaths { - chromiumList = append(chromiumList, &Chromium{ - name: fileutil.BrowserName(name, user), - dataTypes: typeutil.Keys(itemPaths), - Paths: itemPaths, - storage: storage, + + var browsers []*Browser + for _, profileDir := range profileDirs { + sourcePaths := resolveSourcePaths(sources, profileDir) + if len(sourcePaths) == 0 { + continue + } + browsers = append(browsers, &Browser{ + cfg: cfg, + profileDir: profileDir, + sources: sources, + extractors: extractors, + sourcePaths: sourcePaths, }) } - return chromiumList, nil + return browsers, nil } -func (c *Chromium) Name() string { - return c.name +func (b *Browser) BrowserName() string { return b.cfg.Name } +func (b *Browser) ProfileName() string { + if b.profileDir == "" { + return "" + } + return filepath.Base(b.profileDir) } -func (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) { - // delete chromiumKey from dataTypes, doesn't need to export key - var dataTypes []types.DataType - for _, dt := range c.dataTypes { - if dt != types.ChromiumKey { - dataTypes = append(dataTypes, dt) - } - } - - if !isFullExport { - dataTypes = types.FilterSensitiveItems(c.dataTypes) - } - - data := browserdata.New(dataTypes) - - if err := c.copyItemToLocal(); err != nil { - return nil, err - } - - masterKey, err := c.GetMasterKey() +// Extract copies browser files to a temp directory, retrieves the master key, +// and extracts data for the requested categories. +func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) { + session, err := filemanager.NewSession() if err != nil { return nil, err } + defer session.Cleanup() - c.masterKey = masterKey - if err := data.Recovery(c.masterKey); err != nil { - return nil, err + 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) } + data := &types.BrowserData{} + for _, cat := range categories { + path, ok := tempPaths[cat] + if !ok { + continue + } + b.extractCategory(data, cat, masterKey, path) + } return data, nil } -func (c *Chromium) copyItemToLocal() error { - for i, path := range c.Paths { - filename := i.TempFilename() - var err error - switch { - case fileutil.IsDirExists(path): - if i == types.ChromiumLocalStorage { - err = fileutil.CopyDir(path, filename, "lock") - } - if i == types.ChromiumSessionStorage { - err = fileutil.CopyDir(path, filename, "lock") - } - default: - err = fileutil.CopyFile(path, filename) - } - if err != nil { - log.Errorf("copy item to local, path %s, filename %s err %v", path, filename, err) +// acquireFiles copies source files to the session temp directory. +func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string { + tempPaths := make(map[types.Category]string) + for _, cat := range categories { + rp, ok := b.sourcePaths[cat] + if !ok { continue } + dst := filepath.Join(session.TempDir(), cat.String()) + if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil { + log.Debugf("acquire %s: %v", cat, err) + continue + } + tempPaths[cat] = dst } - return nil + return tempPaths } -// userDataTypePaths return a map of user to item path, map[profile 1][item's name & path key pair] -func (c *Chromium) userDataTypePaths(profilePath string, items []types.DataType) (map[string]map[types.DataType]string, error) { - multiItemPaths := make(map[string]map[types.DataType]string) - parentDir := fileutil.ParentDir(profilePath) - err := filepath.Walk(parentDir, chromiumWalkFunc(items, multiItemPaths)) - if err != nil { - return nil, err +// 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) { + // 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. + var localStateDst string + for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} { + candidate := filepath.Join(dir, "Local State") + if fileutil.IsFileExists(candidate) { + localStateDst = filepath.Join(session.TempDir(), "Local State") + if err := session.Acquire(candidate, localStateDst, false); err != nil { + return nil, err + } + break + } } - var keyPath string - var dir string - for userDir, profiles := range multiItemPaths { - for _, profile := range profiles { - if strings.HasSuffix(profile, types.ChromiumKey.Filename()) { - keyPath = profile - dir = userDir + + retriever := keyretriever.DefaultRetriever(b.cfg.KeychainPassword) + return retriever.RetrieveKey(b.cfg.Storage, localStateDst) +} + +// 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) { + if ext, ok := b.extractors[cat]; ok { + if err := ext.extract(masterKey, path, data); err != nil { + log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err) + } + return + } + + var err error + switch cat { + case types.Password: + data.Passwords, err = extractPasswords(masterKey, path) + case types.Cookie: + data.Cookies, err = extractCookies(masterKey, path) + case types.History: + data.Histories, err = extractHistories(path) + case types.Download: + data.Downloads, err = extractDownloads(path) + case types.Bookmark: + data.Bookmarks, err = extractBookmarks(path) + case types.CreditCard: + data.CreditCards, err = extractCreditCards(masterKey, path) + case types.Extension: + data.Extensions, err = extractExtensions(path) + case types.LocalStorage: + data.LocalStorage, err = extractLocalStorage(path) + case types.SessionStorage: + data.SessionStorage, err = extractSessionStorage(path) + } + if err != nil { + log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err) + } +} + +// discoverProfiles lists subdirectories of userDataDir that contain at least +// one known data source. Each such directory is a browser profile. +func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string { + entries, err := os.ReadDir(userDataDir) + if err != nil { + log.Debugf("read user data dir %s: %v", userDataDir, err) + return nil + } + + var profiles []string + for _, e := range entries { + if !e.IsDir() || isSkippedDir(e.Name()) { + continue + } + dir := filepath.Join(userDataDir, e.Name()) + if hasAnySource(sources, dir) { + profiles = append(profiles, dir) + } + } + + // Flat layout fallback (older Opera): data files directly in userDataDir + if len(profiles) == 0 && hasAnySource(sources, userDataDir) { + profiles = append(profiles, userDataDir) + } + return profiles +} + +// hasAnySource checks if dir contains at least one source file or directory. +func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool { + for _, candidates := range sources { + for _, sp := range candidates { + abs := filepath.Join(dir, sp.rel) + if _, err := os.Stat(abs); err == nil { + return true + } + } + } + return false +} + +// resolvedPath holds the absolute path and type for a discovered source. +type resolvedPath struct { + absPath string + isDir bool +} + +// resolveSourcePaths checks which sources actually exist in profileDir. +// Candidates are tried in priority order; the first existing path wins. +func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath { + resolved := make(map[types.Category]resolvedPath) + for cat, candidates := range sources { + for _, sp := range candidates { + abs := filepath.Join(profileDir, sp.rel) + info, err := os.Stat(abs) + if err != nil { + continue + } + if sp.isDir == info.IsDir() { + resolved[cat] = resolvedPath{abs, sp.isDir} break } } } - t := make(map[string]map[types.DataType]string) - for userDir, v := range multiItemPaths { - if userDir == dir { - continue - } - t[userDir] = v - t[userDir][types.ChromiumKey] = keyPath - fillLocalStoragePath(t[userDir], types.ChromiumLocalStorage) - } - return t, nil + return resolved } -// chromiumWalkFunc return a filepath.WalkFunc to find item's path -func chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) filepath.WalkFunc { - return func(path string, info fs.FileInfo, err error) error { - if err != nil { - if os.IsPermission(err) { - log.Warnf("skipping walk chromium path permission error, path %s, err %v", path, err) - return nil - } - return err - } - for _, v := range items { - if info.Name() != v.Filename() { - continue - } - if strings.Contains(path, "System Profile") { - continue - } - if strings.Contains(path, "Snapshot") { - continue - } - if strings.Contains(path, "def") { - continue - } - profileFolder := fileutil.ParentBaseDir(path) - if strings.Contains(filepath.ToSlash(path), "/Network/Cookies") { - profileFolder = fileutil.BaseDir(strings.ReplaceAll(filepath.ToSlash(path), "/Network/Cookies", "")) - } - if _, exist := multiItemPaths[profileFolder]; exist { - multiItemPaths[profileFolder][v] = path - } else { - multiItemPaths[profileFolder] = map[types.DataType]string{v: path} - } - } - return nil - } -} - -func fillLocalStoragePath(itemPaths map[types.DataType]string, storage types.DataType) { - if p, ok := itemPaths[types.ChromiumHistory]; ok { - lsp := filepath.Join(filepath.Dir(p), storage.Filename()) - if fileutil.IsDirExists(lsp) { - itemPaths[types.ChromiumLocalStorage] = lsp - } +// isSkippedDir returns true for directory names that should never be +// treated as browser profiles. +func isSkippedDir(name string) bool { + switch name { + case "System Profile", "Guest Profile", "Snapshot": + return true } + return false } diff --git a/browser/chromium/chromium_darwin.go b/browser/chromium/chromium_darwin.go deleted file mode 100644 index d40278d..0000000 --- a/browser/chromium/chromium_darwin.go +++ /dev/null @@ -1,77 +0,0 @@ -//go:build darwin - -package chromium - -import ( - "bytes" - "crypto/sha1" - "errors" - "fmt" - "os" - "os/exec" - "strings" - - "github.com/moond4rk/hackbrowserdata/crypto" - "github.com/moond4rk/hackbrowserdata/crypto/keyretriever" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" -) - -var ( - errWrongSecurityCommand = errors.New("wrong security command") - errCouldNotFindInKeychain = errors.New("could not be find in keychain") -) - -func (c *Chromium) GetMasterKey() ([]byte, error) { - // don't need chromium key file for macOS - defer os.Remove(types.ChromiumKey.TempFilename()) - - // Try get the master key via gcoredump(CVE-2025-24204) - secret, err := keyretriever.DecryptKeychain(c.storage) - if err == nil && secret != "" { - log.Debugf("get master key via gcoredump(CVE-2025-24204) success, browser %s", c.name) - if key, err := c.parseSecret([]byte(secret)); err == nil { - return key, nil - } - } else { - log.Warnf("get master key via gcoredump(CVE-2025-24204) failed: %v, skipping...", err) - } - - // Get the master key from the keychain - // $ security find-generic-password -wa 'Chrome' - var ( - stdout, stderr bytes.Buffer - ) - cmd := exec.Command("security", "find-generic-password", "-wa", strings.TrimSpace(c.storage)) //nolint:gosec - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("run security command failed: %w, message %s", err, stderr.String()) - } - - if stderr.Len() > 0 { - if strings.Contains(stderr.String(), "could not be found") { - return nil, errCouldNotFindInKeychain - } - return nil, errors.New(stderr.String()) - } - - return c.parseSecret(stdout.Bytes()) -} - -func (c *Chromium) parseSecret(secret []byte) ([]byte, error) { - secret = bytes.TrimSpace(secret) - if len(secret) == 0 { - return nil, errWrongSecurityCommand - } - - salt := []byte("saltysalt") - // @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157 - key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New) - if key == nil { - return nil, errWrongSecurityCommand - } - c.masterKey = key - log.Debugf("get master key success, browser %s", c.name) - return key, nil -} diff --git a/browser/chromium/chromium_linux.go b/browser/chromium/chromium_linux.go deleted file mode 100644 index b654171..0000000 --- a/browser/chromium/chromium_linux.go +++ /dev/null @@ -1,76 +0,0 @@ -//go:build linux - -package chromium - -import ( - "crypto/sha1" - "fmt" - "os" - - "github.com/godbus/dbus/v5" - keyring "github.com/ppacher/go-dbus-keyring" - - "github.com/moond4rk/hackbrowserdata/crypto" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" -) - -func (c *Chromium) GetMasterKey() ([]byte, error) { - // what is d-bus @https://dbus.freedesktop.org/ - // don't need chromium key file for Linux - defer os.Remove(types.ChromiumKey.TempFilename()) - - conn, err := dbus.SessionBus() - if err != nil { - return nil, err - } - svc, err := keyring.GetSecretService(conn) - if err != nil { - return nil, err - } - session, err := svc.OpenSession() - if err != nil { - return nil, err - } - defer func() { - if err := session.Close(); err != nil { - log.Errorf("close dbus session error: %v", err) - } - }() - collections, err := svc.GetAllCollections() - if err != nil { - return nil, err - } - var secret []byte - for _, col := range collections { - items, err := col.GetAllItems() - if err != nil { - return nil, err - } - for _, i := range items { - label, err := i.GetLabel() - if err != nil { - log.Warnf("get label from dbus: %v", err) - continue - } - if label == c.storage { - se, err := i.GetSecret(session.Path()) - if err != nil { - return nil, fmt.Errorf("get storage from dbus: %w", err) - } - secret = se.Value - } - } - } - - if len(secret) == 0 { - // set default secret @https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/os_crypt_linux.cc;l=100 - secret = []byte("peanuts") - } - salt := []byte("saltysalt") - // @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_linux.cc - key := crypto.PBKDF2Key(secret, salt, 1, 16, sha1.New) - c.masterKey = key - log.Debugf("get master key success, browser %s", c.name) - return key, nil -} diff --git a/browser/chromium/chromium_new.go b/browser/chromium/chromium_new.go deleted file mode 100644 index 904c3bc..0000000 --- a/browser/chromium/chromium_new.go +++ /dev/null @@ -1,244 +0,0 @@ -package chromium - -import ( - "os" - "path/filepath" - - "github.com/moond4rk/hackbrowserdata/crypto/keyretriever" - "github.com/moond4rk/hackbrowserdata/filemanager" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/fileutil" -) - -// Browser represents a single Chromium profile ready for extraction. -type Browser struct { - cfg types.BrowserConfig - profileDir string // absolute path to profile directory - 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. Uses ReadDir to find profile directories, -// then Stat to check which data sources exist in each profile. -func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) { - sources := sourcesForKind(cfg.Kind) - extractors := extractorsForKind(cfg.Kind) - - profileDirs := discoverProfiles(cfg.UserDataDir, sources) - if len(profileDirs) == 0 { - return nil, nil - } - - var browsers []*Browser - for _, profileDir := range profileDirs { - sourcePaths := resolveSourcePaths(sources, profileDir) - if len(sourcePaths) == 0 { - continue - } - browsers = append(browsers, &Browser{ - cfg: cfg, - profileDir: profileDir, - sources: sources, - extractors: extractors, - sourcePaths: sourcePaths, - }) - } - return browsers, nil -} - -func (b *Browser) BrowserName() string { return b.cfg.Name } -func (b *Browser) ProfileName() string { - if b.profileDir == "" { - return "" - } - return filepath.Base(b.profileDir) -} - -// Extract copies browser files to a temp directory, retrieves the master key, -// and extracts data for the requested categories. -func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) { - session, err := filemanager.NewSession() - if err != nil { - return nil, err - } - defer session.Cleanup() - - 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) - } - - data := &types.BrowserData{} - for _, cat := range categories { - path, ok := tempPaths[cat] - if !ok { - continue - } - b.extractCategory(data, cat, masterKey, path) - } - return data, nil -} - -// acquireFiles copies source files to the session temp directory. -func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string { - tempPaths := make(map[types.Category]string) - for _, cat := range categories { - rp, ok := b.sourcePaths[cat] - if !ok { - continue - } - dst := filepath.Join(session.TempDir(), cat.String()) - if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil { - log.Debugf("acquire %s: %v", cat, err) - continue - } - tempPaths[cat] = dst - } - 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) { - // 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. - var localStateDst string - for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} { - candidate := filepath.Join(dir, "Local State") - if fileutil.IsFileExists(candidate) { - localStateDst = filepath.Join(session.TempDir(), "Local State") - if err := session.Acquire(candidate, localStateDst, false); err != nil { - return nil, err - } - break - } - } - - retriever := keyretriever.DefaultRetriever(b.cfg.KeychainPassword) - return retriever.RetrieveKey(b.cfg.Storage, localStateDst) -} - -// 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) { - if ext, ok := b.extractors[cat]; ok { - if err := ext.extract(masterKey, path, data); err != nil { - log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err) - } - return - } - - var err error - switch cat { - case types.Password: - data.Passwords, err = extractPasswords(masterKey, path) - case types.Cookie: - data.Cookies, err = extractCookies(masterKey, path) - case types.History: - data.Histories, err = extractHistories(path) - case types.Download: - data.Downloads, err = extractDownloads(path) - case types.Bookmark: - data.Bookmarks, err = extractBookmarks(path) - case types.CreditCard: - data.CreditCards, err = extractCreditCards(masterKey, path) - case types.Extension: - data.Extensions, err = extractExtensions(path) - case types.LocalStorage: - data.LocalStorage, err = extractLocalStorage(path) - case types.SessionStorage: - data.SessionStorage, err = extractSessionStorage(path) - } - if err != nil { - log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err) - } -} - -// discoverProfiles lists subdirectories of userDataDir that contain at least -// one known data source. Each such directory is a browser profile. -func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string { - entries, err := os.ReadDir(userDataDir) - if err != nil { - log.Debugf("read user data dir %s: %v", userDataDir, err) - return nil - } - - var profiles []string - for _, e := range entries { - if !e.IsDir() || isSkippedDir(e.Name()) { - continue - } - dir := filepath.Join(userDataDir, e.Name()) - if hasAnySource(sources, dir) { - profiles = append(profiles, dir) - } - } - - // Flat layout fallback (older Opera): data files directly in userDataDir - if len(profiles) == 0 && hasAnySource(sources, userDataDir) { - profiles = append(profiles, userDataDir) - } - return profiles -} - -// hasAnySource checks if dir contains at least one source file or directory. -func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool { - for _, candidates := range sources { - for _, sp := range candidates { - abs := filepath.Join(dir, sp.rel) - if _, err := os.Stat(abs); err == nil { - return true - } - } - } - return false -} - -// resolvedPath holds the absolute path and type for a discovered source. -type resolvedPath struct { - absPath string - isDir bool -} - -// resolveSourcePaths checks which sources actually exist in profileDir. -// Candidates are tried in priority order; the first existing path wins. -func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath { - resolved := make(map[types.Category]resolvedPath) - for cat, candidates := range sources { - for _, sp := range candidates { - abs := filepath.Join(profileDir, sp.rel) - info, err := os.Stat(abs) - if err != nil { - continue - } - if sp.isDir == info.IsDir() { - resolved[cat] = resolvedPath{abs, sp.isDir} - break - } - } - } - return resolved -} - -// isSkippedDir returns true for directory names that should never be -// treated as browser profiles. -func isSkippedDir(name string) bool { - switch name { - case "System Profile", "Guest Profile", "Snapshot": - return true - } - return false -} diff --git a/browser/chromium/chromium_new_test.go b/browser/chromium/chromium_test.go similarity index 100% rename from browser/chromium/chromium_new_test.go rename to browser/chromium/chromium_test.go diff --git a/browser/chromium/chromium_windows.go b/browser/chromium/chromium_windows.go deleted file mode 100644 index 439eaa2..0000000 --- a/browser/chromium/chromium_windows.go +++ /dev/null @@ -1,43 +0,0 @@ -//go:build windows - -package chromium - -import ( - "encoding/base64" - "errors" - "os" - - "github.com/tidwall/gjson" - - "github.com/moond4rk/hackbrowserdata/crypto" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/fileutil" -) - -var errDecodeMasterKeyFailed = errors.New("decode master key failed") - -func (c *Chromium) GetMasterKey() ([]byte, error) { - b, err := fileutil.ReadFile(types.ChromiumKey.TempFilename()) - if err != nil { - return nil, err - } - defer os.Remove(types.ChromiumKey.TempFilename()) - - encryptedKey := gjson.Get(b, "os_crypt.encrypted_key") - if !encryptedKey.Exists() { - return nil, nil - } - - key, err := base64.StdEncoding.DecodeString(encryptedKey.String()) - if err != nil { - return nil, errDecodeMasterKeyFailed - } - c.masterKey, err = crypto.DecryptWithDPAPI(key[5:]) - if err != nil { - log.Errorf("decrypt master key failed, err %v", err) - return nil, err - } - log.Debugf("get master key success, browser %s", c.name) - return c.masterKey, nil -} diff --git a/browser/firefox/firefox.go b/browser/firefox/firefox.go index 7b87ff8..1a170fd 100644 --- a/browser/firefox/firefox.go +++ b/browser/firefox/firefox.go @@ -3,115 +3,235 @@ package firefox import ( "errors" "fmt" - "io/fs" "os" "path/filepath" - _ "modernc.org/sqlite" // sqlite3 driver TODO: replace with chooseable driver - - "github.com/moond4rk/hackbrowserdata/browserdata" + "github.com/moond4rk/hackbrowserdata/filemanager" "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/fileutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) -type Firefox struct { - name string - storage string - profilePath string - masterKey []byte - items []types.DataType - itemPaths map[types.DataType]string +// Browser represents a single Firefox profile ready for extraction. +type Browser struct { + cfg types.BrowserConfig + profileDir string // absolute path to profile directory + sources map[types.Category][]sourcePath // Category → candidate paths (priority order) + sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path } -var ErrProfilePathNotFound = errors.New("profile path not found") +// NewBrowsers discovers Firefox profiles under cfg.UserDataDir and returns +// one Browser per profile. Firefox profile directories have random names +// (e.g. "97nszz88.default-release"); any subdirectory containing known +// data files is treated as a valid profile. +func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) { + profileDirs := discoverProfiles(cfg.UserDataDir, firefoxSources) + if len(profileDirs) == 0 { + return nil, nil + } -// New returns new Firefox instances. -func New(profilePath string, items []types.DataType) ([]*Firefox, error) { - multiItemPaths := make(map[string]map[types.DataType]string) - // ignore walk dir error since it can be produced by a single entry - _ = filepath.WalkDir(profilePath, firefoxWalkFunc(items, multiItemPaths)) - - firefoxList := make([]*Firefox, 0, len(multiItemPaths)) - for name, itemPaths := range multiItemPaths { - firefoxList = append(firefoxList, &Firefox{ - name: fmt.Sprintf("firefox-%s", name), - items: typeutil.Keys(itemPaths), - itemPaths: itemPaths, + var browsers []*Browser + for _, profileDir := range profileDirs { + sourcePaths := resolveSourcePaths(firefoxSources, profileDir) + if len(sourcePaths) == 0 { + continue + } + browsers = append(browsers, &Browser{ + cfg: cfg, + profileDir: profileDir, + sources: firefoxSources, + sourcePaths: sourcePaths, }) } - - return firefoxList, nil + return browsers, nil } -func (f *Firefox) copyItemToLocal() error { - for i, path := range f.itemPaths { - filename := i.TempFilename() - if err := fileutil.CopyFile(path, filename); err != nil { - return err - } +func (b *Browser) BrowserName() string { return b.cfg.Name } +func (b *Browser) ProfileName() string { + if b.profileDir == "" { + return "" } - return nil + return filepath.Base(b.profileDir) } -func firefoxWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) fs.WalkDirFunc { - return func(path string, info fs.DirEntry, err error) error { - if err != nil { - if os.IsPermission(err) { - log.Warnf("skipping walk firefox path %s permission error: %v", path, err) - return nil - } - return err - } - for _, v := range items { - if info.Name() == v.Filename() { - parentBaseDir := fileutil.ParentBaseDir(path) - if _, exist := multiItemPaths[parentBaseDir]; exist { - multiItemPaths[parentBaseDir][v] = path - } else { - multiItemPaths[parentBaseDir] = map[types.DataType]string{v: path} - } - } - } - - return nil - } -} - -// GetMasterKey returns master key of Firefox. from key4.db -func (f *Firefox) GetMasterKey() ([]byte, error) { - tempFilename := types.FirefoxKey4.TempFilename() - defer os.Remove(tempFilename) - - loginsPath := types.FirefoxPassword.TempFilename() - return retrieveMasterKey(tempFilename, loginsPath) -} - -func (f *Firefox) Name() string { - return f.name -} - -func (f *Firefox) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) { - dataTypes := f.items - if !isFullExport { - dataTypes = types.FilterSensitiveItems(f.items) - } - - data := browserdata.New(dataTypes) - - if err := f.copyItemToLocal(); err != nil { +// Extract copies browser files to a temp directory, retrieves the master key, +// and extracts data for the requested categories. +func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) { + session, err := filemanager.NewSession() + if err != nil { return nil, err } + defer session.Cleanup() - masterKey, err := f.GetMasterKey() + tempPaths := b.acquireFiles(session, categories) + + masterKey, err := b.getMasterKey(session, tempPaths) + if err != nil { + log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err) + } + + data := &types.BrowserData{} + for _, cat := range categories { + path, ok := tempPaths[cat] + if !ok { + continue + } + b.extractCategory(data, cat, masterKey, path) + } + return data, nil +} + +// acquireFiles copies source files to the session temp directory. +func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string { + tempPaths := make(map[types.Category]string) + for _, cat := range categories { + rp, ok := b.sourcePaths[cat] + if !ok { + continue + } + dst := filepath.Join(session.TempDir(), cat.String()) + if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil { + log.Debugf("acquire %s: %v", cat, err) + continue + } + tempPaths[cat] = dst + } + return tempPaths +} + +// getMasterKey retrieves the Firefox master encryption key from key4.db. +// The key is derived via NSS ASN1 PBE decryption (platform-agnostic). +// If logins.json was already acquired by acquireFiles, the derived key +// is validated by attempting to decrypt an actual login entry. +func (b *Browser) getMasterKey(session *filemanager.Session, tempPaths map[types.Category]string) ([]byte, error) { + key4Src := filepath.Join(b.profileDir, "key4.db") + if !fileutil.IsFileExists(key4Src) { + return nil, nil + } + key4Dst := filepath.Join(session.TempDir(), "key4.db") + if err := session.Acquire(key4Src, key4Dst, false); err != nil { + return nil, fmt.Errorf("acquire key4.db: %w", err) + } + + // logins.json is already acquired by acquireFiles as the Password source; + // reuse it for master key validation if available. + loginsPath := tempPaths[types.Password] + return retrieveMasterKey(key4Dst, loginsPath) +} + +// retrieveMasterKey reads key4.db and derives the master key using NSS. +// If loginsPath is non-empty, the derived key is validated against actual +// login data to ensure the correct candidate is selected. +func retrieveMasterKey(key4Path, loginsPath string) ([]byte, error) { + k4, err := readKey4DB(key4Path) if err != nil { return nil, err } - f.masterKey = masterKey - if err := data.Recovery(f.masterKey); err != nil { + keys, err := k4.deriveKeys() + if err != nil { return nil, err } - return data, nil + if len(keys) == 0 { + return nil, errors.New("no valid master key candidates in key4.db") + } + + // No logins to validate against — return the first derived key. + if loginsPath == "" { + return keys[0], nil + } + + // Validate against actual login data. + if key := validateKeyWithLogins(keys, loginsPath); key != nil { + return key, nil + } + + return nil, fmt.Errorf("derived %d key(s) but none could decrypt logins", len(keys)) +} + +// extractCategory calls the appropriate extract function for a category. +func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) { + var err error + switch cat { + case types.Password: + data.Passwords, err = extractPasswords(masterKey, path) + case types.Cookie: + data.Cookies, err = extractCookies(path) + case types.History: + data.Histories, err = extractHistories(path) + case types.Download: + data.Downloads, err = extractDownloads(path) + case types.Bookmark: + data.Bookmarks, err = extractBookmarks(path) + case types.Extension: + data.Extensions, err = extractExtensions(path) + case types.LocalStorage: + data.LocalStorage, err = extractLocalStorage(path) + case types.CreditCard, types.SessionStorage: + // Firefox does not support CreditCard or SessionStorage extraction. + } + if err != nil { + log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err) + } +} + +// resolvedPath holds the absolute path and type for a discovered source. +type resolvedPath struct { + absPath string + isDir bool +} + +// discoverProfiles lists subdirectories of userDataDir that contain at least +// one known data source. Each such directory is a Firefox profile. +func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string { + entries, err := os.ReadDir(userDataDir) + if err != nil { + log.Debugf("read user data dir %s: %v", userDataDir, err) + return nil + } + + var profiles []string + for _, e := range entries { + if !e.IsDir() { + continue + } + dir := filepath.Join(userDataDir, e.Name()) + if hasAnySource(sources, dir) { + profiles = append(profiles, dir) + } + } + return profiles +} + +// hasAnySource checks if dir contains at least one source file or directory. +func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool { + for _, candidates := range sources { + for _, sp := range candidates { + abs := filepath.Join(dir, sp.rel) + if _, err := os.Stat(abs); err == nil { + return true + } + } + } + return false +} + +// resolveSourcePaths checks which sources actually exist in profileDir. +// Candidates are tried in priority order; the first existing path wins. +func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath { + resolved := make(map[types.Category]resolvedPath) + for cat, candidates := range sources { + for _, sp := range candidates { + abs := filepath.Join(profileDir, sp.rel) + info, err := os.Stat(abs) + if err != nil { + continue + } + if sp.isDir == info.IsDir() { + resolved[cat] = resolvedPath{abs, sp.isDir} + break + } + } + } + return resolved } diff --git a/browser/firefox/firefox_new.go b/browser/firefox/firefox_new.go deleted file mode 100644 index 1a170fd..0000000 --- a/browser/firefox/firefox_new.go +++ /dev/null @@ -1,237 +0,0 @@ -package firefox - -import ( - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/moond4rk/hackbrowserdata/filemanager" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/fileutil" -) - -// Browser represents a single Firefox profile ready for extraction. -type Browser struct { - cfg types.BrowserConfig - profileDir string // absolute path to profile directory - sources map[types.Category][]sourcePath // Category → candidate paths (priority order) - sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path -} - -// NewBrowsers discovers Firefox profiles under cfg.UserDataDir and returns -// one Browser per profile. Firefox profile directories have random names -// (e.g. "97nszz88.default-release"); any subdirectory containing known -// data files is treated as a valid profile. -func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) { - profileDirs := discoverProfiles(cfg.UserDataDir, firefoxSources) - if len(profileDirs) == 0 { - return nil, nil - } - - var browsers []*Browser - for _, profileDir := range profileDirs { - sourcePaths := resolveSourcePaths(firefoxSources, profileDir) - if len(sourcePaths) == 0 { - continue - } - browsers = append(browsers, &Browser{ - cfg: cfg, - profileDir: profileDir, - sources: firefoxSources, - sourcePaths: sourcePaths, - }) - } - return browsers, nil -} - -func (b *Browser) BrowserName() string { return b.cfg.Name } -func (b *Browser) ProfileName() string { - if b.profileDir == "" { - return "" - } - return filepath.Base(b.profileDir) -} - -// Extract copies browser files to a temp directory, retrieves the master key, -// and extracts data for the requested categories. -func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) { - session, err := filemanager.NewSession() - if err != nil { - return nil, err - } - defer session.Cleanup() - - tempPaths := b.acquireFiles(session, categories) - - masterKey, err := b.getMasterKey(session, tempPaths) - if err != nil { - log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err) - } - - data := &types.BrowserData{} - for _, cat := range categories { - path, ok := tempPaths[cat] - if !ok { - continue - } - b.extractCategory(data, cat, masterKey, path) - } - return data, nil -} - -// acquireFiles copies source files to the session temp directory. -func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string { - tempPaths := make(map[types.Category]string) - for _, cat := range categories { - rp, ok := b.sourcePaths[cat] - if !ok { - continue - } - dst := filepath.Join(session.TempDir(), cat.String()) - if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil { - log.Debugf("acquire %s: %v", cat, err) - continue - } - tempPaths[cat] = dst - } - return tempPaths -} - -// getMasterKey retrieves the Firefox master encryption key from key4.db. -// The key is derived via NSS ASN1 PBE decryption (platform-agnostic). -// If logins.json was already acquired by acquireFiles, the derived key -// is validated by attempting to decrypt an actual login entry. -func (b *Browser) getMasterKey(session *filemanager.Session, tempPaths map[types.Category]string) ([]byte, error) { - key4Src := filepath.Join(b.profileDir, "key4.db") - if !fileutil.IsFileExists(key4Src) { - return nil, nil - } - key4Dst := filepath.Join(session.TempDir(), "key4.db") - if err := session.Acquire(key4Src, key4Dst, false); err != nil { - return nil, fmt.Errorf("acquire key4.db: %w", err) - } - - // logins.json is already acquired by acquireFiles as the Password source; - // reuse it for master key validation if available. - loginsPath := tempPaths[types.Password] - return retrieveMasterKey(key4Dst, loginsPath) -} - -// retrieveMasterKey reads key4.db and derives the master key using NSS. -// If loginsPath is non-empty, the derived key is validated against actual -// login data to ensure the correct candidate is selected. -func retrieveMasterKey(key4Path, loginsPath string) ([]byte, error) { - k4, err := readKey4DB(key4Path) - if err != nil { - return nil, err - } - - keys, err := k4.deriveKeys() - if err != nil { - return nil, err - } - if len(keys) == 0 { - return nil, errors.New("no valid master key candidates in key4.db") - } - - // No logins to validate against — return the first derived key. - if loginsPath == "" { - return keys[0], nil - } - - // Validate against actual login data. - if key := validateKeyWithLogins(keys, loginsPath); key != nil { - return key, nil - } - - return nil, fmt.Errorf("derived %d key(s) but none could decrypt logins", len(keys)) -} - -// extractCategory calls the appropriate extract function for a category. -func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) { - var err error - switch cat { - case types.Password: - data.Passwords, err = extractPasswords(masterKey, path) - case types.Cookie: - data.Cookies, err = extractCookies(path) - case types.History: - data.Histories, err = extractHistories(path) - case types.Download: - data.Downloads, err = extractDownloads(path) - case types.Bookmark: - data.Bookmarks, err = extractBookmarks(path) - case types.Extension: - data.Extensions, err = extractExtensions(path) - case types.LocalStorage: - data.LocalStorage, err = extractLocalStorage(path) - case types.CreditCard, types.SessionStorage: - // Firefox does not support CreditCard or SessionStorage extraction. - } - if err != nil { - log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err) - } -} - -// resolvedPath holds the absolute path and type for a discovered source. -type resolvedPath struct { - absPath string - isDir bool -} - -// discoverProfiles lists subdirectories of userDataDir that contain at least -// one known data source. Each such directory is a Firefox profile. -func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string { - entries, err := os.ReadDir(userDataDir) - if err != nil { - log.Debugf("read user data dir %s: %v", userDataDir, err) - return nil - } - - var profiles []string - for _, e := range entries { - if !e.IsDir() { - continue - } - dir := filepath.Join(userDataDir, e.Name()) - if hasAnySource(sources, dir) { - profiles = append(profiles, dir) - } - } - return profiles -} - -// hasAnySource checks if dir contains at least one source file or directory. -func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool { - for _, candidates := range sources { - for _, sp := range candidates { - abs := filepath.Join(dir, sp.rel) - if _, err := os.Stat(abs); err == nil { - return true - } - } - } - return false -} - -// resolveSourcePaths checks which sources actually exist in profileDir. -// Candidates are tried in priority order; the first existing path wins. -func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath { - resolved := make(map[types.Category]resolvedPath) - for cat, candidates := range sources { - for _, sp := range candidates { - abs := filepath.Join(profileDir, sp.rel) - info, err := os.Stat(abs) - if err != nil { - continue - } - if sp.isDir == info.IsDir() { - resolved[cat] = resolvedPath{abs, sp.isDir} - break - } - } - } - return resolved -} diff --git a/browser/firefox/firefox_new_test.go b/browser/firefox/firefox_test.go similarity index 100% rename from browser/firefox/firefox_new_test.go rename to browser/firefox/firefox_test.go diff --git a/browserdata/bookmark/bookmark.go b/browserdata/bookmark/bookmark.go deleted file mode 100644 index 1cc064f..0000000 --- a/browserdata/bookmark/bookmark.go +++ /dev/null @@ -1,159 +0,0 @@ -package bookmark - -import ( - "database/sql" - "os" - "sort" - "time" - - "github.com/tidwall/gjson" - _ "modernc.org/sqlite" // import sqlite3 driver - - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/fileutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumBookmark, func() extractor.Extractor { - return new(ChromiumBookmark) - }) - extractor.RegisterExtractor(types.FirefoxBookmark, func() extractor.Extractor { - return new(FirefoxBookmark) - }) -} - -type ChromiumBookmark []bookmark - -type bookmark struct { - ID int64 - Name string - Type string - URL string - DateAdded time.Time -} - -func (c *ChromiumBookmark) Extract(_ []byte) error { - bookmarks, err := fileutil.ReadFile(types.ChromiumBookmark.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.ChromiumBookmark.TempFilename()) - r := gjson.Parse(bookmarks) - if r.Exists() { - roots := r.Get("roots") - roots.ForEach(func(key, value gjson.Result) bool { - getBookmarkChildren(value, c) - return true - }) - } - - sort.Slice(*c, func(i, j int) bool { - return (*c)[i].DateAdded.After((*c)[j].DateAdded) - }) - return nil -} - -const ( - bookmarkID = "id" - bookmarkAdded = "date_added" - bookmarkURL = "url" - bookmarkName = "name" - bookmarkType = "type" - bookmarkChildren = "children" -) - -func getBookmarkChildren(value gjson.Result, w *ChromiumBookmark) (children gjson.Result) { - nodeType := value.Get(bookmarkType) - children = value.Get(bookmarkChildren) - - bm := bookmark{ - ID: value.Get(bookmarkID).Int(), - Name: value.Get(bookmarkName).String(), - URL: value.Get(bookmarkURL).String(), - DateAdded: typeutil.TimeEpoch(value.Get(bookmarkAdded).Int()), - } - if nodeType.Exists() { - bm.Type = nodeType.String() - *w = append(*w, bm) - if children.Exists() && children.IsArray() { - for _, v := range children.Array() { - children = getBookmarkChildren(v, w) - } - } - } - return children -} - -func (c *ChromiumBookmark) Name() string { - return "bookmark" -} - -func (c *ChromiumBookmark) Len() int { - return len(*c) -} - -type FirefoxBookmark []bookmark - -const ( - queryFirefoxBookMark = `SELECT id, url, type, dateAdded, title FROM (SELECT * FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id)` - closeJournalMode = `PRAGMA journal_mode=off` -) - -func (f *FirefoxBookmark) Extract(_ []byte) error { - db, err := sql.Open("sqlite", types.FirefoxBookmark.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.FirefoxBookmark.TempFilename()) - defer db.Close() - _, err = db.Exec(closeJournalMode) - if err != nil { - log.Debugf("close journal mode error: %v", err) - } - rows, err := db.Query(queryFirefoxBookMark) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - id, bt, dateAdded int64 - url string - title sql.NullString - ) - if err = rows.Scan(&id, &url, &bt, &dateAdded, &title); err != nil { - log.Debugf("scan bookmark error: %v", err) - } - *f = append(*f, bookmark{ - ID: id, - Name: title.String, - Type: linkType(bt), - URL: url, - DateAdded: typeutil.TimeStamp(dateAdded / 1000000), - }) - } - sort.Slice(*f, func(i, j int) bool { - return (*f)[i].DateAdded.After((*f)[j].DateAdded) - }) - return nil -} - -func (f *FirefoxBookmark) Name() string { - return "bookmark" -} - -func (f *FirefoxBookmark) Len() int { - return len(*f) -} - -func linkType(a int64) string { - switch a { - case 1: - return "url" - default: - return "folder" - } -} diff --git a/browserdata/browser_data.go b/browserdata/browser_data.go deleted file mode 100644 index d1d4a96..0000000 --- a/browserdata/browser_data.go +++ /dev/null @@ -1,18 +0,0 @@ -package browserdata - -import "github.com/moond4rk/hackbrowserdata/types" - -// Data holds all extracted data from one browser profile. -// Each field is a slice that may be nil (not supported) or empty (no data found). -// This struct will replace the current BrowserData once the refactoring is complete. -type Data struct { - Passwords []types.LoginEntry `json:"passwords,omitempty"` - Cookies []types.CookieEntry `json:"cookies,omitempty"` - Bookmarks []types.BookmarkEntry `json:"bookmarks,omitempty"` - Histories []types.HistoryEntry `json:"histories,omitempty"` - Downloads []types.DownloadEntry `json:"downloads,omitempty"` - CreditCards []types.CreditCardEntry `json:"credit_cards,omitempty"` - Extensions []types.ExtensionEntry `json:"extensions,omitempty"` - LocalStorage []types.StorageEntry `json:"local_storage,omitempty"` - SessionStorage []types.StorageEntry `json:"session_storage,omitempty"` -} diff --git a/browserdata/browserdata.go b/browserdata/browserdata.go deleted file mode 100644 index 646fb1e..0000000 --- a/browserdata/browserdata.go +++ /dev/null @@ -1,67 +0,0 @@ -package browserdata - -import ( - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/fileutil" -) - -type BrowserData struct { - extractors map[types.DataType]extractor.Extractor -} - -func New(items []types.DataType) *BrowserData { - bd := &BrowserData{ - extractors: make(map[types.DataType]extractor.Extractor), - } - bd.addExtractors(items) - return bd -} - -func (d *BrowserData) Recovery(masterKey []byte) error { - for _, source := range d.extractors { - if err := source.Extract(masterKey); err != nil { - log.Debugf("parse %s error: %v", source.Name(), err) - continue - } - } - return nil -} - -func (d *BrowserData) Output(dir, browserName, flag string) { - output := newOutPutter(flag) - - for _, source := range d.extractors { - if source.Len() == 0 { - // if the length of the export data is 0, then it is not necessary to output - continue - } - filename := fileutil.Filename(browserName, source.Name(), output.Ext()) - - f, err := output.CreateFile(dir, filename) - if err != nil { - log.Debugf("create file %s error: %v", filename, err) - continue - } - if err := output.Write(source, f); err != nil { - log.Debugf("write to file %s error: %v", filename, err) - continue - } - if err := f.Close(); err != nil { - log.Debugf("close file %s error: %v", filename, err) - continue - } - log.Warnf("export success: %s", filename) - } -} - -func (d *BrowserData) addExtractors(items []types.DataType) { - for _, itemType := range items { - if source := extractor.CreateExtractor(itemType); source != nil { - d.extractors[itemType] = source - } else { - log.Debugf("source not found: %s", itemType) - } - } -} diff --git a/browserdata/cookie/cookie.go b/browserdata/cookie/cookie.go deleted file mode 100644 index 6d80445..0000000 --- a/browserdata/cookie/cookie.go +++ /dev/null @@ -1,165 +0,0 @@ -package cookie - -import ( - "database/sql" - "os" - "sort" - "time" - - // import sqlite3 driver - _ "modernc.org/sqlite" - - "github.com/moond4rk/hackbrowserdata/crypto" - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumCookie, func() extractor.Extractor { - return new(ChromiumCookie) - }) - extractor.RegisterExtractor(types.FirefoxCookie, func() extractor.Extractor { - return new(FirefoxCookie) - }) -} - -type ChromiumCookie []cookie - -type cookie struct { - Host string - Path string - KeyName string - encryptValue []byte - Value string - IsSecure bool - IsHTTPOnly bool - HasExpire bool - IsPersistent bool - CreateDate time.Time - ExpireDate time.Time -} - -const ( - queryChromiumCookie = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, has_expires, is_persistent FROM cookies` -) - -func (c *ChromiumCookie) Extract(masterKey []byte) error { - db, err := sql.Open("sqlite", types.ChromiumCookie.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.ChromiumCookie.TempFilename()) - defer db.Close() - rows, err := db.Query(queryChromiumCookie) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - key, host, path string - isSecure, isHTTPOnly, hasExpire, isPersistent int - createDate, expireDate int64 - value, encryptValue []byte - ) - if err = rows.Scan(&key, &encryptValue, &host, &path, &createDate, &expireDate, &isSecure, &isHTTPOnly, &hasExpire, &isPersistent); err != nil { - log.Debugf("scan chromium cookie error: %v", err) - } - - cookie := cookie{ - KeyName: key, - Host: host, - Path: path, - encryptValue: encryptValue, - IsSecure: typeutil.IntToBool(isSecure), - IsHTTPOnly: typeutil.IntToBool(isHTTPOnly), - HasExpire: typeutil.IntToBool(hasExpire), - IsPersistent: typeutil.IntToBool(isPersistent), - CreateDate: typeutil.TimeEpoch(createDate), - ExpireDate: typeutil.TimeEpoch(expireDate), - } - - if len(encryptValue) > 0 { - value, err = crypto.DecryptWithDPAPI(encryptValue) - if err != nil { - value, err = crypto.DecryptWithChromium(masterKey, encryptValue) - if err != nil { - log.Debugf("decrypt chromium cookie error: %v", err) - } else if len(value) > 32 { - // https://gist.github.com/kosh04/36cf6023fb75b516451ce933b9db2207?permalink_comment_id=5291243#gistcomment-5291243 - value = value[32:] - } - } - } - cookie.Value = string(value) - *c = append(*c, cookie) - } - sort.Slice(*c, func(i, j int) bool { - return (*c)[i].CreateDate.After((*c)[j].CreateDate) - }) - return nil -} - -func (c *ChromiumCookie) Name() string { - return "cookie" -} - -func (c *ChromiumCookie) Len() int { - return len(*c) -} - -type FirefoxCookie []cookie - -const ( - queryFirefoxCookie = `SELECT name, value, host, path, creationTime, expiry, isSecure, isHttpOnly FROM moz_cookies` -) - -func (f *FirefoxCookie) Extract(_ []byte) error { - db, err := sql.Open("sqlite", types.FirefoxCookie.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.FirefoxCookie.TempFilename()) - defer db.Close() - - rows, err := db.Query(queryFirefoxCookie) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - name, value, host, path string - isSecure, isHTTPOnly int - creationTime, expiry int64 - ) - if err = rows.Scan(&name, &value, &host, &path, &creationTime, &expiry, &isSecure, &isHTTPOnly); err != nil { - log.Debugf("scan firefox cookie error: %v", err) - } - *f = append(*f, cookie{ - KeyName: name, - Host: host, - Path: path, - IsSecure: typeutil.IntToBool(isSecure), - IsHTTPOnly: typeutil.IntToBool(isHTTPOnly), - CreateDate: typeutil.TimeStamp(creationTime / 1000000), - ExpireDate: typeutil.TimeStamp(expiry), - Value: value, - }) - } - - sort.Slice(*f, func(i, j int) bool { - return (*f)[i].CreateDate.After((*f)[j].CreateDate) - }) - return nil -} - -func (f *FirefoxCookie) Name() string { - return "cookie" -} - -func (f *FirefoxCookie) Len() int { - return len(*f) -} diff --git a/browserdata/creditcard/creditcard.go b/browserdata/creditcard/creditcard.go deleted file mode 100644 index 3f1fae7..0000000 --- a/browserdata/creditcard/creditcard.go +++ /dev/null @@ -1,147 +0,0 @@ -package creditcard - -import ( - "database/sql" - "os" - - // import sqlite3 driver - _ "modernc.org/sqlite" - - "github.com/moond4rk/hackbrowserdata/crypto" - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumCreditCard, func() extractor.Extractor { - return new(ChromiumCreditCard) - }) - extractor.RegisterExtractor(types.YandexCreditCard, func() extractor.Extractor { - return new(YandexCreditCard) - }) -} - -type ChromiumCreditCard []card - -type card struct { - GUID string - Name string - ExpirationYear string - ExpirationMonth string - CardNumber string - Address string - NickName string -} - -const ( - queryChromiumCredit = `SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted, billing_address_id, nickname FROM credit_cards` -) - -func (c *ChromiumCreditCard) Extract(masterKey []byte) error { - db, err := sql.Open("sqlite", types.ChromiumCreditCard.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.ChromiumCreditCard.TempFilename()) - defer db.Close() - - rows, err := db.Query(queryChromiumCredit) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - name, month, year, guid, address, nickname string - value, encryptValue []byte - ) - if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil { - log.Debugf("scan chromium credit card error: %v", err) - } - ccInfo := card{ - GUID: guid, - Name: name, - ExpirationMonth: month, - ExpirationYear: year, - Address: address, - NickName: nickname, - } - if len(encryptValue) > 0 { - if len(masterKey) == 0 { - value, err = crypto.DecryptWithDPAPI(encryptValue) - } else { - value, err = crypto.DecryptWithChromium(masterKey, encryptValue) - } - if err != nil { - log.Debugf("decrypt chromium credit card error: %v", err) - } - } - - ccInfo.CardNumber = string(value) - *c = append(*c, ccInfo) - } - return nil -} - -func (c *ChromiumCreditCard) Name() string { - return "creditcard" -} - -func (c *ChromiumCreditCard) Len() int { - return len(*c) -} - -type YandexCreditCard []card - -func (c *YandexCreditCard) Extract(masterKey []byte) error { - db, err := sql.Open("sqlite", types.YandexCreditCard.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.YandexCreditCard.TempFilename()) - defer db.Close() - rows, err := db.Query(queryChromiumCredit) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - name, month, year, guid, address, nickname string - value, encryptValue []byte - ) - if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil { - log.Debugf("scan chromium credit card error: %v", err) - } - ccInfo := card{ - GUID: guid, - Name: name, - ExpirationMonth: month, - ExpirationYear: year, - Address: address, - NickName: nickname, - } - if len(encryptValue) > 0 { - if len(masterKey) == 0 { - value, err = crypto.DecryptWithDPAPI(encryptValue) - } else { - value, err = crypto.DecryptWithChromium(masterKey, encryptValue) - } - if err != nil { - log.Debugf("decrypt chromium credit card error: %v", err) - } - } - ccInfo.CardNumber = string(value) - *c = append(*c, ccInfo) - } - return nil -} - -func (c *YandexCreditCard) Name() string { - return "creditcard" -} - -func (c *YandexCreditCard) Len() int { - return len(*c) -} diff --git a/browserdata/download/download.go b/browserdata/download/download.go deleted file mode 100644 index 22b9eec..0000000 --- a/browserdata/download/download.go +++ /dev/null @@ -1,146 +0,0 @@ -package download - -import ( - "database/sql" - "os" - "sort" - "strings" - "time" - - "github.com/tidwall/gjson" - _ "modernc.org/sqlite" // import sqlite3 driver - - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumDownload, func() extractor.Extractor { - return new(ChromiumDownload) - }) - extractor.RegisterExtractor(types.FirefoxDownload, func() extractor.Extractor { - return new(FirefoxDownload) - }) -} - -type ChromiumDownload []download - -type download struct { - TargetPath string - URL string - TotalBytes int64 - StartTime time.Time - EndTime time.Time - MimeType string -} - -const ( - queryChromiumDownload = `SELECT target_path, tab_url, total_bytes, start_time, end_time, mime_type FROM downloads` -) - -func (c *ChromiumDownload) Extract(_ []byte) error { - db, err := sql.Open("sqlite", types.ChromiumDownload.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.ChromiumDownload.TempFilename()) - defer db.Close() - rows, err := db.Query(queryChromiumDownload) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - targetPath, tabURL, mimeType string - totalBytes, startTime, endTime int64 - ) - if err := rows.Scan(&targetPath, &tabURL, &totalBytes, &startTime, &endTime, &mimeType); err != nil { - log.Warnf("scan chromium download error: %v", err) - } - data := download{ - TargetPath: targetPath, - URL: tabURL, - TotalBytes: totalBytes, - StartTime: typeutil.TimeEpoch(startTime), - EndTime: typeutil.TimeEpoch(endTime), - MimeType: mimeType, - } - *c = append(*c, data) - } - sort.Slice(*c, func(i, j int) bool { - return (*c)[i].TotalBytes > (*c)[j].TotalBytes - }) - return nil -} - -func (c *ChromiumDownload) Name() string { - return "download" -} - -func (c *ChromiumDownload) Len() int { - return len(*c) -} - -type FirefoxDownload []download - -const ( - queryFirefoxDownload = `SELECT place_id, GROUP_CONCAT(content), url, dateAdded FROM (SELECT * FROM moz_annos INNER JOIN moz_places ON moz_annos.place_id=moz_places.id) t GROUP BY place_id` - closeJournalMode = `PRAGMA journal_mode=off` -) - -func (f *FirefoxDownload) Extract(_ []byte) error { - db, err := sql.Open("sqlite", types.FirefoxDownload.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.FirefoxDownload.TempFilename()) - defer db.Close() - - _, err = db.Exec(closeJournalMode) - if err != nil { - log.Debugf("close journal mode error: %v", err) - } - rows, err := db.Query(queryFirefoxDownload) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - content, url string - placeID, dateAdded int64 - ) - if err = rows.Scan(&placeID, &content, &url, &dateAdded); err != nil { - log.Warnf("scan firefox download error: %v", err) - } - contentList := strings.Split(content, ",{") - if len(contentList) > 1 { - path := contentList[0] - json := "{" + contentList[1] - endTime := gjson.Get(json, "endTime") - fileSize := gjson.Get(json, "fileSize") - *f = append(*f, download{ - TargetPath: path, - URL: url, - TotalBytes: fileSize.Int(), - StartTime: typeutil.TimeStamp(dateAdded / 1000000), - EndTime: typeutil.TimeStamp(endTime.Int() / 1000), - }) - } - } - sort.Slice(*f, func(i, j int) bool { - return (*f)[i].TotalBytes < (*f)[j].TotalBytes - }) - return nil -} - -func (f *FirefoxDownload) Name() string { - return "download" -} - -func (f *FirefoxDownload) Len() int { - return len(*f) -} diff --git a/browserdata/extension/extension.go b/browserdata/extension/extension.go deleted file mode 100644 index 7337900..0000000 --- a/browserdata/extension/extension.go +++ /dev/null @@ -1,187 +0,0 @@ -package extension - -import ( - "fmt" - "os" - "strings" - - "github.com/tidwall/gjson" - "golang.org/x/text/language" - - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/fileutil" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumExtension, func() extractor.Extractor { - return new(ChromiumExtension) - }) - extractor.RegisterExtractor(types.FirefoxExtension, func() extractor.Extractor { - return new(FirefoxExtension) - }) -} - -type ChromiumExtension []*extension - -type extension struct { - ID string - URL string - Enabled bool - Name string - Description string - Version string - HomepageURL string -} - -func (c *ChromiumExtension) Extract(_ []byte) error { - extensionFile, err := fileutil.ReadFile(types.ChromiumExtension.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.ChromiumExtension.TempFilename()) - - result, err := parseChromiumExtensions(extensionFile) - if err != nil { - return err - } - *c = result - return nil -} - -func parseChromiumExtensions(content string) ([]*extension, error) { - settingKeys := []string{ - "settings.extensions", - "settings.settings", - "extensions.settings", - } - var settings gjson.Result - for _, key := range settingKeys { - settings = gjson.Parse(content).Get(key) - if settings.Exists() { - break - } - } - if !settings.Exists() { - return nil, fmt.Errorf("cannot find extensions in settings") - } - var c []*extension - - settings.ForEach(func(id, ext gjson.Result) bool { - location := ext.Get("location") - if !location.Exists() { - return true - } - switch location.Int() { - case 5, 10: // https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/mojom/manifest.mojom - return true - } - // https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/disable_reason.h - enabled := !ext.Get("disable_reasons").Exists() - b := ext.Get("manifest") - if !b.Exists() { - c = append(c, &extension{ - ID: id.String(), - Enabled: enabled, - Name: ext.Get("path").String(), - }) - return true - } - c = append(c, &extension{ - ID: id.String(), - URL: getChromiumExtURL(id.String(), b.Get("update_url").String()), - Enabled: enabled, - Name: b.Get("name").String(), - Description: b.Get("description").String(), - Version: b.Get("version").String(), - HomepageURL: b.Get("homepage_url").String(), - }) - return true - }) - - return c, nil -} - -func getChromiumExtURL(id, updateURL string) string { - if strings.HasSuffix(updateURL, "clients2.google.com/service/update2/crx") { - return "https://chrome.google.com/webstore/detail/" + id - } else if strings.HasSuffix(updateURL, "edge.microsoft.com/extensionwebstorebase/v1/crx") { - return "https://microsoftedge.microsoft.com/addons/detail/" + id - } - return "" -} - -func (c *ChromiumExtension) Name() string { - return "extension" -} - -func (c *ChromiumExtension) Len() int { - return len(*c) -} - -type FirefoxExtension []*extension - -var lang = language.Und - -func (f *FirefoxExtension) Extract(_ []byte) error { - s, err := fileutil.ReadFile(types.FirefoxExtension.TempFilename()) - if err != nil { - return err - } - _ = os.Remove(types.FirefoxExtension.TempFilename()) - j := gjson.Parse(s) - for _, v := range j.Get("addons").Array() { - // https://searchfox.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIDatabase.jsm#157 - if v.Get("location").String() != "app-profile" { - continue - } - - if lang != language.Und { - locale := findFirefoxLocale(v.Get("locales").Array(), lang) - *f = append(*f, &extension{ - ID: v.Get("id").String(), - Enabled: v.Get("active").Bool(), - Name: locale.Get("name").String(), - Description: locale.Get("description").String(), - Version: v.Get("version").String(), - HomepageURL: locale.Get("homepageURL").String(), - }) - continue - } - - *f = append(*f, &extension{ - ID: v.Get("id").String(), - Enabled: v.Get("active").Bool(), - Name: v.Get("defaultLocale.name").String(), - Description: v.Get("defaultLocale.description").String(), - Version: v.Get("version").String(), - HomepageURL: v.Get("defaultLocale.homepageURL").String(), - }) - } - return nil -} - -func findFirefoxLocale(locales []gjson.Result, targetLang language.Tag) gjson.Result { - tags := make([]language.Tag, 0, len(locales)) - indices := make([]int, 0, len(locales)) - for i, locale := range locales { - for _, tagStr := range locale.Get("locales").Array() { - tag, _ := language.Parse(tagStr.String()) - if tag == language.Und { - continue - } - tags = append(tags, tag) - indices = append(indices, i) - } - } - _, tagIndex, _ := language.NewMatcher(tags).Match(targetLang) - return locales[indices[tagIndex]] -} - -func (f *FirefoxExtension) Name() string { - return "extension" -} - -func (f *FirefoxExtension) Len() int { - return len(*f) -} diff --git a/browserdata/history/history.go b/browserdata/history/history.go deleted file mode 100644 index 5460263..0000000 --- a/browserdata/history/history.go +++ /dev/null @@ -1,137 +0,0 @@ -package history - -import ( - "database/sql" - "os" - "sort" - "time" - - // import sqlite3 driver - _ "modernc.org/sqlite" - - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumHistory, func() extractor.Extractor { - return new(ChromiumHistory) - }) - extractor.RegisterExtractor(types.FirefoxHistory, func() extractor.Extractor { - return new(FirefoxHistory) - }) -} - -type ChromiumHistory []history - -type history struct { - Title string - URL string - VisitCount int - LastVisitTime time.Time -} - -const ( - queryChromiumHistory = `SELECT url, title, visit_count, last_visit_time FROM urls` -) - -func (c *ChromiumHistory) Extract(_ []byte) error { - db, err := sql.Open("sqlite", types.ChromiumHistory.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.ChromiumHistory.TempFilename()) - defer db.Close() - - rows, err := db.Query(queryChromiumHistory) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - url, title string - visitCount int - lastVisitTime int64 - ) - if err := rows.Scan(&url, &title, &visitCount, &lastVisitTime); err != nil { - log.Warnf("scan chromium history error: %v", err) - } - data := history{ - URL: url, - Title: title, - VisitCount: visitCount, - LastVisitTime: typeutil.TimeEpoch(lastVisitTime), - } - *c = append(*c, data) - } - sort.Slice(*c, func(i, j int) bool { - return (*c)[i].VisitCount > (*c)[j].VisitCount - }) - return nil -} - -func (c *ChromiumHistory) Name() string { - return "history" -} - -func (c *ChromiumHistory) Len() int { - return len(*c) -} - -type FirefoxHistory []history - -const ( - queryFirefoxHistory = `SELECT id, url, COALESCE(last_visit_date, 0), COALESCE(title, ''), visit_count FROM moz_places` - closeJournalMode = `PRAGMA journal_mode=off` -) - -func (f *FirefoxHistory) Extract(_ []byte) error { - db, err := sql.Open("sqlite", types.FirefoxHistory.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.FirefoxHistory.TempFilename()) - defer db.Close() - - _, err = db.Exec(closeJournalMode) - if err != nil { - return err - } - defer db.Close() - rows, err := db.Query(queryFirefoxHistory) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var ( - id, visitDate int64 - url, title string - visitCount int - ) - if err = rows.Scan(&id, &url, &visitDate, &title, &visitCount); err != nil { - log.Debugf("scan firefox history error: %v", err) - } - *f = append(*f, history{ - Title: title, - URL: url, - VisitCount: visitCount, - LastVisitTime: typeutil.TimeStamp(visitDate / 1000000), - }) - } - sort.Slice(*f, func(i, j int) bool { - return (*f)[i].VisitCount < (*f)[j].VisitCount - }) - return nil -} - -func (f *FirefoxHistory) Name() string { - return "history" -} - -func (f *FirefoxHistory) Len() int { - return len(*f) -} diff --git a/browserdata/imports.go b/browserdata/imports.go deleted file mode 100644 index 91c4718..0000000 --- a/browserdata/imports.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package browserdata is responsible for initializing all the necessary -// components that handle different types of browser data extraction. -// This file, imports.go, is specifically used to import various data -// handler packages to ensure their initialization logic is executed. -// These imports are crucial as they trigger the `init()` functions -// within each package, which typically handle registration of their -// specific data handlers to a central registry. -package browserdata - -import ( - _ "github.com/moond4rk/hackbrowserdata/browserdata/bookmark" - _ "github.com/moond4rk/hackbrowserdata/browserdata/cookie" - _ "github.com/moond4rk/hackbrowserdata/browserdata/creditcard" - _ "github.com/moond4rk/hackbrowserdata/browserdata/download" - _ "github.com/moond4rk/hackbrowserdata/browserdata/extension" - _ "github.com/moond4rk/hackbrowserdata/browserdata/history" - _ "github.com/moond4rk/hackbrowserdata/browserdata/localstorage" - _ "github.com/moond4rk/hackbrowserdata/browserdata/password" - _ "github.com/moond4rk/hackbrowserdata/browserdata/sessionstorage" -) diff --git a/browserdata/localstorage/localstorage.go b/browserdata/localstorage/localstorage.go deleted file mode 100644 index ebb99df..0000000 --- a/browserdata/localstorage/localstorage.go +++ /dev/null @@ -1,234 +0,0 @@ -package localstorage - -import ( - "bytes" - "database/sql" - "fmt" - "os" - "strings" - - "github.com/syndtr/goleveldb/leveldb" - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" - - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumLocalStorage, func() extractor.Extractor { - return new(ChromiumLocalStorage) - }) - extractor.RegisterExtractor(types.FirefoxLocalStorage, func() extractor.Extractor { - return new(FirefoxLocalStorage) - }) -} - -type ChromiumLocalStorage []storage - -type storage struct { - IsMeta bool - URL string - Key string - Value string -} - -const maxLocalStorageValueLength = 1024 * 2 - -const ( - chromiumLocalStorageVersionKey = "VERSION" - chromiumLocalStorageMetaPrefix = "META:" - chromiumLocalStorageMetaAccessKey = "METAACCESS:" - chromiumLocalStorageDataPrefix = '_' - chromiumStringUTF16Format = 0 - chromiumStringLatin1Format = 1 -) - -func (c *ChromiumLocalStorage) Extract(_ []byte) error { - entries, err := extractChromiumLocalStorage(types.ChromiumLocalStorage.TempFilename()) - if err != nil { - return err - } - defer os.RemoveAll(types.ChromiumLocalStorage.TempFilename()) - *c = append(*c, entries...) - return nil -} - -func (c *ChromiumLocalStorage) Name() string { - return "localStorage" -} - -func (c *ChromiumLocalStorage) Len() int { - return len(*c) -} - -func extractChromiumLocalStorage(path string) (ChromiumLocalStorage, error) { - db, err := leveldb.OpenFile(path, nil) - if err != nil { - return nil, err - } - defer db.Close() - - var entries ChromiumLocalStorage - iter := db.NewIterator(nil, nil) - defer iter.Release() - - for iter.Next() { - entry, ok := parseChromiumLocalStorageEntry(iter.Key(), iter.Value()) - if !ok { - continue - } - entries = append(entries, entry) - } - return entries, iter.Error() -} - -func parseChromiumLocalStorageEntry(key, value []byte) (storage, bool) { - switch { - case bytes.Equal(key, []byte(chromiumLocalStorageVersionKey)): - return storage{}, false - case bytes.HasPrefix(key, []byte(chromiumLocalStorageMetaAccessKey)): - return storage{ - IsMeta: true, - URL: string(bytes.TrimPrefix(key, []byte(chromiumLocalStorageMetaAccessKey))), - Value: fmt.Sprintf("meta data, value bytes is %v", value), - }, true - case bytes.HasPrefix(key, []byte(chromiumLocalStorageMetaPrefix)): - return storage{ - IsMeta: true, - URL: string(bytes.TrimPrefix(key, []byte(chromiumLocalStorageMetaPrefix))), - Value: fmt.Sprintf("meta data, value bytes is %v", value), - }, true - case len(key) > 0 && key[0] == chromiumLocalStorageDataPrefix: - return parseChromiumLocalStorageDataEntry(key[1:], value), true - default: - return storage{}, false - } -} - -func parseChromiumLocalStorageDataEntry(key, value []byte) storage { - entry := storage{ - Value: decodeChromiumLocalStorageValue(value), - } - - separator := bytes.IndexByte(key, 0) - if separator < 0 { - entry.Key = "unsupported chromium localStorage key encoding: missing origin separator" - return entry - } - - entry.URL = string(key[:separator]) - scriptKey, err := decodeChromiumString(key[separator+1:]) - if err != nil { - entry.Key = fmt.Sprintf("unsupported chromium localStorage key encoding: %v", err) - return entry - } - entry.Key = scriptKey - return entry -} - -func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) { - r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source) - return r, err -} - -func decodeChromiumString(b []byte) (string, error) { - if len(b) == 0 { - return "", fmt.Errorf("empty chromium string") - } - - switch b[0] { - case chromiumStringLatin1Format: - return string(b[1:]), nil - case chromiumStringUTF16Format: - if len(b) == 1 { - return "", nil - } - if (len(b)-1)%2 != 0 { - return "", fmt.Errorf("invalid UTF-16 byte length %d", len(b)-1) - } - value, err := convertUTF16toUTF8(b[1:], unicode.LittleEndian) - if err != nil { - return "", err - } - return string(value), nil - default: - return "", fmt.Errorf("unknown chromium string format 0x%02x", b[0]) - } -} - -func decodeChromiumLocalStorageValue(value []byte) string { - if len(value) >= maxLocalStorageValueLength { - return fmt.Sprintf( - "value is too long, length is %d, supported max length is %d", - len(value), - maxLocalStorageValueLength, - ) - } - - decoded, err := decodeChromiumString(value) - if err != nil { - return fmt.Sprintf("unsupported chromium localStorage value encoding: %v", err) - } - return decoded -} - -type FirefoxLocalStorage []storage - -const ( - queryLocalStorage = `SELECT originKey, key, value FROM webappsstore2` - closeJournalMode = `PRAGMA journal_mode=off` -) - -func (f *FirefoxLocalStorage) Extract(_ []byte) error { - db, err := sql.Open("sqlite", types.FirefoxLocalStorage.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.FirefoxLocalStorage.TempFilename()) - defer db.Close() - - _, err = db.Exec(closeJournalMode) - if err != nil { - log.Debugf("close journal mode error: %v", err) - } - rows, err := db.Query(queryLocalStorage) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var originKey, key, value string - if err = rows.Scan(&originKey, &key, &value); err != nil { - log.Debugf("scan firefox local storage error: %v", err) - } - s := new(storage) - s.fillFirefox(originKey, key, value) - *f = append(*f, *s) - } - return nil -} - -func (s *storage) fillFirefox(originKey, key, value string) { - // originKey = moc.buhtig.:https:443 - p := strings.Split(originKey, ":") - h := typeutil.Reverse([]byte(p[0])) - if bytes.HasPrefix(h, []byte(".")) { - h = h[1:] - } - if len(p) == 3 { - s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2]) - } - s.Key = key - s.Value = value -} - -func (f *FirefoxLocalStorage) Name() string { - return "localStorage" -} - -func (f *FirefoxLocalStorage) Len() int { - return len(*f) -} diff --git a/browserdata/localstorage/localstorage_test.go b/browserdata/localstorage/localstorage_test.go deleted file mode 100644 index f328f3a..0000000 --- a/browserdata/localstorage/localstorage_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package localstorage - -import ( - "encoding/binary" - "testing" - "unicode/utf16" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/syndtr/goleveldb/leveldb" -) - -func TestDecodeChromiumString(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - input []byte - want string - wantErr string - }{ - { - name: "latin1", - input: encodeChromiumLatin1("abc123"), - want: "abc123", - }, - { - name: "utf16le", - input: encodeChromiumUTF16("飞连"), - want: "飞连", - }, - { - name: "unknown format", - input: []byte{2, 'x'}, - wantErr: "unknown chromium string format", - }, - { - name: "invalid utf16 byte length", - input: []byte{chromiumStringUTF16Format, 0x61}, - wantErr: "invalid UTF-16 byte length", - }, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - got, err := decodeChromiumString(tc.input) - if tc.wantErr != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.wantErr) - return - } - - require.NoError(t, err) - assert.Equal(t, tc.want, got) - }) - } -} - -func TestParseChromiumLocalStorageEntry(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - key []byte - value []byte - wantParsed bool - wantMeta bool - wantURL string - wantKey string - wantValue string - wantContains string - }{ - { - name: "skip version key", - key: []byte(chromiumLocalStorageVersionKey), - wantParsed: false, - }, - { - name: "meta key", - key: []byte(chromiumLocalStorageMetaPrefix + "https://example.com"), - value: []byte{0x08, 0x96, 0x01}, - wantParsed: true, - wantMeta: true, - wantURL: "https://example.com", - wantValue: "meta data, value bytes is [8 150 1]", - }, - { - name: "meta access key", - key: []byte(chromiumLocalStorageMetaAccessKey + "https://example.com"), - value: []byte{0x10, 0x20}, - wantParsed: true, - wantMeta: true, - wantURL: "https://example.com", - wantValue: "meta data, value bytes is [16 32]", - }, - { - name: "latin1 business key", - key: append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...), - value: encodeChromiumLatin1("abc123"), - wantParsed: true, - wantURL: "https://example.com", - wantKey: "token", - wantValue: "abc123", - }, - { - name: "utf16 business key", - key: append([]byte("_https://example.com\x00"), encodeChromiumUTF16("飞连")...), - value: encodeChromiumUTF16("终端安全"), - wantParsed: true, - wantURL: "https://example.com", - wantKey: "飞连", - wantValue: "终端安全", - }, - { - name: "unsupported business key format", - key: append([]byte("_https://example.com\x00"), []byte{2, 'x'}...), - value: encodeChromiumLatin1("abc123"), - wantParsed: true, - wantURL: "https://example.com", - wantContains: "unsupported chromium localStorage key encoding", - wantValue: "abc123", - }, - { - name: "missing origin separator", - key: append([]byte("_https://example.com"), encodeChromiumLatin1("token")...), - value: encodeChromiumLatin1("abc123"), - wantParsed: true, - wantContains: "missing origin separator", - wantValue: "abc123", - }, - { - name: "unsupported value format", - key: append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...), - value: []byte{2, 'x'}, - wantParsed: true, - wantURL: "https://example.com", - wantKey: "token", - wantValue: "unsupported chromium localStorage value encoding: unknown chromium string format 0x02", - }, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - got, parsed := parseChromiumLocalStorageEntry(tc.key, tc.value) - - assert.Equal(t, tc.wantParsed, parsed) - assert.Equal(t, tc.wantMeta, got.IsMeta) - assert.Equal(t, tc.wantURL, got.URL) - assert.Equal(t, tc.wantValue, got.Value) - if tc.wantContains != "" { - assert.Contains(t, got.Key, tc.wantContains) - return - } - assert.Equal(t, tc.wantKey, got.Key) - }) - } -} - -func TestExtractChromiumLocalStorage(t *testing.T) { - dir := t.TempDir() - db, err := leveldb.OpenFile(dir, nil) - require.NoError(t, err) - - testEntries := map[string][]byte{ - chromiumLocalStorageVersionKey: []byte("1"), - chromiumLocalStorageMetaPrefix + "https://example.com": {0x08, 0x96, 0x01}, - chromiumLocalStorageMetaAccessKey + "https://example.com": {0x10, 0x20}, - string(append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...)): encodeChromiumLatin1("abc123"), - string(append([]byte("_https://example.com\x00"), encodeChromiumUTF16("飞连")...)): encodeChromiumUTF16("终端安全"), - } - - for key, value := range testEntries { - require.NoError(t, db.Put([]byte(key), value, nil)) - } - require.NoError(t, db.Close()) - - got, err := extractChromiumLocalStorage(dir) - require.NoError(t, err) - require.Len(t, got, 4) - - metaCount := 0 - valuesByKey := make(map[string]string) - for _, entry := range got { - if entry.IsMeta { - metaCount++ - assert.Equal(t, "https://example.com", entry.URL) - assert.Contains(t, entry.Value, "meta data, value bytes is") - continue - } - valuesByKey[entry.Key] = entry.Value - assert.Equal(t, "https://example.com", entry.URL) - } - - assert.Equal(t, 2, metaCount) - assert.Equal(t, "abc123", valuesByKey["token"]) - assert.Equal(t, "终端安全", valuesByKey["飞连"]) -} - -func encodeChromiumLatin1(s string) []byte { - return append([]byte{chromiumStringLatin1Format}, []byte(s)...) -} - -func encodeChromiumUTF16(s string) []byte { - encoded := utf16.Encode([]rune(s)) - result := make([]byte, 1, 1+len(encoded)*2) - result[0] = chromiumStringUTF16Format - for _, r := range encoded { - var raw [2]byte - binary.LittleEndian.PutUint16(raw[:], r) - result = append(result, raw[:]...) - } - return result -} diff --git a/browserdata/outputter.go b/browserdata/outputter.go deleted file mode 100644 index 4e78001..0000000 --- a/browserdata/outputter.go +++ /dev/null @@ -1,79 +0,0 @@ -package browserdata - -import ( - "encoding/csv" - "encoding/json" - "errors" - "io" - "os" - "path/filepath" - - "github.com/gocarina/gocsv" - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" - - "github.com/moond4rk/hackbrowserdata/extractor" -) - -type outPutter struct { - json bool - csv bool -} - -func newOutPutter(flag string) *outPutter { - o := &outPutter{} - if flag == "json" { - o.json = true - } else { - o.csv = true - } - return o -} - -func (o *outPutter) Write(data extractor.Extractor, writer io.Writer) error { - switch o.json { - case true: - encoder := json.NewEncoder(writer) - encoder.SetIndent("", " ") - encoder.SetEscapeHTML(false) - return encoder.Encode(data) - default: - gocsv.SetCSVWriter(func(w io.Writer) *gocsv.SafeCSVWriter { - writer := csv.NewWriter(transform.NewWriter(w, unicode.UTF8BOM.NewEncoder())) - writer.Comma = ',' - return gocsv.NewSafeCSVWriter(writer) - }) - return gocsv.Marshal(data, writer) - } -} - -func (o *outPutter) CreateFile(dir, filename string) (*os.File, error) { - if filename == "" { - return nil, errors.New("empty filename") - } - - if dir != "" { - if _, err := os.Stat(dir); os.IsNotExist(err) { - err := os.MkdirAll(dir, 0o750) - if err != nil { - return nil, err - } - } - } - - var file *os.File - var err error - p := filepath.Join(dir, filename) - file, err = os.OpenFile(filepath.Clean(p), os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) - if err != nil { - return nil, err - } - return file, nil -} - -func (o *outPutter) Ext() string { - if o.json { - return "json" - } - return "csv" -} diff --git a/browserdata/outputter_test.go b/browserdata/outputter_test.go deleted file mode 100644 index 6767c48..0000000 --- a/browserdata/outputter_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package browserdata - -import ( - "os" - "testing" -) - -func TestNewOutPutter(t *testing.T) { - t.Parallel() - out := newOutPutter("json") - if out == nil { - t.Error("New() returned nil") - } - f, err := out.CreateFile("results", "test.json") - if err != nil { - t.Error("CreateFile() returned an error", err) - } - defer os.RemoveAll("results") - err = out.Write(nil, f) - if err != nil { - t.Error("Write() returned an error", err) - } -} diff --git a/browserdata/password/password.go b/browserdata/password/password.go deleted file mode 100644 index 7fc8117..0000000 --- a/browserdata/password/password.go +++ /dev/null @@ -1,259 +0,0 @@ -package password - -import ( - "database/sql" - "encoding/base64" - "os" - "sort" - "time" - - "github.com/tidwall/gjson" - _ "modernc.org/sqlite" // import sqlite3 driver - - "github.com/moond4rk/hackbrowserdata/crypto" - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumPassword, func() extractor.Extractor { - return new(ChromiumPassword) - }) - extractor.RegisterExtractor(types.YandexPassword, func() extractor.Extractor { - return new(YandexPassword) - }) - extractor.RegisterExtractor(types.FirefoxPassword, func() extractor.Extractor { - return new(FirefoxPassword) - }) -} - -type ChromiumPassword []loginData - -type loginData struct { - UserName string - encryptPass []byte - encryptUser []byte - Password string - LoginURL string - CreateDate time.Time -} - -const ( - queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins` -) - -func (c *ChromiumPassword) Extract(masterKey []byte) error { - db, err := sql.Open("sqlite", types.ChromiumPassword.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.ChromiumPassword.TempFilename()) - defer db.Close() - - rows, err := db.Query(queryChromiumLogin) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var ( - url, username string - pwd, password []byte - create int64 - ) - if err := rows.Scan(&url, &username, &pwd, &create); err != nil { - log.Debugf("scan chromium password error: %v", err) - } - login := loginData{ - UserName: username, - encryptPass: pwd, - LoginURL: url, - } - - if len(pwd) > 0 { - password, err = crypto.DecryptWithDPAPI(pwd) - if err != nil { - password, err = crypto.DecryptWithChromium(masterKey, pwd) - if err != nil { - log.Debugf("decrypt chromium password error: %v", err) - } - } - } - - if create > time.Now().Unix() { - login.CreateDate = typeutil.TimeEpoch(create) - } else { - login.CreateDate = typeutil.TimeStamp(create) - } - login.Password = string(password) - *c = append(*c, login) - } - // sort with create date - sort.Slice(*c, func(i, j int) bool { - return (*c)[i].CreateDate.After((*c)[j].CreateDate) - }) - return nil -} - -func (c *ChromiumPassword) Name() string { - return "password" -} - -func (c *ChromiumPassword) Len() int { - return len(*c) -} - -type YandexPassword []loginData - -const ( - queryYandexLogin = `SELECT action_url, username_value, password_value, date_created FROM logins` -) - -func (c *YandexPassword) Extract(masterKey []byte) error { - db, err := sql.Open("sqlite", types.YandexPassword.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.YandexPassword.TempFilename()) - defer db.Close() - - rows, err := db.Query(queryYandexLogin) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var ( - url, username string - pwd, password []byte - create int64 - ) - if err := rows.Scan(&url, &username, &pwd, &create); err != nil { - log.Debugf("scan yandex password error: %v", err) - } - login := loginData{ - UserName: username, - encryptPass: pwd, - LoginURL: url, - } - - if len(pwd) > 0 { - if len(masterKey) == 0 { - password, err = crypto.DecryptWithDPAPI(pwd) - } else { - password, err = crypto.DecryptWithChromium(masterKey, pwd) - } - if err != nil { - log.Debugf("decrypt yandex password error: %v", err) - } - } - if create > time.Now().Unix() { - login.CreateDate = typeutil.TimeEpoch(create) - } else { - login.CreateDate = typeutil.TimeStamp(create) - } - login.Password = string(password) - *c = append(*c, login) - } - // sort with create date - sort.Slice(*c, func(i, j int) bool { - return (*c)[i].CreateDate.After((*c)[j].CreateDate) - }) - return nil -} - -func (c *YandexPassword) Name() string { - return "password" -} - -func (c *YandexPassword) Len() int { - return len(*c) -} - -type FirefoxPassword []loginData - -func (f *FirefoxPassword) Extract(globalSalt []byte) error { - logins, err := getFirefoxLoginData() - if err != nil { - return err - } - - for _, v := range logins { - userPBE, err := crypto.NewASN1PBE(v.encryptUser) - if err != nil { - return err - } - pwdPBE, err := crypto.NewASN1PBE(v.encryptPass) - if err != nil { - return err - } - user, err := userPBE.Decrypt(globalSalt) - if err != nil { - return err - } - pwd, err := pwdPBE.Decrypt(globalSalt) - if err != nil { - return err - } - *f = append(*f, loginData{ - LoginURL: v.LoginURL, - UserName: string(user), - Password: string(pwd), - CreateDate: v.CreateDate, - }) - } - - sort.Slice(*f, func(i, j int) bool { - return (*f)[i].CreateDate.After((*f)[j].CreateDate) - }) - return nil -} - -func getFirefoxLoginData() ([]loginData, error) { - s, err := os.ReadFile(types.FirefoxPassword.TempFilename()) - if err != nil { - return nil, err - } - defer os.Remove(types.FirefoxPassword.TempFilename()) - loginsJSON := gjson.GetBytes(s, "logins") - var logins []loginData - if loginsJSON.Exists() { - for _, v := range loginsJSON.Array() { - var ( - m loginData - user []byte - pass []byte - ) - // Use formSubmitURL if available, otherwise fallback to hostname - m.LoginURL = v.Get("formSubmitURL").String() - if m.LoginURL == "" { - m.LoginURL = v.Get("hostname").String() - } - user, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String()) - if err != nil { - return nil, err - } - pass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String()) - if err != nil { - return nil, err - } - m.encryptUser = user - m.encryptPass = pass - m.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000) - logins = append(logins, m) - } - } - return logins, nil -} - -func (f *FirefoxPassword) Name() string { - return "password" -} - -func (f *FirefoxPassword) Len() int { - return len(*f) -} diff --git a/browserdata/sessionstorage/sessionstorage.go b/browserdata/sessionstorage/sessionstorage.go deleted file mode 100644 index 7c10b96..0000000 --- a/browserdata/sessionstorage/sessionstorage.go +++ /dev/null @@ -1,175 +0,0 @@ -package sessionstorage - -import ( - "bytes" - "database/sql" - "fmt" - "os" - "strings" - - "github.com/syndtr/goleveldb/leveldb" - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" - - "github.com/moond4rk/hackbrowserdata/extractor" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/byteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" -) - -func init() { - extractor.RegisterExtractor(types.ChromiumSessionStorage, func() extractor.Extractor { - return new(ChromiumSessionStorage) - }) - extractor.RegisterExtractor(types.FirefoxSessionStorage, func() extractor.Extractor { - return new(FirefoxSessionStorage) - }) -} - -type ChromiumSessionStorage []session - -type session struct { - IsMeta bool - URL string - Key string - Value string -} - -const maxLocalStorageValueLength = 1024 * 2 - -func (c *ChromiumSessionStorage) Extract(_ []byte) error { - db, err := leveldb.OpenFile(types.ChromiumSessionStorage.TempFilename(), nil) - if err != nil { - return err - } - defer os.RemoveAll(types.ChromiumSessionStorage.TempFilename()) - defer db.Close() - - iter := db.NewIterator(nil, nil) - for iter.Next() { - key := iter.Key() - value := iter.Value() - s := new(session) - s.fillKey(key) - // don't all value upper than 2KB - if len(value) < maxLocalStorageValueLength { - s.fillValue(value) - } else { - s.Value = fmt.Sprintf("value is too long, length is %d, supported max length is %d", len(value), maxLocalStorageValueLength) - } - if s.IsMeta { - s.Value = fmt.Sprintf("meta data, value bytes is %v", value) - } - *c = append(*c, *s) - } - iter.Release() - err = iter.Error() - return err -} - -func (c *ChromiumSessionStorage) Name() string { - return "sessionStorage" -} - -func (c *ChromiumSessionStorage) Len() int { - return len(*c) -} - -func (s *session) fillKey(b []byte) { - keys := bytes.Split(b, []byte("-")) - if len(keys) == 1 && bytes.HasPrefix(keys[0], []byte("META:")) { - s.IsMeta = true - s.fillMetaHeader(keys[0]) - } - if len(keys) == 2 && bytes.HasPrefix(keys[0], []byte("_")) { - s.fillHeader(keys[0], keys[1]) - } - if len(keys) == 3 { - if string(keys[0]) == "map" { - s.Key = string(keys[2]) - } else if string(keys[0]) == "namespace" { - s.URL = string(keys[2]) - s.Key = string(keys[1]) - } - } -} - -func (s *session) fillMetaHeader(b []byte) { - s.URL = string(bytes.Trim(b, "META:")) -} - -func (s *session) fillHeader(url, key []byte) { - s.URL = string(bytes.Trim(url, "_")) - s.Key = string(bytes.Trim(key, "\x01")) -} - -func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) { - r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source) - return r, err -} - -// fillValue fills value of the storage -// TODO: support unicode charter -func (s *session) fillValue(b []byte) { - value := bytes.Map(byteutil.OnSplitUTF8Func, b) - s.Value = string(value) -} - -type FirefoxSessionStorage []session - -const ( - querySessionStorage = `SELECT originKey, key, value FROM webappsstore2` - closeJournalMode = `PRAGMA journal_mode=off` -) - -func (f *FirefoxSessionStorage) Extract(_ []byte) error { - db, err := sql.Open("sqlite", types.FirefoxSessionStorage.TempFilename()) - if err != nil { - return err - } - defer os.Remove(types.FirefoxSessionStorage.TempFilename()) - defer db.Close() - - _, err = db.Exec(closeJournalMode) - if err != nil { - log.Debugf("close journal mode error: %v", err) - } - rows, err := db.Query(querySessionStorage) - if err != nil { - return err - } - defer rows.Close() - for rows.Next() { - var originKey, key, value string - if err = rows.Scan(&originKey, &key, &value); err != nil { - log.Debugf("scan session storage error: %v", err) - } - s := new(session) - s.fillFirefox(originKey, key, value) - *f = append(*f, *s) - } - return nil -} - -func (s *session) fillFirefox(originKey, key, value string) { - // originKey = moc.buhtig.:https:443 - p := strings.Split(originKey, ":") - h := typeutil.Reverse([]byte(p[0])) - if bytes.HasPrefix(h, []byte(".")) { - h = h[1:] - } - if len(p) == 3 { - s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2]) - } - s.Key = key - s.Value = value -} - -func (f *FirefoxSessionStorage) Name() string { - return "sessionStorage" -} - -func (f *FirefoxSessionStorage) Len() int { - return len(*f) -} diff --git a/extractor/extractor.go b/extractor/extractor.go deleted file mode 100644 index 1cc6f69..0000000 --- a/extractor/extractor.go +++ /dev/null @@ -1,10 +0,0 @@ -package extractor - -// Extractor is an interface for extracting data from browser data files -type Extractor interface { - Extract(masterKey []byte) error - - Name() string - - Len() int -} diff --git a/extractor/registration.go b/extractor/registration.go deleted file mode 100644 index 747a328..0000000 --- a/extractor/registration.go +++ /dev/null @@ -1,20 +0,0 @@ -package extractor - -import ( - "github.com/moond4rk/hackbrowserdata/types" -) - -var extractorRegistry = make(map[types.DataType]func() Extractor) - -// RegisterExtractor is used to register the data source -func RegisterExtractor(dataType types.DataType, factoryFunc func() Extractor) { - extractorRegistry[dataType] = factoryFunc -} - -// CreateExtractor is used to create the data source -func CreateExtractor(dataType types.DataType) Extractor { - if factoryFunc, ok := extractorRegistry[dataType]; ok { - return factoryFunc() - } - return nil -} diff --git a/go.mod b/go.mod index 9dab3ba..16d77bb 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,8 @@ module github.com/moond4rk/hackbrowserdata go 1.20 require ( - github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/godbus/dbus/v5 v5.2.2 - github.com/moond4rk/keychainbreaker v0.1.0 + github.com/moond4rk/keychainbreaker v0.2.5 github.com/otiai10/copy v1.14.1 github.com/ppacher/go-dbus-keyring v1.0.1 github.com/stretchr/testify v1.11.1 @@ -14,7 +12,6 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/urfave/cli/v2 v2.27.7 golang.org/x/sys v0.27.0 - golang.org/x/text v0.19.0 modernc.org/sqlite v1.31.1 ) @@ -35,6 +32,8 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.55.3 // indirect diff --git a/go.sum b/go.sum index 3f9d4ea..004a57f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= -github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -7,8 +5,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= -github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= @@ -22,11 +18,10 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/moond4rk/keychainbreaker v0.1.0 h1:9hkE70c4jxaTHStZ3kny4GEJ/srcvt2DZe0vUg3m8V0= -github.com/moond4rk/keychainbreaker v0.1.0/go.mod h1:VVx2VXwL2EGhuU2WBD67w66JCKKqLFXGJg91y3FY4f0= +github.com/moond4rk/keychainbreaker v0.2.5 h1:1f2qmgpt1sl+mXA8DTW9nnVhzo4oGO08bnkXu70DL04= +github.com/moond4rk/keychainbreaker v0.2.5/go.mod h1:VVx2VXwL2EGhuU2WBD67w66JCKKqLFXGJg91y3FY4f0= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -61,8 +56,8 @@ github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AO github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -74,6 +69,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/types/types.go b/types/types.go deleted file mode 100644 index c955252..0000000 --- a/types/types.go +++ /dev/null @@ -1,221 +0,0 @@ -package types - -import ( - "fmt" - "os" - "path/filepath" -) - -type DataType int - -const ( - ChromiumKey DataType = iota - ChromiumPassword - ChromiumCookie - ChromiumBookmark - ChromiumHistory - ChromiumDownload - ChromiumCreditCard - ChromiumLocalStorage - ChromiumSessionStorage - ChromiumExtension - - YandexPassword - YandexCreditCard - - FirefoxKey4 - FirefoxPassword - FirefoxCookie - FirefoxBookmark - FirefoxHistory - FirefoxDownload - FirefoxCreditCard - FirefoxLocalStorage - FirefoxSessionStorage - FirefoxExtension -) - -var itemFileNames = map[DataType]string{ - ChromiumKey: fileChromiumKey, - ChromiumPassword: fileChromiumPassword, - ChromiumCookie: fileChromiumCookie, - ChromiumBookmark: fileChromiumBookmark, - ChromiumDownload: fileChromiumDownload, - ChromiumLocalStorage: fileChromiumLocalStorage, - ChromiumSessionStorage: fileChromiumSessionStorage, - ChromiumCreditCard: fileChromiumCredit, - ChromiumExtension: fileChromiumExtension, - ChromiumHistory: fileChromiumHistory, - YandexPassword: fileYandexPassword, - YandexCreditCard: fileYandexCredit, - FirefoxKey4: fileFirefoxKey4, - FirefoxPassword: fileFirefoxPassword, - FirefoxCookie: fileFirefoxCookie, - FirefoxBookmark: fileFirefoxData, - FirefoxDownload: fileFirefoxData, - FirefoxLocalStorage: fileFirefoxLocalStorage, - FirefoxHistory: fileFirefoxData, - FirefoxExtension: fileFirefoxExtension, - FirefoxSessionStorage: UnsupportedItem, - FirefoxCreditCard: UnsupportedItem, -} - -func (i DataType) String() string { - switch i { - case ChromiumKey: - return "ChromiumKey" - case ChromiumPassword: - return "ChromiumPassword" - case ChromiumCookie: - return "ChromiumCookie" - case ChromiumBookmark: - return "ChromiumBookmark" - case ChromiumHistory: - return "ChromiumHistory" - case ChromiumDownload: - return "ChromiumDownload" - case ChromiumCreditCard: - return "ChromiumCreditCard" - case ChromiumLocalStorage: - return "ChromiumLocalStorage" - case ChromiumSessionStorage: - return "ChromiumSessionStorage" - case ChromiumExtension: - return "ChromiumExtension" - case YandexPassword: - return "YandexPassword" - case YandexCreditCard: - return "YandexCreditCard" - case FirefoxKey4: - return "FirefoxKey4" - case FirefoxPassword: - return "FirefoxPassword" - case FirefoxCookie: - return "FirefoxCookie" - case FirefoxBookmark: - return "FirefoxBookmark" - case FirefoxHistory: - return "FirefoxHistory" - case FirefoxDownload: - return "FirefoxDownload" - case FirefoxCreditCard: - return "FirefoxCreditCard" - case FirefoxLocalStorage: - return "FirefoxLocalStorage" - case FirefoxSessionStorage: - return "FirefoxSessionStorage" - case FirefoxExtension: - return "FirefoxExtension" - default: - return "UnsupportedItem" - } -} - -// Filename returns the filename for the item, defined by browser -// chromium local storage is a folder, so it returns the file name of the folder -func (i DataType) Filename() string { - if fileName, ok := itemFileNames[i]; ok { - return fileName - } - return UnsupportedItem -} - -// TempFilename returns the temp filename for the item with suffix -// eg: chromiumKey_0.temp -func (i DataType) TempFilename() string { - const tempSuffix = "temp" - tempFile := fmt.Sprintf("%s_%d.%s", i.Filename(), i, tempSuffix) - return filepath.Join(os.TempDir(), tempFile) -} - -// IsSensitive returns whether the item is sensitive data -// password, cookie, credit card, master key is unlimited -func (i DataType) IsSensitive() bool { - switch i { - case ChromiumKey, ChromiumCookie, ChromiumPassword, ChromiumCreditCard, - FirefoxKey4, FirefoxPassword, FirefoxCookie, FirefoxCreditCard, - YandexPassword, YandexCreditCard: - return true - default: - return false - } -} - -// FilterSensitiveItems returns the sensitive items -func FilterSensitiveItems(items []DataType) []DataType { - var filtered []DataType - for _, item := range items { - if item.IsSensitive() { - filtered = append(filtered, item) - } - } - return filtered -} - -// DefaultFirefoxTypes returns the default items for the firefox browser -var DefaultFirefoxTypes = []DataType{ - FirefoxKey4, - FirefoxPassword, - FirefoxCookie, - FirefoxBookmark, - FirefoxHistory, - FirefoxDownload, - FirefoxCreditCard, - FirefoxLocalStorage, - FirefoxSessionStorage, - FirefoxExtension, -} - -// DefaultYandexTypes returns the default items for the yandex browser -var DefaultYandexTypes = []DataType{ - ChromiumKey, - ChromiumCookie, - ChromiumBookmark, - ChromiumHistory, - ChromiumDownload, - ChromiumExtension, - YandexPassword, - ChromiumLocalStorage, - ChromiumSessionStorage, - YandexCreditCard, -} - -// DefaultChromiumTypes returns the default items for the chromium browser -var DefaultChromiumTypes = []DataType{ - ChromiumKey, - ChromiumPassword, - ChromiumCookie, - ChromiumBookmark, - ChromiumHistory, - ChromiumDownload, - ChromiumCreditCard, - ChromiumLocalStorage, - ChromiumSessionStorage, - ChromiumExtension, -} - -// item's default filename -const ( - fileChromiumKey = "Local State" - fileChromiumCredit = "Web Data" - fileChromiumPassword = "Login Data" - fileChromiumHistory = "History" - fileChromiumDownload = "History" - fileChromiumCookie = "Cookies" - fileChromiumBookmark = "Bookmarks" - fileChromiumLocalStorage = "Local Storage/leveldb" - fileChromiumSessionStorage = "Session Storage" - fileChromiumExtension = "Secure Preferences" // TODO: add more extension files and folders, eg: Preferences - - fileYandexPassword = "Ya Passman Data" - fileYandexCredit = "Ya Credit Cards" - - fileFirefoxKey4 = "key4.db" - fileFirefoxCookie = "cookies.sqlite" - fileFirefoxPassword = "logins.json" - fileFirefoxData = "places.sqlite" - fileFirefoxLocalStorage = "webappsstore.sqlite" - fileFirefoxExtension = "extensions.json" - - UnsupportedItem = "unsupported item" -) diff --git a/types/types_test.go b/types/types_test.go deleted file mode 100644 index 0d0b03a..0000000 --- a/types/types_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package types - -import ( - "fmt" - "os" - "path/filepath" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDataType_FileName(t *testing.T) { - for _, item := range DefaultChromiumTypes { - assert.Equal(t, item.Filename(), item.filename()) - } - for _, item := range DefaultFirefoxTypes { - assert.Equal(t, item.Filename(), item.filename()) - } - for _, item := range DefaultYandexTypes { - assert.Equal(t, item.Filename(), item.filename()) - } -} - -func TestDataType_TempFilename(t *testing.T) { - asserts := assert.New(t) - - testCases := []struct { - item DataType - expected string - }{ - {ChromiumKey, "Local State"}, - {ChromiumPassword, "Login Data"}, - {ChromiumLocalStorage, "Local Storage/leveldb"}, - {FirefoxSessionStorage, "unsupported item"}, - {FirefoxLocalStorage, "webappsstore.sqlite"}, - {YandexPassword, "Ya Passman Data"}, - {YandexCreditCard, "Ya Credit Cards"}, - } - - for _, tc := range testCases { - expectedPrefix := tc.expected + "_" + strconv.Itoa(int(tc.item)) + ".temp" - actualPath := filepath.ToSlash(tc.item.TempFilename()) - asserts.Contains(actualPath, expectedPrefix, "TempFilename should contain the correct prefix for "+tc.expected) - asserts.Contains(actualPath, filepath.ToSlash(os.TempDir()), "TempFilename should be in the system temp directory for "+tc.expected) - } -} - -func TestDataType_IsSensitive(t *testing.T) { - asserts := assert.New(t) - testCases := []struct { - item DataType - expected bool - }{ - {ChromiumKey, true}, - {ChromiumPassword, true}, - {ChromiumBookmark, false}, - } - for _, tc := range testCases { - asserts.Equal(tc.expected, tc.item.IsSensitive(), fmt.Sprintf("IsSensitive for %v should be %v", tc.item, tc.expected)) - } -} - -func TestFilterSensitiveItems(t *testing.T) { - asserts := assert.New(t) - testCases := []struct { - items []DataType - expected int - }{ - {[]DataType{ChromiumKey, ChromiumBookmark, ChromiumPassword}, 2}, - {[]DataType{ChromiumBookmark, ChromiumHistory}, 0}, - } - - for _, tc := range testCases { - filteredItems := FilterSensitiveItems(tc.items) - asserts.Len(filteredItems, tc.expected, "FilterSensitiveItems should return the correct number of sensitive items") - for _, item := range filteredItems { - asserts.True(item.IsSensitive(), "Filtered items should be sensitive") - } - } -} - -func (i DataType) filename() string { - switch i { - case ChromiumKey: - return fileChromiumKey - case ChromiumPassword: - return fileChromiumPassword - case ChromiumCookie: - return fileChromiumCookie - case ChromiumBookmark: - return fileChromiumBookmark - case ChromiumDownload: - return fileChromiumDownload - case ChromiumLocalStorage: - return fileChromiumLocalStorage - case ChromiumSessionStorage: - return fileChromiumSessionStorage - case ChromiumCreditCard: - return fileChromiumCredit - case ChromiumExtension: - return fileChromiumExtension - case ChromiumHistory: - return fileChromiumHistory - case YandexPassword: - return fileYandexPassword - case YandexCreditCard: - return fileYandexCredit - case FirefoxKey4: - return fileFirefoxKey4 - case FirefoxPassword: - return fileFirefoxPassword - case FirefoxCookie: - return fileFirefoxCookie - case FirefoxBookmark: - return fileFirefoxData - case FirefoxDownload: - return fileFirefoxData - case FirefoxLocalStorage: - return fileFirefoxLocalStorage - case FirefoxHistory: - return fileFirefoxData - case FirefoxExtension: - return fileFirefoxExtension - case FirefoxCreditCard: - return UnsupportedItem - default: - return UnsupportedItem - } -} diff --git a/utils/byteutil/byteutil.go b/utils/byteutil/byteutil.go deleted file mode 100644 index 8b1268b..0000000 --- a/utils/byteutil/byteutil.go +++ /dev/null @@ -1,8 +0,0 @@ -package byteutil - -var OnSplitUTF8Func = func(r rune) rune { - if r == 0x00 || r == 0x01 { - return -1 - } - return r -} diff --git a/utils/fileutil/filetutil.go b/utils/fileutil/filetutil.go index 4197f62..3ea2e55 100644 --- a/utils/fileutil/filetutil.go +++ b/utils/fileutil/filetutil.go @@ -6,9 +6,6 @@ import ( "fmt" "os" "path/filepath" - "strings" - - cp "github.com/otiai10/copy" ) // IsFileExists checks if the file exists in the provided path @@ -23,72 +20,6 @@ func IsFileExists(filename string) bool { return !info.IsDir() } -// IsDirExists checks if the folder exists -func IsDirExists(folder string) bool { - info, err := os.Stat(folder) - if os.IsNotExist(err) { - return false - } - if err != nil { - return false - } - return info.IsDir() -} - -// ReadFile reads the file from the provided path -func ReadFile(filename string) (string, error) { - s, err := os.ReadFile(filename) - return string(s), err -} - -// CopyDir copies the directory from the source to the destination -// skip the file if you don't want to copy -func CopyDir(src, dst, skip string) error { - s := cp.Options{Skip: func(info os.FileInfo, src, dst string) (bool, error) { - return strings.HasSuffix(strings.ToLower(src), skip), nil - }} - return cp.Copy(src, dst, s) -} - -// CopyFile copies the file from the source to the destination -func CopyFile(src, dst string) error { - s, err := os.ReadFile(src) - if err != nil { - return err - } - err = os.WriteFile(dst, s, 0o600) - if err != nil { - return err - } - return nil -} - -// Filename returns the filename from the provided path -func Filename(browser, dataType, ext string) string { - replace := strings.NewReplacer(" ", "_", ".", "_", "-", "_") - return strings.ToLower(fmt.Sprintf("%s_%s.%s", replace.Replace(browser), dataType, ext)) -} - -func BrowserName(browser, user string) string { - replace := strings.NewReplacer(" ", "_", ".", "_", "-", "_", "Profile", "user") - return strings.ToLower(fmt.Sprintf("%s_%s", replace.Replace(browser), replace.Replace(user))) -} - -// ParentDir returns the parent directory of the provided path -func ParentDir(p string) string { - return filepath.Dir(filepath.Clean(p)) -} - -// BaseDir returns the base directory of the provided path -func BaseDir(p string) string { - return filepath.Base(p) -} - -// ParentBaseDir returns the parent base directory of the provided path -func ParentBaseDir(p string) string { - return BaseDir(ParentDir(p)) -} - // CompressDir compresses the directory into a zip file func CompressDir(dir string) error { files, err := os.ReadDir(dir) diff --git a/utils/typeutil/typeutil.go b/utils/typeutil/typeutil.go index 58c9c0b..58b13ff 100644 --- a/utils/typeutil/typeutil.go +++ b/utils/typeutil/typeutil.go @@ -4,30 +4,6 @@ import ( "time" ) -// Keys returns a slice of the keys of the map. based with go 1.18 generics -func Keys[K comparable, V any](m map[K]V) []K { - r := make([]K, 0, len(m)) - for k := range m { - r = append(r, k) - } - return r -} - -// Signed is a constraint that permits any signed integer type. -// If future releases of Go add new predeclared signed integer types, -// this constraint will be modified to include them. -type Signed interface { - ~int | ~int8 | ~int16 | ~int32 | ~int64 -} - -func IntToBool[T Signed](a T) bool { - switch a { - case 0, -1: - return false - } - return true -} - func Reverse[T any](s []T) []T { h := make([]T, len(s)) for i := 0; i < len(s); i++ {