From 28280220d1c9b96efcf7d9c4e9c927db7326ec80 Mon Sep 17 00:00:00 2001 From: moonD4rk Date: Sun, 24 Nov 2024 00:35:25 +0800 Subject: [PATCH 1/4] feat: add new browser profile finder and datatypes --- profile/filesystem.go | 16 ++++ profile/finder.go | 161 +++++++++++++++++++++++++++++++++++++++++ profile/finder_test.go | 142 ++++++++++++++++++++++++++++++++++++ profile/profile.go | 62 ++++++++++++++++ types2/datatype.go | 143 ++++++++++++++++++++++++++++++++++++ types2/datatypeinfo.go | 19 +++++ 6 files changed, 543 insertions(+) create mode 100644 profile/filesystem.go create mode 100644 profile/finder.go create mode 100644 profile/finder_test.go create mode 100644 profile/profile.go create mode 100644 types2/datatype.go create mode 100644 types2/datatypeinfo.go diff --git a/profile/filesystem.go b/profile/filesystem.go new file mode 100644 index 00000000..971b6482 --- /dev/null +++ b/profile/filesystem.go @@ -0,0 +1,16 @@ +package profile + +import ( + "io/fs" + "path/filepath" +) + +type FileSystem interface { + WalkDir(root string, fn fs.WalkDirFunc) error +} + +type osFS struct{} + +func (o osFS) WalkDir(root string, fn fs.WalkDirFunc) error { + return filepath.WalkDir(root, fn) +} diff --git a/profile/finder.go b/profile/finder.go new file mode 100644 index 00000000..70a8f9db --- /dev/null +++ b/profile/finder.go @@ -0,0 +1,161 @@ +package profile + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/moond4rk/hackbrowserdata/log" + "github.com/moond4rk/hackbrowserdata/types2" +) + +type Finder struct { + // // FileSystem is the file system interface. + // // Default is os package, can be replaced with a mock for testing + FileSystem FileSystem +} + +func NewFinder() *Finder { + manager := &Finder{ + FileSystem: osFS{}, + } + return manager +} + +func (m *Finder) FindProfiles(rootPath string, browserType types2.BrowserType, dataTypes []types2.DataType) (Profiles, error) { + var err error + var profiles Profiles + switch browserType { + case types2.FirefoxType: + profiles, err = m.findFirefoxProfiles(rootPath, browserType, dataTypes) + case types2.ChromiumType, types2.YandexType: + profiles, err = m.findChromiumProfiles(rootPath, browserType, dataTypes) + default: + return nil, fmt.Errorf("unsupported browser type: %s", browserType.String()) + } + if err != nil { + return nil, fmt.Errorf("failed to find profiles: %w", err) + } + return profiles, nil +} + +var ( + defaultExcludeDirs = []string{"Snapshot", "System Profile", "Crash Reports", "def"} +) + +func (m *Finder) findChromiumProfiles(rootPath string, browserType types2.BrowserType, dataTypes []types2.DataType) (Profiles, error) { + profiles := NewProfiles() + var masterKeyPath string + + err := m.FileSystem.WalkDir(rootPath, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + if os.IsPermission(err) { + log.Debugf("skipping walk chromium path permission error: %s", err) + return nil + } + return err + } + // Skip directories that should not be included + if entry.IsDir() && skipDirs(path, defaultExcludeDirs) { + return fs.SkipDir + } + for _, dataType := range dataTypes { + dataTypeFilename := dataType.Filename(browserType) + if dataType == types2.MasterKey && entry.Name() == dataTypeFilename { + masterKeyPath = path + break + } + if !isEntryMatchesDataType(browserType, entry, dataType, path) { + continue + } + // Calculate relative path from baseDir path + profileName := extractProfileName(rootPath, path, entry.IsDir()) + if profileName == "" { + continue + } + profiles.SetDataTypePath(profileName, dataType, path) + } + return nil + }) + profiles.SetMasterKey(masterKeyPath) + return profiles, err +} + +func (m *Finder) findFirefoxProfiles(rootPath string, browserType types2.BrowserType, dataTypes []types2.DataType) (Profiles, error) { + profiles := NewProfiles() + err := m.FileSystem.WalkDir(rootPath, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + if os.IsPermission(err) { + log.Debugf("skipping walk firefox path permission error: %s", err) + return nil + } + return err + } + for _, dataType := range dataTypes { + if !isEntryMatchesDataType(browserType, entry, dataType, path) { + continue + } + // Calculate relative path from Firefox baseDir path + profileName := extractProfileName(rootPath, path, entry.IsDir()) + if profileName == "" { + continue + } + profiles.SetDataTypePath(profileName, dataType, path) + } + return nil + }) + profiles.AssignMasterKey() + return profiles, err +} + +// skipDirs returns true if the path should be excluded from processing. +// `dirs` is a slice of directory names to skip. +func skipDirs(path string, dirs []string) bool { + base := filepath.Base(path) + for _, dir := range dirs { + if strings.Contains(base, dir) { + return true + } + } + return false +} + +// extractProfileName extracts the profile name from the path relative to the baseDir path. +// The profile name is the first directory in the relative path. +// If the path is not relative to the baseDir, an empty string is returned. +func extractProfileName(basePath, currentPath string, isDir bool) string { + relativePath, err := filepath.Rel(basePath, currentPath) + // If the path is not relative to the baseDir, return empty string + if err != nil || strings.HasPrefix(relativePath, "..") || relativePath == "." { + return "" + } + pathParts := strings.Split(relativePath, string(filepath.Separator)) + if len(pathParts) == 0 { + return "" + } + if isDir { + return pathParts[0] + } + if len(pathParts) > 1 { + return pathParts[0] + } + return "" +} + +func isEntryMatchesDataType(browserType types2.BrowserType, entry fs.DirEntry, dataType types2.DataType, path string) bool { + // if dataType and entry type (file or directory) do not match, return false + if entry.IsDir() != dataType.IsDir(browserType) { + return false + } + + dataTypeFilename := dataType.Filename(browserType) + // if entry is a directory, check if path ends with dataTypeFilename + // e.g. for Chrome, "Local Storage / leveldb" is a directory, so we check if path ends with "leveldb" + if entry.IsDir() { + return strings.HasSuffix(path, dataTypeFilename) + } + // if entry is a file, check if entry name matches dataTypeFilename + return entry.Name() == dataTypeFilename +} diff --git a/profile/finder_test.go b/profile/finder_test.go new file mode 100644 index 00000000..06b2d1b9 --- /dev/null +++ b/profile/finder_test.go @@ -0,0 +1,142 @@ +package profile + +import ( + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/moond4rk/hackbrowserdata/types2" +) + +func TestNewManager_ChromiumMacOS(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skip("skipping test on non-darwin system") + } + paths, err := filepath.Glob(`/Users/*/Library/Application Support/Google/Chrome`) + assert.NoError(t, err) + if len(paths) == 0 { + t.Skip("no chrome profile found") + } + rootPath := paths[0] + browserType := types2.ChromiumType + dataTypes := types2.AllDataTypes + finder := NewFinder() + profiles, err := finder.FindProfiles(rootPath, browserType, dataTypes) + assert.NoError(t, err) + assert.NotNil(t, profiles) + for name, profile := range profiles { + for k, v := range profile.DataFilePath { + t.Logf("name: %s, datatype: %s, datapath: %s", name, k.String(), v) + } + t.Log(name, profile.MasterKeyPath) + } +} + +func TestProfileFinder_FirefoxMacOS(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skip("skipping test on non-darwin system") + } + paths, err := filepath.Glob(`/Users/*/Library/Application Support/Firefox/Profiles`) + assert.NoError(t, err) + if len(paths) == 0 { + t.Skip("no firefox profile found") + } + rootPath := paths[0] + browserType := types2.FirefoxType + dataTypes := types2.AllDataTypes + finder := NewFinder() + profiles, err := finder.FindProfiles(rootPath, browserType, dataTypes) + assert.NoError(t, err) + assert.NotNil(t, profiles) + for name, profile := range profiles { + for k, v := range profile.DataFilePath { + t.Logf("name: %s, datatype: %s, value: %s", name, k.String(), v) + } + t.Log(name, profile.MasterKeyPath) + } +} + +func Test_extractProfileNameFromPath(t *testing.T) { + testCases := []struct { + name string + basePath string + currentPath string + isCurrentPathDir bool + expected string + }{ + { + name: "Valid profile with data file", + basePath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles"), + currentPath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles/abcd1234.default-release/cookies.sqlite"), + isCurrentPathDir: false, + expected: "abcd1234.default-release", + }, + { + name: "Valid profile without data file", + basePath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles"), + currentPath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles/abcd1234.default-release/"), + isCurrentPathDir: true, + expected: "abcd1234.default-release", + }, + { + name: "Invalid path outside basePath", + basePath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles"), + currentPath: filepath.FromSlash("/Users/username/Documents/cookies.sqlite"), + isCurrentPathDir: false, + expected: "", + }, + { + name: "MasterKey in root directory", + basePath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles"), + currentPath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles/key4.db"), + isCurrentPathDir: false, + expected: "", + }, + { + name: "Nested profile directory", + basePath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles"), + currentPath: filepath.FromSlash("/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles/abcd1234.default-release/subdir/cookies.sqlite"), + isCurrentPathDir: false, + expected: "abcd1234.default-release", + }, + { + name: "Windows path format", + basePath: filepath.FromSlash("C:/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles"), + currentPath: filepath.FromSlash("C:/Users/username/AppData/Roaming/Mozilla/Firefox/Profiles/abcd1234.default-release/cookies.sqlite"), + expected: "abcd1234.default-release", + }, + { + name: "cookies in network path", + basePath: filepath.FromSlash("C:/AppData/Local/Google/Chrome/User Data"), + currentPath: filepath.FromSlash("C:/AppData/Local/Google/Chrome/User Data/Profile 1/Network/Cookies"), + isCurrentPathDir: false, + expected: "Profile 1", + }, + { + name: "cookies in network path", + basePath: filepath.FromSlash("C:/AppData/Local/Google/Chrome/User Data"), + currentPath: filepath.FromSlash("C:/AppData/Local/Google/Chrome/User Data"), + isCurrentPathDir: true, + expected: "", + }, + { + name: "local state", + basePath: filepath.FromSlash("/Users/moond4rk/Library/Application Support/Google/Chrome/"), + currentPath: filepath.FromSlash("/Users/moond4rk/Library/Application Support/Google/Chrome/Local State"), + isCurrentPathDir: false, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + profileName := extractProfileName(tc.basePath, tc.currentPath, tc.isCurrentPathDir) + if profileName != tc.expected { + t.Errorf("extractProfileName(%q, %q) = %q; expected %q", tc.basePath, tc.currentPath, profileName, tc.expected) + } + }) + } + +} diff --git a/profile/profile.go b/profile/profile.go new file mode 100644 index 00000000..766250ca --- /dev/null +++ b/profile/profile.go @@ -0,0 +1,62 @@ +package profile + +import ( + "github.com/moond4rk/hackbrowserdata/types2" +) + +type Profiles map[string]*Profile + +func NewProfiles() Profiles { + return make(Profiles) +} + +func (profiles Profiles) GetOrCreateProfile(profileName string) *Profile { + profile, ok := profiles[profileName] + if !ok { + profile = NewProfile(profileName) + profiles[profileName] = profile + } + return profile +} + +func (profiles Profiles) SetDataTypePath(profileName string, dataType types2.DataType, path string) { + profile := profiles.GetOrCreateProfile(profileName) + profile.AddPath(dataType, path) +} + +func (profiles Profiles) SetMasterKey(path string) { + for _, profile := range profiles { + profile.MasterKeyPath = path + } +} + +func (profiles Profiles) AssignMasterKey() { + for _, profile := range profiles { + keyPath, ok := profile.DataFilePath[types2.MasterKey] + if ok && len(keyPath) > 0 { + profile.MasterKeyPath = keyPath[0] + delete(profile.DataFilePath, types2.MasterKey) + } + } +} + +type Profile struct { + Name string + BrowserType types2.BrowserType + // MasterKeyPath is the path to the master key file. + // chromium - "Local State" is shared by all profiles + // firefox - "key4.db" is unique per profile + MasterKeyPath string + DataFilePath map[types2.DataType][]string +} + +func NewProfile(profileName string) *Profile { + return &Profile{ + Name: profileName, + DataFilePath: make(map[types2.DataType][]string), + } +} + +func (p *Profile) AddPath(dataType types2.DataType, path string) { + p.DataFilePath[dataType] = append(p.DataFilePath[dataType], path) +} diff --git a/types2/datatype.go b/types2/datatype.go new file mode 100644 index 00000000..7aecc815 --- /dev/null +++ b/types2/datatype.go @@ -0,0 +1,143 @@ +package types2 + +import ( + "path/filepath" +) + +type DataType int + +const ( + MasterKey DataType = iota + Password + Cookie + Bookmark + History + Download + CreditCard + LocalStorage + SessionStorage + Extension +) + +func (d DataType) String() string { + switch d { + case MasterKey: + return "MasterKey" + case Password: + return "Password" + case Cookie: + return "Cookie" + case Bookmark: + return "Bookmark" + case History: + return "History" + case Download: + return "Download" + case CreditCard: + return "CreditCard" + case LocalStorage: + return "LocalStorage" + case SessionStorage: + return "SessionStorage" + case Extension: + return "Extension" + default: + return "Unknown" + } +} + +func (d DataType) Info(browserType BrowserType) DataTypeInfo { + if browserData, ok := typesDataMap[browserType]; ok { + if info, ok := browserData[d]; ok { + return info + } + } + return DataTypeInfo{} +} + +func (d DataType) Filename(browserType BrowserType) string { + return d.Info(browserType).Filename() +} + +func (d DataType) IsDir(browserType BrowserType) bool { + return d.Info(browserType).IsDir() +} + +var AllDataTypes = []DataType{ + MasterKey, + Password, + Cookie, + Bookmark, + History, + Download, + CreditCard, + LocalStorage, + SessionStorage, + Extension, +} + +type BrowserType int + +const ( + ChromiumType BrowserType = iota + FirefoxType + YandexType +) + +func (b BrowserType) String() string { + switch b { + case ChromiumType: + return "ChromiumType" + case FirefoxType: + return "FirefoxType" + case YandexType: + return "YandexType" + default: + return "Unknown" + } +} + +var typesDataMap = map[BrowserType]map[DataType]DataTypeInfo{ + ChromiumType: chromiumDataMap, + FirefoxType: firefoxDataMap, + YandexType: yandexDataMap, +} + +var chromiumDataMap = map[DataType]DataTypeInfo{ + MasterKey: {filename: "Local State"}, + Password: {filename: "Login Data"}, + Cookie: {filename: "Cookies"}, + Bookmark: {filename: "Bookmarks"}, + History: {filename: "History"}, + Download: {filename: "History"}, + CreditCard: {filename: "Web Data"}, + LocalStorage: {filename: filepath.Join("Local Storage", "leveldb"), isDir: true}, + SessionStorage: {filename: "Session Storage", isDir: true}, + Extension: {filename: "Secure Preferences", alternateNames: []string{"Preferences"}}, +} + +var firefoxDataMap = map[DataType]DataTypeInfo{ + MasterKey: {filename: "key4.db"}, + Password: {filename: "logins.json"}, + Cookie: {filename: "cookies.sqlite"}, + Bookmark: {filename: "places.sqlite"}, + History: {filename: "places.sqlite"}, + Download: {filename: "places.sqlite"}, + // CreditCard: {"logins.json"}, + LocalStorage: {filename: "webappsstore.sqlite"}, + SessionStorage: {filename: "sessionstore.jsonlz4"}, + Extension: {filename: "extensions.json"}, +} + +var yandexDataMap = map[DataType]DataTypeInfo{ + MasterKey: {filename: "Local State"}, + Password: {filename: "Login Data"}, + Cookie: {filename: "Cookies"}, + Bookmark: {filename: "Bookmarks"}, + History: {filename: "History"}, + Download: {filename: "History"}, + CreditCard: {filename: "Web Data"}, + LocalStorage: {filename: filepath.Join("Local Storage", "leveldb"), isDir: true}, + SessionStorage: {filename: "Session Storage", isDir: true}, + Extension: {filename: "Secure Preferences", alternateNames: []string{"Preferences"}}, +} diff --git a/types2/datatypeinfo.go b/types2/datatypeinfo.go new file mode 100644 index 00000000..a4cd8a77 --- /dev/null +++ b/types2/datatypeinfo.go @@ -0,0 +1,19 @@ +package types2 + +type DataTypeInfo struct { + filename string + isDir bool + alternateNames []string +} + +func (dt DataTypeInfo) Filename() string { + return dt.filename +} + +func (dt DataTypeInfo) IsDir() bool { + return dt.isDir +} + +func (dt DataTypeInfo) AlternateNames() []string { + return dt.alternateNames +} From faf988d1c1c71601884a1c2a16b56b4f61b9b13c Mon Sep 17 00:00:00 2001 From: moonD4rk Date: Sun, 24 Nov 2024 00:35:40 +0800 Subject: [PATCH 2/4] refactor: Refactor logging and profile handling functionalities - Refactored variable declarations in log package for improved readability and consistency - Removed redundant code and unnecessary parentheses in logger_test.go - Simplified variable assignment in logger.go for better code maintainability --- log/log.go | 6 ++-- log/logger.go | 2 +- log/logger_test.go | 62 ++++++++++++++++++++---------------------- profile/finder.go | 4 +-- profile/finder_test.go | 1 - 5 files changed, 33 insertions(+), 42 deletions(-) diff --git a/log/log.go b/log/log.go index a89e32da..14f522ca 100644 --- a/log/log.go +++ b/log/log.go @@ -4,10 +4,8 @@ import ( "github.com/moond4rk/hackbrowserdata/log/level" ) -var ( - // defaultLogger is the default logger used by the package-level functions. - defaultLogger = NewLogger(nil) -) +// defaultLogger is the default logger used by the package-level functions. +var defaultLogger = NewLogger(nil) func SetVerbose() { defaultLogger.SetLevel(level.DebugLevel) diff --git a/log/logger.go b/log/logger.go index 8e7243de..a0062080 100644 --- a/log/logger.go +++ b/log/logger.go @@ -136,7 +136,7 @@ func (l *baseLogger) prefixPrint(prefix string, args ...any) { } func (l *baseLogger) getCallDepth() int { - var defaultCallDepth = 2 + defaultCallDepth := 2 pcs := make([]uintptr, 10) n := runtime.Callers(defaultCallDepth, pcs) frames := runtime.CallersFrames(pcs[:n]) diff --git a/log/logger_test.go b/log/logger_test.go index 65e2cd1c..9fa8a85f 100644 --- a/log/logger_test.go +++ b/log/logger_test.go @@ -22,20 +22,18 @@ type baseTestCase struct { wantedPattern string } -var ( - baseTestCases = []baseTestCase{ - { - description: "without trailing newline, logger adds newline", - message: "hello, hacker!", - suffix: "", - }, - { - description: "with trailing newline, logger preserves newline", - message: "hello, hacker!", - suffix: "\n", - }, - } -) +var baseTestCases = []baseTestCase{ + { + description: "without trailing newline, logger adds newline", + message: "hello, hacker!", + suffix: "", + }, + { + description: "with trailing newline, logger preserves newline", + message: "hello, hacker!", + suffix: "\n", + }, +} func TestLoggerDebug(t *testing.T) { for _, tc := range baseTestCases { @@ -121,25 +119,23 @@ type formatTestCase struct { wantedPattern string } -var ( - formatTestCases = []formatTestCase{ - { - description: "message with format prefix", - format: "hello, %s!", - args: []any{"Hacker"}, - }, - { - description: "message with format prefix", - format: "hello, %d,%d,%d!", - args: []any{1, 2, 3}, - }, - { - description: "message with format prefix", - format: "hello, %s,%d,%d!", - args: []any{"Hacker", 2, 3}, - }, - } -) +var formatTestCases = []formatTestCase{ + { + description: "message with format prefix", + format: "hello, %s!", + args: []any{"Hacker"}, + }, + { + description: "message with format prefix", + format: "hello, %d,%d,%d!", + args: []any{1, 2, 3}, + }, + { + description: "message with format prefix", + format: "hello, %s,%d,%d!", + args: []any{"Hacker", 2, 3}, + }, +} func TestLoggerDebugf(t *testing.T) { for _, tc := range formatTestCases { diff --git a/profile/finder.go b/profile/finder.go index 70a8f9db..251e48e3 100644 --- a/profile/finder.go +++ b/profile/finder.go @@ -41,9 +41,7 @@ func (m *Finder) FindProfiles(rootPath string, browserType types2.BrowserType, d return profiles, nil } -var ( - defaultExcludeDirs = []string{"Snapshot", "System Profile", "Crash Reports", "def"} -) +var defaultExcludeDirs = []string{"Snapshot", "System Profile", "Crash Reports", "def"} func (m *Finder) findChromiumProfiles(rootPath string, browserType types2.BrowserType, dataTypes []types2.DataType) (Profiles, error) { profiles := NewProfiles() diff --git a/profile/finder_test.go b/profile/finder_test.go index 06b2d1b9..a8be06c0 100644 --- a/profile/finder_test.go +++ b/profile/finder_test.go @@ -138,5 +138,4 @@ func Test_extractProfileNameFromPath(t *testing.T) { } }) } - } From 8b37ad577a8b0a21888e9417521acbc08c178fb3 Mon Sep 17 00:00:00 2001 From: moonD4rk Date: Mon, 25 Nov 2024 00:04:31 +0800 Subject: [PATCH 3/4] feat: introduce file manager for data extraction operations - Added error handling for missing master key and data files in profile package - Introduced error variables for missing files in profile package - Implemented validation function for checking presence of master key and data files in profile package - Created FileManager type for managing temporary file operations in filemanager package - Implemented methods for copying profiles, copying profile data, and cleaning up temporary files in FileManager type --- .gitignore | 3 +- filemanager/filemanager.go | 143 ++++++++++++++++++++++++++++++++ filemanager/filemanager_test.go | 46 ++++++++++ profile/profile.go | 16 ++++ 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 filemanager/filemanager.go create mode 100644 filemanager/filemanager_test.go diff --git a/.gitignore b/.gitignore index 0c4f246b..407c0eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -207,4 +207,5 @@ hack-browser-data !.typos.toml !.github/*.yml !log/ -examples/*.go \ No newline at end of file +examples/*.go +profile/**/*.go \ No newline at end of file diff --git a/filemanager/filemanager.go b/filemanager/filemanager.go new file mode 100644 index 00000000..67b0eedb --- /dev/null +++ b/filemanager/filemanager.go @@ -0,0 +1,143 @@ +package filemanager + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + cp "github.com/otiai10/copy" + + "github.com/moond4rk/hackbrowserdata/log" + "github.com/moond4rk/hackbrowserdata/profile" +) + +// FileManager manages temporary file operations for data extraction. +type FileManager struct { + // TempDir is the path to the temporary directory. + TempDir string +} + +// defaultDirPattern is the default pattern used for generating temporary directory names. +const defaultDirPattern = "hackbrowserdata" + +// NewFileManager creates a new instance of FileManager with a unique temporary directory. +func NewFileManager() (*FileManager, error) { + baseTempDir := os.TempDir() + tempDir, err := os.MkdirTemp(baseTempDir, defaultDirPattern) + if err != nil { + return nil, fmt.Errorf("failed to create temporary directory: %w", err) + } + return &FileManager{TempDir: tempDir}, nil +} + +func (fm *FileManager) CopyProfiles(originalProfiles profile.Profiles) (profile.Profiles, error) { + newProfiles := profile.NewProfiles() + for profileName, originalProfile := range originalProfiles { + newProfile, err := fm.CopyProfile(originalProfile) + // TODO: Handle multi error + if err != nil { + return nil, fmt.Errorf("failed to copy profile %s: %w", profileName, err) + } + newProfiles[profileName] = newProfile + } + return newProfiles, nil +} + +// CopyProfile copies the profile to a temporary directory and returns the new profile. +// TODO: Handle multi error +func (fm *FileManager) CopyProfile(originalProfile *profile.Profile) (*profile.Profile, error) { + newProfile := profile.NewProfile(originalProfile.Name) + newProfile.BrowserType = originalProfile.BrowserType + + if originalProfile.MasterKeyPath != "" { + copiedMasterKeyPath, err := fm.CopyToTemp(originalProfile.Name, originalProfile.MasterKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to copy master key: %w", err) + } + newProfile.MasterKeyPath = copiedMasterKeyPath + } + + for dataType, paths := range originalProfile.DataFilePath { + for _, originalPath := range paths { + copiedPath, err := fm.CopyToTemp(originalProfile.Name, originalPath) + if err != nil { + log.Errorf("failed to copy data file %s: %v", originalPath, err) + continue + // return nil, fmt.Errorf("failed to copy data file %s: %w", originalPath, err) + } + newProfile.DataFilePath[dataType] = append(newProfile.DataFilePath[dataType], copiedPath) + } + } + + return newProfile, nil +} + +// CopyToTemp copies the specified file or directory to temporary directory and returns its new path. +func (fm *FileManager) CopyToTemp(baseName, originalPath string) (string, error) { + stat, err := os.Stat(originalPath) + if err != nil { + return "", fmt.Errorf("error accessing path %s: %w", originalPath, err) + } + + // unique target filepath is generated by appending the current time in nanoseconds to the original filename + // such is profileName--. + timestampSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) + targetFilename := fmt.Sprintf("%s-%s-%s", baseName, filepath.Base(originalPath), timestampSuffix) + targetPath := filepath.Join(fm.TempDir, targetFilename) + + if stat.IsDir() { + // skip copying the directory if it is a lock file, mostly used in leveldb + err = fm.CopyDir(originalPath, targetPath, "lock") + } else { + err = fm.CopyFile(originalPath, targetPath) + } + + if err != nil { + return "", fmt.Errorf("failed to copy %s to %s: %w", originalPath, targetPath, err) + } + + return targetPath, nil +} + +// Cleanup removes the temporary directory and all its contents. +func (fm *FileManager) Cleanup() error { + return os.RemoveAll(fm.TempDir) +} + +// CopyDir copies the directory from the source to the destination +// skip the file if you don't want to copy +func (fm *FileManager) 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. +// Here `dst` is expected to be the complete path including the filename where the content is to be written. +func (fm *FileManager) CopyFile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + srcInfo, err := srcFile.Stat() + if err != nil { + return err + } + + dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, srcInfo.Mode()) + if err != nil { + return err + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + return nil +} diff --git a/filemanager/filemanager_test.go b/filemanager/filemanager_test.go new file mode 100644 index 00000000..794ccae0 --- /dev/null +++ b/filemanager/filemanager_test.go @@ -0,0 +1,46 @@ +package filemanager + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/moond4rk/hackbrowserdata/profile" + "github.com/moond4rk/hackbrowserdata/types2" +) + +func TestNewFileManager(t *testing.T) { + fm, err := NewFileManager() + assert.NoError(t, err) + defer fm.Cleanup() + fmt.Println(fm.TempDir) +} + +func TestFileManager_CopyProfile(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skip("skipping test on non-darwin system") + } + paths, err := filepath.Glob(`/Users/*/Library/Application Support/Firefox/Profiles`) + assert.NoError(t, err) + if len(paths) == 0 { + t.Skip("no chrome profile found") + } + rootPath := paths[0] + browserType := types2.FirefoxType + dataTypes := types2.AllDataTypes + finder := profile.NewFinder() + profiles, err := finder.FindProfiles(rootPath, browserType, dataTypes) + assert.NoError(t, err) + assert.NotNil(t, profiles) + fmt.Println(profiles) + fm, err := NewFileManager() + assert.NoError(t, err) + fmt.Println(fm.TempDir) + defer fm.Cleanup() + newProfiles, err := fm.CopyProfiles(profiles) + assert.NoError(t, err) + _ = newProfiles +} diff --git a/profile/profile.go b/profile/profile.go index 766250ca..33050f35 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -1,6 +1,8 @@ package profile import ( + "errors" + "github.com/moond4rk/hackbrowserdata/types2" ) @@ -28,6 +30,7 @@ func (profiles Profiles) SetMasterKey(path string) { for _, profile := range profiles { profile.MasterKeyPath = path } + profiles.AssignMasterKey() } func (profiles Profiles) AssignMasterKey() { @@ -60,3 +63,16 @@ func NewProfile(profileName string) *Profile { func (p *Profile) AddPath(dataType types2.DataType, path string) { p.DataFilePath[dataType] = append(p.DataFilePath[dataType], path) } + +var ErrMissingMasterKey = errors.New("missing master key") +var ErrMissingDataFiles = errors.New("missing data files") + +func (p *Profile) Validate() error { + if p.MasterKeyPath == "" { + return ErrMissingMasterKey + } + if len(p.DataFilePath) == 0 { + return ErrMissingDataFiles + } + return nil +} From 14a2d9e71ef26cf599bdaac121e696f213253de6 Mon Sep 17 00:00:00 2001 From: Aquilao Date: Mon, 25 Nov 2024 11:15:19 +0800 Subject: [PATCH 4/4] test: add unit tests for chromium and firefox finder on Windows --- profile/finder_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++ types/types_test.go | 3 ++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/profile/finder_test.go b/profile/finder_test.go index a8be06c0..43fdb795 100644 --- a/profile/finder_test.go +++ b/profile/finder_test.go @@ -1,6 +1,7 @@ package profile import ( + "os" "path/filepath" "runtime" "testing" @@ -58,6 +59,58 @@ func TestProfileFinder_FirefoxMacOS(t *testing.T) { } } +func TestNewManager_ChromiumWindows(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("skipping test on non-windows system") + } + userProfile := os.Getenv("USERPROFILE") + rootPath := filepath.Join(userProfile, `AppData\Local\Google\Chrome\User Data`) + paths, err := filepath.Glob(rootPath) + + assert.NoError(t, err) + if len(paths) == 0 { + t.Skip("no chrome profile found") + } + browserType := types2.ChromiumType + dataTypes := types2.AllDataTypes + finder := NewFinder() + profiles, err := finder.FindProfiles(rootPath, browserType, dataTypes) + assert.NoError(t, err) + assert.NotNil(t, profiles) + for name, profile := range profiles { + for k, v := range profile.DataFilePath { + t.Logf("name: %s, datatype: %s, datapath: %s", name, k.String(), v) + } + t.Log(name, profile.MasterKeyPath) + } +} + +func TestProfileFinder_FirefoxWindows(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("skipping test on non-windows system") + } + userProfile := os.Getenv("USERPROFILE") + rootPath := filepath.Join(userProfile, `AppData\Roaming\Mozilla\Firefox\Profiles`) + paths, err := filepath.Glob(rootPath) + + assert.NoError(t, err) + if len(paths) == 0 { + t.Skip("no firefox profile found") + } + browserType := types2.FirefoxType + dataTypes := types2.AllDataTypes + finder := NewFinder() + profiles, err := finder.FindProfiles(rootPath, browserType, dataTypes) + assert.NoError(t, err) + assert.NotNil(t, profiles) + for name, profile := range profiles { + for k, v := range profile.DataFilePath { + t.Logf("name: %s, datatype: %s, value: %s", name, k.String(), v) + } + t.Log(name, profile.MasterKeyPath) + } +} + func Test_extractProfileNameFromPath(t *testing.T) { testCases := []struct { name string diff --git a/types/types_test.go b/types/types_test.go index 9a68c97d..76c3aec1 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -3,6 +3,7 @@ package types import ( "fmt" "os" + "path/filepath" "strconv" "testing" @@ -30,7 +31,7 @@ func TestDataType_TempFilename(t *testing.T) { }{ {ChromiumKey, "Local State"}, {ChromiumPassword, "Login Data"}, - {ChromiumLocalStorage, "Local Storage/leveldb"}, + {ChromiumLocalStorage, filepath.Join("Local Storage", "leveldb")}, {FirefoxSessionStorage, "unsupported item"}, {FirefoxLocalStorage, "webappsstore.sqlite"}, {YandexPassword, "Ya Passman Data"},