diff --git a/browser.go b/browser.go index 56c4120..e5800e6 100644 --- a/browser.go +++ b/browser.go @@ -1,52 +1,41 @@ package hackbrowserdata +import ( + "github.com/moond4rk/hackbrowserdata/browserdata" +) + type Browser interface { - BrowserData + Passwords() ([]browserdata.Password, error) + + Cookies() ([]browserdata.Cookie, error) - Init() error + ExtractBrowserData(dataTypes []DataType) (map[DataType]interface{}, error) } func NewBrowser(b browser, options ...BrowserOption) (Browser, error) { - browser := browsers[b] - if setter, ok := browser.(browserOptionsSetter); ok { - for _, option := range options { - option(setter) - } + opt, ok := defaultBrowserOptions[b] + if !ok { + return nil, ErrBrowserNotSupport } - if err := browser.Init(); err != nil { - return nil, err - } - - return browser, nil -} - -type browser string - -type BrowserData interface { - Passwords() ([]Password, error) - Cookies() ([]Cookie, error) -} - -func (c *chromium) BrowsingData(items []browserDataType) ([]BrowserData, error) { - for _, item := range items { - _ = item + for _, options := range options { + options(opt) } - return nil, nil -} -func (c *chromium) AllBrowsingData() ([]BrowserData, error) { - return nil, nil + if opt.NewBrowserFunc == nil { + return nil, ErrBrowserNotSupport + } + return opt.NewBrowserFunc(opt) } -func (f *firefox) BrowsingData(_ []browserDataType) (BrowserData, error) { - return nil, nil -} +type browser string const ( - Chrome browser = "chrome" - Firefox browser = "firefox" - Yandex browser = "yandex" + Chrome browser = "chrome" + Firefox browser = "firefox" + Yandex browser = "yandex" + Edge browser = "edge" + Chromium browser = "chromium" ) type browserType int @@ -67,18 +56,3 @@ func (b browser) Type() browserType { return browserTypeChromium } } - -var browsers = map[browser]Browser{ - Chrome: &chromium{ - name: Chrome, - storage: chromeStorageName, - profilePath: chromeProfilePath, - supportedData: []browserDataType{TypePassword}, - }, - Firefox: &firefox{ - name: Firefox, - profilePath: firefoxProfilePath, - supportedData: []browserDataType{TypePassword}, - }, - Yandex: &chromium{}, -} diff --git a/consts.go b/browser_darwin.go similarity index 83% rename from consts.go rename to browser_darwin.go index 0322e98..18344ba 100644 --- a/consts.go +++ b/browser_darwin.go @@ -34,3 +34,17 @@ var ( firefoxProfilePath = homeDir + "/Library/Application Support/Firefox/Profiles/" ) + +var defaultBrowserOptions = map[browser]*Options{ + Chrome: { + Name: Chrome, + Storage: chromeStorageName, + ProfilePath: chromeProfilePath, + NewBrowserFunc: NewChromium, + }, + Firefox: { + Name: Firefox, + ProfilePath: firefoxProfilePath, + NewBrowserFunc: NewFirefox, + }, +} diff --git a/browser_linux.go b/browser_linux.go new file mode 100644 index 0000000..190ebe2 --- /dev/null +++ b/browser_linux.go @@ -0,0 +1 @@ +package hackbrowserdata diff --git a/browser_windows.go b/browser_windows.go new file mode 100644 index 0000000..190ebe2 --- /dev/null +++ b/browser_windows.go @@ -0,0 +1 @@ +package hackbrowserdata diff --git a/browserdata/bookmark.go b/browserdata/bookmark.go new file mode 100644 index 0000000..7f6c1c2 --- /dev/null +++ b/browserdata/bookmark.go @@ -0,0 +1,13 @@ +package browserdata + +import ( + "time" +) + +type Bookmark struct { + ID int64 + Name string + Type string + URL string + DateAdded time.Time +} diff --git a/browserdata/browserdata.go b/browserdata/browserdata.go new file mode 100644 index 0000000..3be4b8d --- /dev/null +++ b/browserdata/browserdata.go @@ -0,0 +1,51 @@ +package browserdata + +import ( + "database/sql" + "os" + "path/filepath" + + _ "github.com/mattn/go-sqlite3" + + "github.com/moond4rk/hackbrowserdata/utils/fileutil" +) + +type Extractor interface { + Extract() (interface{}, error) +} + +type RowsHandler func([]byte, interface{}) (interface{}, error) + +type ExtractorHandler func([]byte, string, string, RowsHandler) (interface{}, error) + +func DefaultDBHandler(masterKey []byte, dbpath, dbQuery string, rowsHandler RowsHandler) (interface{}, error) { + tempFile := filepath.Join(os.TempDir(), filepath.Base(dbpath)) + if err := fileutil.CopyFile(dbpath, tempFile); err != nil { + return nil, err + } + defer os.Remove(tempFile) + db, err := sql.Open("sqlite3", tempFile) + if err != nil { + return nil, err + } + defer db.Close() + rows, err := db.Query(dbQuery) + if err != nil { + return nil, err + } + defer rows.Close() + return rowsHandler(masterKey, rows) +} + +func DefaultJSONHandler(masterKey []byte, dbpath, dbQuery string, rowsHandler RowsHandler) (interface{}, error) { + tempFile := filepath.Join(os.TempDir(), filepath.Base(dbpath)) + if err := fileutil.CopyFile(dbpath, tempFile); err != nil { + return nil, err + } + defer os.Remove(tempFile) + s, err := os.ReadFile(tempFile) + if err != nil { + return nil, err + } + return rowsHandler(masterKey, s) +} diff --git a/browserdata/cookie.go b/browserdata/cookie.go new file mode 100644 index 0000000..4a397f9 --- /dev/null +++ b/browserdata/cookie.go @@ -0,0 +1,131 @@ +package browserdata + +import ( + "database/sql" + "os" + "path/filepath" + "sort" + "time" + + "github.com/moond4rk/hackbrowserdata/crypto" + "github.com/moond4rk/hackbrowserdata/log" + "github.com/moond4rk/hackbrowserdata/utils/fileutil" + "github.com/moond4rk/hackbrowserdata/utils/typeutil" +) + +type CookieExtractor struct { + Data []Cookie + masterKey []byte + datafile string +} + +func NewCookieExtractor(masterKey []byte, datafile string) *CookieExtractor { + return &CookieExtractor{masterKey: masterKey, datafile: datafile} +} + +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 +} + +func ExportCookie(masterKey []byte, passwordPath string) ([]Cookie, error) { + tempPassFile := filepath.Join(os.TempDir(), filepath.Base(passwordPath)) + if err := fileutil.CopyFile(passwordPath, tempPassFile); err != nil { + return nil, err + } + defer os.Remove(tempPassFile) + cookies, err := exportCookies(masterKey, "", tempPassFile) + if err != nil { + return nil, err + } + return cookies, err +} + +func exportCookies(masterKey []byte, profile, dbFile string) ([]Cookie, error) { + data, err := exportData(masterKey, profile, dbFile, handlerCookie) + if err != nil { + return nil, err + } + cookies := make([]Cookie, 0, len(data)) + for _, v := range data { + cookies = append(cookies, v.(Cookie)) + } + sort.Slice(cookies, func(i, j int) bool { + return (cookies)[i].CreateDate.After((cookies)[j].CreateDate) + }) + return cookies, nil +} + +type rowHandlerFunc func(masterKey []byte, rows *sql.Rows) (interface{}, error) + +func handlerCookie(masterKey []byte, rows *sql.Rows) (interface{}, error) { + var ( + err error + 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.Warn(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 { + if len(masterKey) == 0 { + value, err = crypto.DPAPI(encryptValue) + } else { + value, err = crypto.DecryptPass(masterKey, encryptValue) + } + if err != nil { + log.Error(err) + } + } + cookie.Value = string(value) + return cookie, nil +} + +func exportData(masterKey []byte, passFile string, query string, rowHandler rowHandlerFunc) ([]interface{}, error) { + db, err := sql.Open("sqlite3", passFile) + if err != nil { + return nil, err + } + defer db.Close() + + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var data []interface{} + for rows.Next() { + item, err := rowHandler(masterKey, rows) + if err != nil { + log.Warn(err) + continue + } + data = append(data, item) + } + return data, nil +} diff --git a/browserdata/password.go b/browserdata/password.go new file mode 100644 index 0000000..e1e9b15 --- /dev/null +++ b/browserdata/password.go @@ -0,0 +1,306 @@ +package browserdata + +import ( + "database/sql" + "encoding/base64" + "os" + "path/filepath" + "time" + + // import go-sqlite3 driver + _ "github.com/mattn/go-sqlite3" + "github.com/tidwall/gjson" + + "github.com/moond4rk/hackbrowserdata/crypto" + "github.com/moond4rk/hackbrowserdata/log" + "github.com/moond4rk/hackbrowserdata/utils/fileutil" + "github.com/moond4rk/hackbrowserdata/utils/typeutil" +) + +type PasswordExtractor struct { + masterKey []byte + datafiles []string + extractorHandler ExtractorHandler + rowsHandler RowsHandler +} + +type Password struct { + Profile string + Username string + Password string + encryptPass []byte + encryptUser []byte + LoginURL string + CreateDate time.Time +} + +func NewPassExtractor(masterKey []byte, datafiles []string, fileHandler ExtractorHandler, rowsHandler RowsHandler) *PasswordExtractor { + return &PasswordExtractor{ + masterKey: masterKey, + datafiles: datafiles, + extractorHandler: fileHandler, + rowsHandler: rowsHandler, + } +} + +func (d *PasswordExtractor) Extract() (interface{}, error) { + var passwords []Password + var err error + for _, datafile := range d.datafiles { + data, err := d.extractorHandler(d.masterKey, datafile, queryChromiumLogin, d.rowsHandler) + if err != nil { + log.Error(err) + continue + } + passwords = append(passwords, data.([]Password)...) + } + return passwords, err +} + +func ChromiumPassRowsHandler(masterKey []byte, rows interface{}) (interface{}, error) { + sqlRows := rows.(*sql.Rows) + var passwords []Password + for sqlRows.Next() { + var ( + url, username string + encryptPass, password []byte + create int64 + err error + ) + if err := sqlRows.Scan(&url, &username, &encryptPass, &create); err != nil { + log.Warn(err) + continue + } + pass := Password{ + // Profile: filepath.Base(profile), + Username: username, + encryptPass: encryptPass, + LoginURL: url, + } + if len(encryptPass) > 0 { + if len(masterKey) == 0 { + password, err = crypto.DPAPI(encryptPass) + } else { + password, err = crypto.DecryptPass(masterKey, encryptPass) + } + if err != nil { + log.Error(err) + } + } + if create > time.Now().Unix() { + pass.CreateDate = typeutil.TimeEpoch(create) + } else { + pass.CreateDate = typeutil.TimeStamp(create) + } + pass.Password = string(password) + passwords = append(passwords, pass) + } + return passwords, nil +} + +func FirefoxPassRowsHandler(masterKey []byte, rows interface{}) (interface{}, error) { + var passwords []Password + + jsonBytes := rows.([]byte) + jsonRows := gjson.GetBytes(jsonBytes, "logins").Array() + + if len(jsonRows) == 0 { + return nil, nil + } + + for _, v := range jsonRows { + var ( + p Password + encryptUser []byte + encryptPass []byte + err error + ) + p.LoginURL = v.Get("formSubmitURL").String() + encryptUser, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String()) + if err != nil { + return nil, err + } + encryptPass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String()) + if err != nil { + return nil, err + } + p.encryptUser = encryptUser + p.encryptPass = encryptPass + // TODO: handle error + userPBE, err := crypto.NewASN1PBE(p.encryptUser) + if err != nil { + return nil, err + } + pwdPBE, err := crypto.NewASN1PBE(p.encryptPass) + if err != nil { + return nil, err + } + username, err := userPBE.Decrypt(masterKey) + if err != nil { + return nil, err + } + password, err := pwdPBE.Decrypt(masterKey) + if err != nil { + return nil, err + } + p.Password = string(password) + p.Username = string(username) + p.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000) + passwords = append(passwords, p) + } + return passwords, nil +} + +func (d *PasswordExtractor) ExtractChromium() (interface{}, error) { + var passwords []Password + var err error + for _, datafile := range d.datafiles { + data, err := DefaultDBHandler(d.masterKey, datafile, queryChromiumLogin, d.rowsHandler) + if err != nil { + log.Error(err) + continue + } + passwords = append(passwords, data.([]Password)...) + } + return passwords, err +} + +func (d *PasswordExtractor) ExtractFirefox() (interface{}, error) { + return nil, nil +} + +func Export(masterKey []byte, passwordPath string) ([]Password, error) { + tempPassFile := filepath.Join(os.TempDir(), filepath.Base(passwordPath)) + if err := fileutil.CopyFile(passwordPath, tempPassFile); err != nil { + return nil, err + } + defer os.Remove(tempPassFile) + passwords, err := exportPasswords(masterKey, "", tempPassFile) + if err != nil { + return nil, err + } + return passwords, err +} + +const ( + queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins` +) + +func exportPasswords(masterKey []byte, profile, passFile string) ([]Password, error) { + db, err := sql.Open("sqlite3", passFile) + if err != nil { + return nil, err + } + defer db.Close() + rows, err := db.Query(queryChromiumLogin) + if err != nil { + return nil, err + } + var passwords []Password + for rows.Next() { + var ( + url, username string + encryptPass, password []byte + create int64 + ) + if err := rows.Scan(&url, &username, &encryptPass, &create); err != nil { + log.Warn(err) + } + pass := Password{ + Profile: filepath.Base(profile), + Username: username, + encryptPass: encryptPass, + LoginURL: url, + } + if len(encryptPass) > 0 { + if len(masterKey) == 0 { + password, err = crypto.DPAPI(encryptPass) + } else { + password, err = crypto.DecryptPass(masterKey, encryptPass) + } + if err != nil { + log.Error(err) + } + } + if create > time.Now().Unix() { + pass.CreateDate = typeutil.TimeEpoch(create) + } else { + pass.CreateDate = typeutil.TimeStamp(create) + } + pass.Password = string(password) + passwords = append(passwords, pass) + } + return passwords, nil +} + +const ( + queryChromiumCookie = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, has_expires, is_persistent FROM cookies` +) + +func ExportPasswords(masterKey []byte, passwordPath string) ([]Password, error) { + tempPassFile := filepath.Join(os.TempDir(), filepath.Base(passwordPath)) + if err := fileutil.CopyFile(passwordPath, tempPassFile); err != nil { + return nil, err + } + defer os.Remove(tempPassFile) + passwords, err := exportFirefoxPasswords(masterKey, "", tempPassFile) + if err != nil { + return nil, err + } + return passwords, err +} + +func exportFirefoxPasswords(masterKey []byte, profile, passFile string) ([]Password, error) { + s, err := os.ReadFile(passFile) + if err != nil { + return nil, err + } + defer os.Remove(passFile) + loginsJSON := gjson.GetBytes(s, "logins") + var passwords []Password + if !loginsJSON.Exists() { + return nil, err + } + + for _, v := range loginsJSON.Array() { + var ( + p Password + encryptUser []byte + encryptPass []byte + ) + p.LoginURL = v.Get("formSubmitURL").String() + encryptUser, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String()) + if err != nil { + return nil, err + } + encryptPass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String()) + if err != nil { + return nil, err + } + p.encryptUser = encryptUser + p.encryptPass = encryptPass + // TODO: handle error + userPBE, err := crypto.NewASN1PBE(p.encryptUser) + if err != nil { + return nil, err + } + pwdPBE, err := crypto.NewASN1PBE(p.encryptPass) + if err != nil { + return nil, err + } + username, err := userPBE.Decrypt(masterKey) + if err != nil { + return nil, err + } + password, err := pwdPBE.Decrypt(masterKey) + if err != nil { + return nil, err + } + p.Password = string(password) + p.Username = string(username) + p.Profile = profile + p.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000) + passwords = append(passwords, p) + } + return passwords, nil +} diff --git a/browsingdata.go b/browsingdata.go deleted file mode 100644 index 4fdd023..0000000 --- a/browsingdata.go +++ /dev/null @@ -1,60 +0,0 @@ -package hackbrowserdata - -type browserDataType int - -const ( - TypePassword browserDataType = iota + 1 - TypeCookie - TypeHistory - TypeBookmark - TypeCreditCard - TypeDownload - TypeExtensions - TypeSessionStorage - TypeLocalStorage -) - -func (i browserDataType) Filename(b browser) string { - switch b.Type() { - case browserTypeChromium: - return i.chromiumFilename() - case browserTypeFirefox: - return i.firefoxFilename() - case browserTypeYandex: - return i.yandexFilename() - } - return "" -} - -func (i browserDataType) chromiumFilename() string { - switch i { - case TypePassword: - return "Login Data" - case TypeCookie: - return "Cookies" - case TypeHistory: - } - return "" -} - -func (i browserDataType) yandexFilename() string { - switch i { - case TypePassword: - return "Login State" - case TypeCookie: - return "Cookies" - case TypeHistory: - } - return "" -} - -func (i browserDataType) firefoxFilename() string { - switch i { - case TypePassword: - return "logins.json" - case TypeCookie: - return "cookies.sqlite" - case TypeHistory: - } - return "" -} diff --git a/chromium.go b/chromium.go index ac507a7..7e245d7 100644 --- a/chromium.go +++ b/chromium.go @@ -1,56 +1,167 @@ package hackbrowserdata import ( - "bytes" - "crypto/sha1" "errors" "fmt" "io/fs" - "os/exec" "path/filepath" "strings" - "golang.org/x/crypto/pbkdf2" - + "github.com/moond4rk/hackbrowserdata/browserdata" "github.com/moond4rk/hackbrowserdata/utils/fileutil" ) type chromium struct { - name browser - storage string - profilePath string - profilePaths []string - disableFindAllUser bool - masterKey []byte - supportedData []browserDataType - supportedDataMap map[browserDataType]struct{} + name browser + storage string + profilePath string + enableAllUsers bool + profilePaths []string + masterKey []byte + // defaultDataTypes + supportedDataTypes []DataType + extractors map[DataType]browserdata.Extractor + extractedData map[DataType]interface{} } -func (c *chromium) Init() error { - if err := c.initBrowserData(); err != nil { - return err +func NewChromium(options *Options) (Browser, error) { + if options.ProfilePath == "" { + return nil, errors.New("profile path is required") + } + if options.Name == "" { + return nil, errors.New("browser name is required") + } + c := &chromium{ + name: options.Name, + profilePath: options.ProfilePath, + enableAllUsers: true, + supportedDataTypes: defaultDataTypes, + extractors: make(map[DataType]browserdata.Extractor), + extractedData: make(map[DataType]interface{}), + } + if !options.IsEnableAllUser { + c.enableAllUsers = false + } + if len(options.DataTypes) > 0 { + c.supportedDataTypes = options.DataTypes + } + if err := c.init(); err != nil { + return nil, err } - if err := c.initProfile(); err != nil { + return c, nil +} + +func (c *chromium) init() error { + if err := c.initProfiles(); err != nil { return fmt.Errorf("profile path '%s' does not exist %w", c.profilePath, ErrBrowserNotExists) } + if err := c.initExtractors(); err != nil { + return err + } return c.initMasterKey() } -func (c *chromium) initBrowserData() error { - if c.supportedDataMap == nil { - c.supportedDataMap = make(map[browserDataType]struct{}) +func (c *chromium) ExtractBrowserData(dataTypes []DataType) (map[DataType]interface{}, error) { + for _, dataType := range dataTypes { + if extractor, ok := c.extractors[dataType]; ok { + data, err := extractor.Extract() + if err != nil { + fmt.Printf("extract %s data failed: %v", dataType, err) + continue + } + c.extractedData[dataType] = data + } + } + return c.extractedData, nil +} + +// func (c *chromium) Passwords() ([]password.Password, error) { +// // browserData, err := c.ExtractBrowserData([]DataType{TypePassword}) +// // if err != nil { +// // return nil, err +// // } +// dataType := TypePassword +// if data, ok := c.extractedData[dataType]; ok { +// return data.([]password.Password), nil +// } +// extractor, ok := c.extractors[dataType] +// if !ok { +// return nil, fmt.Errorf("%s extractor for %s not found", dataType, c.name) +// } +// data, err := extractor.ExtractChromium() +// if err != nil { +// return nil, err +// } +// return data.([]password.Password), nil +// } + +func (c *chromium) filterExistDataPaths(dataTypes []DataType) (map[DataType][]string, error) { + // exporters := make(map[DataType]BrowserData) + dataPaths := make(map[DataType][]string) + var errs []error + for _, profile := range c.profilePaths { + for _, dataType := range dataTypes { + dataTypeFile := filepath.Join(profile, dataType.Filename(c.name)) + if !fileutil.IsFileExists(dataTypeFile) { + errs = append(errs, ErrBrowsingDataNotExists) + } + dataPaths[dataType] = append(dataPaths[dataType], dataTypeFile) + } + } + return dataPaths, nil +} + +func (c *chromium) Passwords() ([]browserdata.Password, error) { + dataType := TypePassword + if data, ok := c.extractedData[dataType]; ok { + return data.([]browserdata.Password), nil + } + extractor, ok := c.extractors[dataType] + if !ok { + return nil, fmt.Errorf("%s extractor for %s not found", dataType, c.name) + } + data, err := extractor.Extract() + if err != nil { + return nil, err + } + return data.([]browserdata.Password), nil +} + +func (c *chromium) Cookies() ([]browserdata.Cookie, error) { + dataType := TypeCookie + if data, ok := c.extractedData[dataType]; ok { + return data.([]browserdata.Cookie), nil + } + extractor, ok := c.extractors[dataType] + if !ok { + return nil, fmt.Errorf("%s extractor for %s not found", dataType, c.name) + } + data, err := extractor.Extract() + if err != nil { + return nil, err + } + return data.([]browserdata.Cookie), nil +} + +func (c *chromium) initExtractors() error { + dataPaths, err := c.filterExistDataPaths(c.supportedDataTypes) + if err != nil { + return err } - for _, v := range c.supportedData { - c.supportedDataMap[v] = struct{}{} + for _, dataType := range c.supportedDataTypes { + if _, ok := dataPaths[dataType]; !ok { + continue + } + c.extractors[dataType] = dataType.NewExtractor(c.name.Type(), c.masterKey, dataPaths[dataType]) } return nil } -func (c *chromium) initProfile() error { +func (c *chromium) initProfiles() error { if !fileutil.IsDirExists(c.profilePath) { return ErrBrowserNotExists } - if !c.disableFindAllUser { + if c.enableAllUsers { profilesPaths, err := c.findAllProfiles() if err != nil { return err @@ -62,6 +173,7 @@ func (c *chromium) initProfile() error { return nil } +// TODO: mix it as firefox's find All Profiles func (c *chromium) findAllProfiles() ([]string, error) { var profiles []string root := fileutil.ParentDir(c.profilePath) @@ -70,7 +182,8 @@ func (c *chromium) findAllProfiles() ([]string, error) { return err } // if the path ends with "History", add it to the list - if strings.HasSuffix(path, "History") { + if strings.HasSuffix(path, TypeHistory.Filename(c.name)) { + // skip the "System Profile" directory if !strings.Contains(path, "System Profile") { profiles = append(profiles, filepath.Dir(path)) } @@ -78,7 +191,6 @@ func (c *chromium) findAllProfiles() ([]string, error) { // calculate the depth of the current path depth := len(strings.Split(path, string(filepath.Separator))) - len(strings.Split(root, string(filepath.Separator))) - // if the depth is more than 2 and it's a directory, skip it if info.IsDir() && path != root && depth >= 2 { return filepath.SkipDir @@ -90,46 +202,3 @@ func (c *chromium) findAllProfiles() ([]string, error) { } return profiles, err } - -func (c *chromium) initMasterKey() error { - var stdout, stderr bytes.Buffer - args := []string{"find-generic-password", "-wa", strings.TrimSpace(c.storage)} - cmd := exec.Command("security", args...) //nolint:gosec - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return 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 ErrCouldNotFindInKeychain - } - return errors.New(stderr.String()) - } - - secret := bytes.TrimSpace(stdout.Bytes()) - if len(secret) == 0 { - return ErrNoPasswordInOutput - } - salt := []byte("saltysalt") - // @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157 - key := pbkdf2.Key(secret, salt, 1003, 16, sha1.New) - if len(key) == 0 { - return ErrWrongSecurityCommand - } - c.masterKey = key - return nil -} - -func (c *chromium) setProfilePath(p string) { - c.profilePath = p -} - -func (c *chromium) setDisableAllUsers(e bool) { - c.disableFindAllUser = e -} - -func (c *chromium) setStorageName(s string) { - c.storage = s -} diff --git a/chromium_darwin.go b/chromium_darwin.go new file mode 100644 index 0000000..d8fcc69 --- /dev/null +++ b/chromium_darwin.go @@ -0,0 +1,46 @@ +package hackbrowserdata + +import ( + "bytes" + "crypto/sha1" + "errors" + "fmt" + "os/exec" + "strings" + + "golang.org/x/crypto/pbkdf2" +) + +var ( + salt = []byte("saltysalt") +) + +func (c *chromium) initMasterKey() error { + var stdout, stderr bytes.Buffer + args := []string{"find-generic-password", "-wa", strings.TrimSpace(c.storage)} + cmd := exec.Command("security", args...) //nolint:gosec + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return 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 ErrCouldNotFindInKeychain + } + return errors.New(stderr.String()) + } + + secret := bytes.TrimSpace(stdout.Bytes()) + if len(secret) == 0 { + return ErrNoPasswordInOutput + } + + key := pbkdf2.Key(secret, salt, 1003, 16, sha1.New) + if len(key) == 0 { + return ErrWrongSecurityCommand + } + c.masterKey = key + return nil +} diff --git a/chromium_linux.go b/chromium_linux.go new file mode 100644 index 0000000..190ebe2 --- /dev/null +++ b/chromium_linux.go @@ -0,0 +1 @@ +package hackbrowserdata diff --git a/chromium_test.go b/chromium_test.go deleted file mode 100644 index 8084613..0000000 --- a/chromium_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package hackbrowserdata - -import ( - "testing" -) - -func TestChromium_Init(_ *testing.T) { -} - -func BenchmarkChromium_Init(b *testing.B) { - chromium := browsers[Chrome] - for i := 0; i < b.N; i++ { - if err := chromium.Init(); err != nil { - b.Fatal(err) - } - } -} diff --git a/chromium_windows.go b/chromium_windows.go new file mode 100644 index 0000000..190ebe2 --- /dev/null +++ b/chromium_windows.go @@ -0,0 +1 @@ +package hackbrowserdata diff --git a/cookie.go b/cookie.go deleted file mode 100644 index 8dc75aa..0000000 --- a/cookie.go +++ /dev/null @@ -1,15 +0,0 @@ -package hackbrowserdata - -type Cookie struct { - Domain string - Expiration float64 - Value string -} - -func (c *chromium) Cookies() ([]Cookie, error) { - return nil, nil -} - -func (f *firefox) Cookies() ([]Cookie, error) { - return nil, nil -} diff --git a/datatype.go b/datatype.go new file mode 100644 index 0000000..296d4ac --- /dev/null +++ b/datatype.go @@ -0,0 +1,144 @@ +package hackbrowserdata + +import ( + "github.com/moond4rk/hackbrowserdata/browserdata" +) + +type DataType int + +const ( + TypeMasterKey DataType = iota + TypePassword + TypeCookie + TypeHistory + TypeBookmark + TypeCreditCard + TypeDownload + TypeExtensions + TypeSessionStorage + TypeLocalStorage +) + +func (i DataType) NewExtractor(browserType browserType, masterKey []byte, datafiles []string) browserdata.Extractor { + switch i { + case TypePassword: + switch browserType { + case browserTypeChromium: + return browserdata.NewPassExtractor(masterKey, datafiles, browserdata.DefaultDBHandler, browserdata.ChromiumPassRowsHandler) + case browserTypeFirefox: + return browserdata.NewPassExtractor(masterKey, datafiles, browserdata.DefaultJSONHandler, browserdata.FirefoxPassRowsHandler) + } + case TypeCookie: + } + return nil +} + +var ( + defaultDataTypes = []DataType{TypePassword, TypeCookie} +) + +const unsupportedType = "" + +func (i DataType) Filename(b browser) string { + switch b.Type() { + case browserTypeChromium: + return i.chromiumFilename() + case browserTypeFirefox: + return i.firefoxFilename() + case browserTypeYandex: + return i.yandexFilename() + default: + return unsupportedType + } +} + +func (i DataType) chromiumFilename() string { + switch i { + case TypeMasterKey: + return fileChromiumKey + case TypePassword: + return fileChromiumPassword + case TypeCookie: + return fileChromiumCookie + case TypeHistory: + return fileChromiumHistory + case TypeBookmark: + return fileChromiumBookmark + case TypeCreditCard: + return fileChromiumCredit + case TypeDownload: + return fileChromiumDownload + case TypeExtensions: + return fileChromiumExtension + case TypeSessionStorage: + return fileChromiumSessionStorage + case TypeLocalStorage: + return fileChromiumLocalStorage + default: + return unsupportedFile + } +} + +func (i DataType) yandexFilename() string { + switch i { + case TypePassword: + return fileYandexPassword + case TypeCreditCard: + return fileYandexCredit + default: + return i.chromiumFilename() + } +} + +func (i DataType) firefoxFilename() string { + switch i { + case TypeMasterKey: + return fileFirefoxMasterKey + case TypePassword: + return fileFirefoxPassword + case TypeCookie: + return fileFirefoxCookie + case TypeHistory: + return fileFirefoxData + case TypeBookmark: + return fileFirefoxData + case TypeCreditCard: + // Firefox does not store credit cards + return unsupportedFile + case TypeDownload: + return fileFirefoxData + case TypeExtensions: + return fileFirefoxExtension + case TypeSessionStorage: + return fileFirefoxData + case TypeLocalStorage: + return fileFirefoxLocalStorage + default: + return unsupportedFile + } +} + +const unsupportedFile = "unsupported file" + +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 = "Extensions" + + fileYandexPassword = "Ya Passman Data" + fileYandexCredit = "Ya Credit Cards" + + fileFirefoxMasterKey = "key4.db" + fileFirefoxCookie = "cookies.sqlite" + fileFirefoxPassword = "logins.json" + fileFirefoxData = "places.sqlite" + fileFirefoxLocalStorage = "webappsstore.sqlite" + fileFirefoxExtension = "extensions.json" +) diff --git a/errors.go b/errors.go index 7424d53..5aa7d7b 100644 --- a/errors.go +++ b/errors.go @@ -6,7 +6,10 @@ import ( var ( ErrBrowserNotExists = errors.New("browser not exists") + ErrBrowserNotSupport = errors.New("browser not support") ErrWrongSecurityCommand = errors.New("wrong security command") ErrNoPasswordInOutput = errors.New("no password in output") ErrCouldNotFindInKeychain = errors.New("could not be find in keychain") + ErrBrowsingDataNotSupport = errors.New("browsing data not support") + ErrBrowsingDataNotExists = errors.New("browsing data not exists") ) diff --git a/examples/export.go b/examples/export.go index e86eb7e..b0e2abb 100644 --- a/examples/export.go +++ b/examples/export.go @@ -7,44 +7,17 @@ import ( ) func main() { - chrome, err := hkb.NewBrowser(hkb.Chrome) + browser, err := hkb.NewBrowser(hkb.Firefox) if err != nil { panic(err) } - passwords, err := chrome.Passwords() + passwords, err := browser.Passwords() if err != nil { panic(err) } - for _, pass := range passwords { - fmt.Printf("%+v\n", pass) - } - // cookies, err := chrome.Cookies() - // if err != nil { - // panic(err) - // } - - // creditCards, err := browser.CreditCards() - // bookmarks, err := browser.Bookmarks() - // downloads, err := browser.Downloads() - // extensions, err := browser.Extensions() - // history, err := browser.History() - // localStorage, err := browser.LocalStorage() - // sessionStorage, err := browser.SessionStorage() - - // items := []hkb.browsingDataType{hkb.Cookie, hkb.Password, hkb.History, hkb.Bookmark} - // browsingData, err := browser.BrowsingDatas(items) - // if err != nil { - // panic(err) - // } - // - // if err != nil { - // panic(err) - // } - // _ = cookies - // items := []hkb.browsingDataType{hkb.Cookie} - // datas, err := browser.BrowsingDatas(items) + fmt.Println(len(passwords)) + // all, err := browser.AllBrowsingData() // if err != nil { // panic(err) // } - // _ = datas } diff --git a/firefox.go b/firefox.go index c59a54e..aff87a7 100644 --- a/firefox.go +++ b/firefox.go @@ -3,6 +3,7 @@ package hackbrowserdata import ( "bytes" "database/sql" + "errors" "fmt" "io/fs" "os" @@ -12,6 +13,7 @@ import ( // import sqlite3 driver _ "github.com/mattn/go-sqlite3" + "github.com/moond4rk/hackbrowserdata/browserdata" "github.com/moond4rk/hackbrowserdata/crypto" "github.com/moond4rk/hackbrowserdata/utils/fileutil" ) @@ -24,23 +26,49 @@ type firefox struct { profilePathKeys map[string][]byte disableFindAllUser bool firefoxPassword []byte - supportedData []browserDataType - supportedDataMap map[browserDataType]struct{} + supportedData []DataType + supportedDataMap map[DataType]struct{} +} + +func NewFirefox(options *Options) (Browser, error) { + return nil, nil } func (f *firefox) Init() error { if err := f.initBrowserData(); err != nil { return err } - if err := f.initProfile(); err != nil { + if err := f.initProfiles(); err != nil { return fmt.Errorf("profile path '%s' does not exist %w", f.profilePath, ErrBrowserNotExists) } return f.initMasterKey() } +func (f *firefox) Passwords() ([]browserdata.Password, error) { + if _, ok := f.supportedDataMap[TypePassword]; !ok { + // TODO: Error handle more gracefully + return nil, errors.New("password for c.name is not supported") + } + var fullPass []browserdata.Password + for profile, masterKey := range f.profilePathKeys { + passFile := filepath.Join(profile, TypePassword.Filename(f.name)) + if !fileutil.IsFileExists(passFile) { + return nil, errors.New("password file does not exist") + } + passwords, err := browserdata.ExportPasswords(masterKey, passFile) + if err != nil { + return nil, err + } + if len(passwords) > 0 { + fullPass = append(fullPass, passwords...) + } + } + return fullPass, nil +} + func (f *firefox) initBrowserData() error { if f.supportedDataMap == nil { - f.supportedDataMap = make(map[browserDataType]struct{}) + f.supportedDataMap = make(map[DataType]struct{}) } for _, v := range f.supportedData { f.supportedDataMap[v] = struct{}{} @@ -48,7 +76,7 @@ func (f *firefox) initBrowserData() error { return nil } -func (f *firefox) initProfile() error { +func (f *firefox) initProfiles() error { if !fileutil.IsDirExists(f.profilePath) { return ErrBrowserNotExists } @@ -102,15 +130,18 @@ func (f *firefox) initMasterKey() error { } func (f *firefox) findMasterKey(profile string) ([]byte, error) { - keyPath := filepath.Join(profile, "key4.db") + keyFile := "key4.db" + keyPath := filepath.Join(profile, keyFile) if !fileutil.IsFileExists(keyPath) { + // TODO: handle error with more details return nil, ErrBrowserNotExists } - if err := fileutil.CopyFile(keyPath, "key4-copy.db"); err != nil { + tempFile := filepath.Join(os.TempDir(), keyFile) + if err := fileutil.CopyFile(keyPath, tempFile); err != nil { return nil, err } - defer os.Remove("key4-copy.db") - globalSalt, metaBytes, nssA11, nssA102, err := getFirefoxDecryptKey("key4-copy.db") + defer os.Remove(tempFile) + globalSalt, metaBytes, nssA11, nssA102, err := getFirefoxDecryptKey(tempFile) if err != nil { return nil, err } @@ -123,35 +154,31 @@ func (f *firefox) findMasterKey(profile string) ([]byte, error) { if err != nil { return nil, err } - if bytes.Contains(k, []byte("password-check")) { - keyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} - if bytes.Equal(nssA102, keyLin) { - nssPBE, err := crypto.NewASN1PBE(nssA11) - if err != nil { - return nil, err - } - masterKey, err := nssPBE.Decrypt(globalSalt) - if err != nil { - return nil, err - } - return masterKey, nil - } + keyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + if !bytes.Equal(k, []byte("password-check")) || !bytes.Equal(nssA102, keyLin) { + return nil, fmt.Errorf("invalid master key") } - return nil, nil + nssPBE, err := crypto.NewASN1PBE(nssA11) + if err != nil { + return nil, err + } + masterKey, err := nssPBE.Decrypt(globalSalt) + if err != nil { + return nil, err + } + return masterKey, nil } -const ( - queryMetaData = `SELECT item1, item2 FROM metaData WHERE id = 'password'` - queryNssPrivate = `SELECT a11, a102 from nssPrivate` -) - func getFirefoxDecryptKey(key4file string) (item1, item2, a11, a102 []byte, err error) { db, err := sql.Open("sqlite3", key4file) if err != nil { return nil, nil, nil, nil, err } defer db.Close() - + var ( + queryMetaData = `SELECT item1, item2 FROM metaData WHERE id = 'password'` + queryNssPrivate = `SELECT a11, a102 from nssPrivate` + ) if err = db.QueryRow(queryMetaData).Scan(&item1, &item2); err != nil { return nil, nil, nil, nil, fmt.Errorf("query metaData failed: %w, query: %s", err, queryMetaData) } diff --git a/firefox_test.go b/firefox_test.go index ee1e159..aa06880 100644 --- a/firefox_test.go +++ b/firefox_test.go @@ -1,18 +1,17 @@ package hackbrowserdata import ( - "fmt" "testing" ) func TestFirefox_Init(t *testing.T) { - firefox := browsers[Firefox] - if err := firefox.Init(); err != nil { - t.Fatal(err) - } - passwords, err := firefox.Passwords() - if err != nil { - t.Fatal(err) - } - fmt.Println(passwords) + // firefox := browsers[Firefox] + // if err := firefox.Init(); err != nil { + // t.Fatal(err) + // } + // passwords, err := firefox.Passwords() + // if err != nil { + // t.Fatal(err) + // } + // fmt.Println(passwords) } diff --git a/options.go b/options.go index 07c8e81..b8bdd17 100644 --- a/options.go +++ b/options.go @@ -1,33 +1,36 @@ package hackbrowserdata -import ( - "path/filepath" -) - -type BrowserOption func(browserOptionsSetter) - -type browserOptionsSetter interface { - setProfilePath(string) +type Options struct { + Name browser + Storage string + ProfilePath string + IsEnableAllUser bool + DataTypes []DataType + NewBrowserFunc func(*Options) (Browser, error) +} - setDisableAllUsers(bool) +type BrowserOption func(*Options) - setStorageName(string) +func WithBrowserName(p string) BrowserOption { + return func(o *Options) { + o.Name = browser(p) + } } func WithProfilePath(p string) BrowserOption { - return func(b browserOptionsSetter) { - b.setProfilePath(filepath.Clean(p)) + return func(o *Options) { + o.ProfilePath = p } } -func WithDisableAllUsers(e bool) BrowserOption { - return func(b browserOptionsSetter) { - b.setDisableAllUsers(e) +func WithEnableAllUsers(e bool) BrowserOption { + return func(o *Options) { + o.IsEnableAllUser = e } } func WithStorageName(s string) BrowserOption { - return func(b browserOptionsSetter) { - b.setStorageName(s) + return func(o *Options) { + o.Storage = s } } diff --git a/password.go b/password.go deleted file mode 100644 index 6756762..0000000 --- a/password.go +++ /dev/null @@ -1,185 +0,0 @@ -package hackbrowserdata - -import ( - "database/sql" - "encoding/base64" - "errors" - "fmt" - "os" - "path/filepath" - "time" - - // import sqlite3 driver - _ "github.com/mattn/go-sqlite3" - "github.com/tidwall/gjson" - - "github.com/moond4rk/hackbrowserdata/crypto" - "github.com/moond4rk/hackbrowserdata/item" - "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/utils/fileutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" -) - -type Password struct { - Profile string - Username string - Password string - encryptPass []byte - encryptUser []byte - LoginURL string - CreateDate time.Time -} - -func (c *chromium) Passwords() ([]Password, error) { - if _, ok := c.supportedDataMap[TypePassword]; !ok { - // TODO: Error handle more gracefully - return nil, errors.New("password for c.name is not supported") - } - var fullPass []Password - for _, profile := range c.profilePaths { - passFile := filepath.Join(profile, TypePassword.Filename(c.name)) - if !fileutil.IsFileExists(passFile) { - return nil, errors.New("password file does not exist") - } - if err := fileutil.CopyFile(passFile, item.TempChromiumPassword); err != nil { - return nil, err - } - passwords, err := c.exportPasswords(profile, item.TempChromiumPassword) - if err != nil { - return nil, err - } - if len(passwords) > 0 { - fullPass = append(fullPass, passwords...) - } - } - return fullPass, nil -} - -func (c *chromium) exportPasswords(profile, dbfile string) ([]Password, error) { - db, err := sql.Open("sqlite3", dbfile) - if err != nil { - return nil, err - } - defer db.Close() - rows, err := db.Query(queryChromiumLogin) - if err != nil { - return nil, err - } - var passwords []Password - for rows.Next() { - var ( - url, username string - encryptPass, password []byte - create int64 - ) - if err := rows.Scan(&url, &username, &encryptPass, &create); err != nil { - log.Warn(err) - } - pass := Password{ - Profile: filepath.Base(profile), - Username: username, - encryptPass: encryptPass, - LoginURL: url, - } - if len(encryptPass) > 0 { - if len(c.masterKey) == 0 { - password, err = crypto.DPAPI(encryptPass) - } else { - password, err = crypto.DecryptPass(c.masterKey, encryptPass) - } - if err != nil { - log.Error(err) - } - } - if create > time.Now().Unix() { - pass.CreateDate = typeutil.TimeEpoch(create) - } else { - pass.CreateDate = typeutil.TimeStamp(create) - } - pass.Password = string(password) - passwords = append(passwords, pass) - } - return passwords, nil -} - -const ( - queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins` -) - -func (f *firefox) Passwords() ([]Password, error) { - if _, ok := f.supportedDataMap[TypePassword]; !ok { - // TODO: Error handle more gracefully - return nil, errors.New("password for c.name is not supported") - } - var fullPass []Password - for profile, masterKey := range f.profilePathKeys { - passFile := filepath.Join(profile, TypePassword.Filename(f.name)) - if !fileutil.IsFileExists(passFile) { - fmt.Println(passFile) - return nil, errors.New("password file does not exist") - } - if err := fileutil.CopyFile(passFile, item.TempFirefoxPassword); err != nil { - return nil, err - } - passwords, err := f.exportPasswords(masterKey, item.TempFirefoxPassword) - if err != nil { - return nil, err - } - if len(passwords) > 0 { - fullPass = append(fullPass, passwords...) - } - } - return fullPass, nil -} - -func (f *firefox) exportPasswords(masterKey []byte, loginFile string) ([]Password, error) { - s, err := os.ReadFile(loginFile) - if err != nil { - return nil, err - } - defer os.Remove(loginFile) - loginsJSON := gjson.GetBytes(s, "logins") - var passwords []Password - if loginsJSON.Exists() { - for _, v := range loginsJSON.Array() { - var ( - p Password - encryptUser []byte - encryptPass []byte - ) - p.LoginURL = v.Get("formSubmitURL").String() - encryptUser, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String()) - if err != nil { - return nil, err - } - encryptPass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String()) - if err != nil { - return nil, err - } - p.encryptUser = encryptUser - p.encryptPass = encryptPass - // TODO: handle error - userPBE, err := crypto.NewASN1PBE(p.encryptUser) - if err != nil { - return nil, err - } - pwdPBE, err := crypto.NewASN1PBE(p.encryptPass) - if err != nil { - return nil, err - } - username, err := userPBE.Decrypt(masterKey) - if err != nil { - return nil, err - } - password, err := pwdPBE.Decrypt(masterKey) - if err != nil { - return nil, err - } - p.Password = string(password) - p.Username = string(username) - p.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000) - passwords = append(passwords, p) - } - } - return passwords, nil -}