From aa2e1af5b540f829d688a06bc064c464cf68301c Mon Sep 17 00:00:00 2001 From: moonD4rk Date: Sun, 18 Jun 2023 18:58:43 +0800 Subject: [PATCH] feat/library: Add support for use as library for macos chrome - Implement `Browser` interface and related functions - Add `Passwords` method to `chromium` for exporting and returning passwords - Ignore `/docs` directory --- .gitignore | 2 + browser.go | 86 ++++++++++++++++++++++++++++ browsingdata.go | 60 +++++++++++++++++++ chromium.go | 139 +++++++++++++++++++++++++++++++++++++++++++++ chromium_test.go | 18 ++++++ consts.go | 38 +++++++++++++ cookie.go | 15 +++++ errors.go | 12 ++++ examples/export.go | 50 ++++++++++++++++ firefox.go | 25 ++++++++ options.go | 33 +++++++++++ password.go | 111 ++++++++++++++++++++++++++++++++++++ 12 files changed, 589 insertions(+) create mode 100644 browser.go create mode 100644 browsingdata.go create mode 100644 chromium.go create mode 100644 chromium_test.go create mode 100644 consts.go create mode 100644 cookie.go create mode 100644 errors.go create mode 100644 examples/export.go create mode 100644 firefox.go create mode 100644 options.go create mode 100644 password.go diff --git a/.gitignore b/.gitignore index 9b6e196..1e48429 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,5 @@ hack-browser-data !/browingdata/history !/browingdata/history/history.go !/browingdata/history/history_test.go + +/docs diff --git a/browser.go b/browser.go new file mode 100644 index 0000000..2836e9e --- /dev/null +++ b/browser.go @@ -0,0 +1,86 @@ +package hackbrowserdata + +type Browser interface { + BrowserData + + Init() error +} + +func NewBrowser(b browser, options ...BrowserOption) (Browser, error) { + browser := browsers[b] + if setter, ok := browser.(browserOptionsSetter); ok { + for _, option := range options { + option(setter) + } + } + 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 + } + return nil, nil +} + +func (c *chromium) AllBrowsingData() ([]BrowserData, error) { + return nil, nil +} + +func (f *firefox) BrowsingData(items []browserDataType) (BrowserData, error) { + return nil, nil +} + +const ( + Chrome browser = "chrome" + Firefox browser = "firefox" + Yandex browser = "yandex" +) + +type browserType int + +const ( + browserTypeChromium browserType = iota + 1 + browserTypeFirefox + browserTypeYandex +) + +func (b browser) Type() browserType { + switch b { + case Firefox: + return browserTypeYandex + case Yandex: + return browserTypeFirefox + default: + return browserTypeChromium + } +} + +var ( + browsers = map[browser]Browser{ + Chrome: &chromium{ + name: Chrome, + storage: chromeStorageName, + profilePath: chromeProfilePath, + supportedData: []browserDataType{TypePassword}, + }, + Firefox: &firefox{ + name: "", + storage: "", + profilePath: "", + }, + Yandex: &chromium{}, + } +) diff --git a/browsingdata.go b/browsingdata.go new file mode 100644 index 0000000..4fdd023 --- /dev/null +++ b/browsingdata.go @@ -0,0 +1,60 @@ +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 new file mode 100644 index 0000000..27dee5c --- /dev/null +++ b/chromium.go @@ -0,0 +1,139 @@ +package hackbrowserdata + +import ( + "bytes" + "crypto/sha1" + "errors" + "fmt" + "io/fs" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/crypto/pbkdf2" + + "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{} +} + +func (c *chromium) Init() error { + if err := c.initBrowserData(); err != nil { + return err + } + if err := c.initProfile(); err != nil { + return fmt.Errorf("profile path '%s' does not exist %w", c.profilePath, ErrBrowserNotExists) + } + if err := c.initMasterKey(); err != nil { + return err + } + return nil +} + +func (c *chromium) initBrowserData() error { + if c.supportedDataMap == nil { + c.supportedDataMap = make(map[browserDataType]struct{}) + } + for _, v := range c.supportedData { + c.supportedDataMap[v] = struct{}{} + } + return nil +} + +func (c *chromium) initProfile() error { + if !fileutil.IsDirExists(c.profilePath) { + return ErrBrowserNotExists + } + if !c.disableFindAllUser { + profilesPaths, err := c.findAllProfiles() + if err != nil { + return err + } + c.profilePaths = profilesPaths + } + return nil +} + +func (c *chromium) findAllProfiles() ([]string, error) { + var profiles []string + root := fileutil.ParentDir(c.profilePath) + err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + // if the path ends with "History", add it to the list + if strings.HasSuffix(path, "History") { + if !strings.Contains(path, "System Profile") { + profiles = append(profiles, filepath.Dir(path)) + } + } + + // 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 + } + + return err + }) + if err != nil { + return nil, err + } + 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 key == nil { + return ErrWrongSecurityCommand + } + c.masterKey = key + return nil +} + +func (c *chromium) setProfilePath(p string) { + c.profilePath = p +} + +func (c *chromium) setEnableAllUsers(e bool) { + c.disableFindAllUser = e +} + +func (c *chromium) setStorageName(s string) { + c.storage = s +} diff --git a/chromium_test.go b/chromium_test.go new file mode 100644 index 0000000..622b376 --- /dev/null +++ b/chromium_test.go @@ -0,0 +1,18 @@ +package hackbrowserdata + +import ( + "testing" +) + +func TestChromium_Init(t *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/consts.go b/consts.go new file mode 100644 index 0000000..94a5fd4 --- /dev/null +++ b/consts.go @@ -0,0 +1,38 @@ +package hackbrowserdata + +import ( + "os" +) + +const ( + chromeStorageName = "Chrome" + chromeBetaStorageName = "Chrome" + chromiumStorageName = "Chromium" + edgeStorageName = "Microsoft Edge" + braveStorageName = "Brave" + operaStorageName = "Opera" + vivaldiStorageName = "Vivaldi" + coccocStorageName = "CocCoc" + yandexStorageName = "Yandex" + arcStorageName = "Arc" +) + +var ( + homeDir, _ = os.UserHomeDir() +) + +var ( + chromeProfilePath = homeDir + "/Library/Application Support/Google/Chrome/Default/" + chromeBetaProfilePath = homeDir + "/Library/Application Support/Google/Chrome Beta/Default/" + chromiumProfilePath = homeDir + "/Library/Application Support/Chromium/Default/" + edgeProfilePath = homeDir + "/Library/Application Support/Microsoft Edge/Default/" + braveProfilePath = homeDir + "/Library/Application Support/BraveSoftware/Brave-Browser/Default/" + operaProfilePath = homeDir + "/Library/Application Support/com.operasoftware.Opera/Default/" + operaGXProfilePath = homeDir + "/Library/Application Support/com.operasoftware.OperaGX/Default/" + vivaldiProfilePath = homeDir + "/Library/Application Support/Vivaldi/Default/" + coccocProfilePath = homeDir + "/Library/Application Support/Coccoc/Default/" + yandexProfilePath = homeDir + "/Library/Application Support/Yandex/YandexBrowser/Default/" + arcProfilePath = homeDir + "/Library/Application Support/Arc/User Data/Default" + + firefoxProfilePath = homeDir + "/Library/Application Support/Firefox/Profiles/" +) diff --git a/cookie.go b/cookie.go new file mode 100644 index 0000000..8dc75aa --- /dev/null +++ b/cookie.go @@ -0,0 +1,15 @@ +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/errors.go b/errors.go new file mode 100644 index 0000000..7424d53 --- /dev/null +++ b/errors.go @@ -0,0 +1,12 @@ +package hackbrowserdata + +import ( + "errors" +) + +var ( + ErrBrowserNotExists = errors.New("browser not exists") + ErrWrongSecurityCommand = errors.New("wrong security command") + ErrNoPasswordInOutput = errors.New("no password in output") + ErrCouldNotFindInKeychain = errors.New("could not be find in keychain") +) diff --git a/examples/export.go b/examples/export.go new file mode 100644 index 0000000..e86eb7e --- /dev/null +++ b/examples/export.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + + hkb "github.com/moond4rk/hackbrowserdata" +) + +func main() { + chrome, err := hkb.NewBrowser(hkb.Chrome) + if err != nil { + panic(err) + } + passwords, err := chrome.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) + // if err != nil { + // panic(err) + // } + // _ = datas +} diff --git a/firefox.go b/firefox.go new file mode 100644 index 0000000..1196360 --- /dev/null +++ b/firefox.go @@ -0,0 +1,25 @@ +package hackbrowserdata + +type firefox struct { + name string + storage string + profilePath string + enableAllUser bool + masterKey []byte +} + +func (f *firefox) Init() error { + return nil +} + +func (f *firefox) setEnableAllUsers(e bool) { + f.enableAllUser = e +} + +func (f *firefox) setProfilePath(p string) { + f.profilePath = p +} + +func (f *firefox) setStorageName(s string) { + f.storage = s +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..4092484 --- /dev/null +++ b/options.go @@ -0,0 +1,33 @@ +package hackbrowserdata + +import ( + "path/filepath" +) + +type BrowserOption func(browserOptionsSetter) + +type browserOptionsSetter interface { + setProfilePath(string) + + setEnableAllUsers(bool) + + setStorageName(string) +} + +func WithProfilePath(p string) BrowserOption { + return func(b browserOptionsSetter) { + b.setProfilePath(filepath.Clean(p)) + } +} + +func WithEnableAllUsers(e bool) BrowserOption { + return func(b browserOptionsSetter) { + b.setEnableAllUsers(e) + } +} + +func WithStorageName(s string) BrowserOption { + return func(b browserOptionsSetter) { + b.setStorageName(s) + } +} diff --git a/password.go b/password.go new file mode 100644 index 0000000..150779d --- /dev/null +++ b/password.go @@ -0,0 +1,111 @@ +package hackbrowserdata + +import ( + "database/sql" + "errors" + "fmt" + "path/filepath" + "time" + + // import sqlite3 driver + _ "github.com/mattn/go-sqlite3" + + "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 { + 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) { + fmt.Println(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 f.masterKey != nil { + return nil, nil + } + return nil, nil +}