From 48d38ef39ca9b816278b02c69dd4f6e2f0f8f245 Mon Sep 17 00:00:00 2001 From: moonD4rk Date: Sun, 18 Jun 2023 23:20:36 +0800 Subject: [PATCH] feat: add firefox browser to support multiple profiles and export passwords - Refactored and improved readability of Firefox initialization code - Added support for multiple Firefox profiles - Implemented functionality to find and decrypt Firefox master key - Modified password retrieval functions in Firefox and Chromium to handle unsupported passwords and improve error handling - Updated return values for Firefox and Yandex browser types, and removed Yandex browser from map - Changed function name and parameter in options.go to disable all users instead of enable all users - Updated crypto package to improve error messaging and removed unused code --- browingdata/password/password.go | 8 +- browser.go | 10 +- chromium.go | 7 +- crypto/crypto.go | 27 +++-- firefox.go | 163 +++++++++++++++++++++++++++++-- firefox_test.go | 18 ++++ options.go | 6 +- password.go | 82 +++++++++++++++- 8 files changed, 286 insertions(+), 35 deletions(-) create mode 100644 firefox_test.go diff --git a/browingdata/password/password.go b/browingdata/password/password.go index 837e4ad..48184d0 100644 --- a/browingdata/password/password.go +++ b/browingdata/password/password.go @@ -179,7 +179,7 @@ func (f *FirefoxPassword) Parse(masterKey []byte) error { return err } - k, err := metaPBE.Decrypt(globalSalt, masterKey) + k, err := metaPBE.Decrypt(globalSalt) if err != nil { return err } @@ -190,7 +190,7 @@ func (f *FirefoxPassword) Parse(masterKey []byte) error { if err != nil { return err } - finallyKey, err := nssPBE.Decrypt(globalSalt, masterKey) + finallyKey, err := nssPBE.Decrypt(globalSalt) if err != nil { return err } @@ -210,11 +210,11 @@ func (f *FirefoxPassword) Parse(masterKey []byte) error { if err != nil { return err } - user, err := userPBE.Decrypt(finallyKey, masterKey) + user, err := userPBE.Decrypt(finallyKey) if err != nil { return err } - pwd, err := pwdPBE.Decrypt(finallyKey, masterKey) + pwd, err := pwdPBE.Decrypt(finallyKey) if err != nil { return err } diff --git a/browser.go b/browser.go index af842ee..56c4120 100644 --- a/browser.go +++ b/browser.go @@ -60,9 +60,9 @@ const ( func (b browser) Type() browserType { switch b { case Firefox: - return browserTypeYandex - case Yandex: return browserTypeFirefox + case Yandex: + return browserTypeYandex default: return browserTypeChromium } @@ -76,9 +76,9 @@ var browsers = map[browser]Browser{ supportedData: []browserDataType{TypePassword}, }, Firefox: &firefox{ - name: "", - storage: "", - profilePath: "", + name: Firefox, + profilePath: firefoxProfilePath, + supportedData: []browserDataType{TypePassword}, }, Yandex: &chromium{}, } diff --git a/chromium.go b/chromium.go index 37aa5ca..ac507a7 100644 --- a/chromium.go +++ b/chromium.go @@ -56,6 +56,8 @@ func (c *chromium) initProfile() error { return err } c.profilePaths = profilesPaths + } else { + c.profilePaths = []string{c.profilePath} } return nil } @@ -81,7 +83,6 @@ func (c *chromium) findAllProfiles() ([]string, error) { if info.IsDir() && path != root && depth >= 2 { return filepath.SkipDir } - return err }) if err != nil { @@ -114,7 +115,7 @@ func (c *chromium) initMasterKey() error { 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 { + if len(key) == 0 { return ErrWrongSecurityCommand } c.masterKey = key @@ -125,7 +126,7 @@ func (c *chromium) setProfilePath(p string) { c.profilePath = p } -func (c *chromium) setEnableAllUsers(e bool) { +func (c *chromium) setDisableAllUsers(e bool) { c.disableFindAllUser = e } diff --git a/crypto/crypto.go b/crypto/crypto.go index 04d47d9..f0bfbc2 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -9,6 +9,8 @@ import ( "crypto/sha256" "encoding/asn1" "errors" + "fmt" + "strings" "golang.org/x/crypto/pbkdf2" ) @@ -20,25 +22,32 @@ var ( ) type ASN1PBE interface { - Decrypt(globalSalt, masterPwd []byte) (key []byte, err error) + Decrypt(globalSalt []byte) (key []byte, err error) } func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) { var ( - n nssPBE - m metaPBE - l loginPBE + n nssPBE + m metaPBE + l loginPBE + errs []string ) if _, err := asn1.Unmarshal(b, &n); err == nil { return n, nil + } else { + errs = append(errs, err.Error()) } if _, err := asn1.Unmarshal(b, &m); err == nil { return m, nil + } else { + errs = append(errs, err.Error()) } if _, err := asn1.Unmarshal(b, &l); err == nil { return l, nil + } else { + errs = append(errs, err.Error()) } - return nil, errDecodeASN1Failed + return nil, fmt.Errorf("%w: %s", err, strings.Join(errs, "; ")) } // nssPBE Struct @@ -60,8 +69,8 @@ type nssPBE struct { Encrypted []byte } -func (n nssPBE) Decrypt(globalSalt, masterPwd []byte) (key []byte, err error) { - glmp := append(globalSalt, masterPwd...) +func (n nssPBE) Decrypt(globalSalt []byte) (key []byte, err error) { + glmp := globalSalt hp := sha1.Sum(glmp) s := append(hp[:], n.salt()...) chp := sha1.Sum(s) @@ -134,7 +143,7 @@ type slatAttr struct { } } -func (m metaPBE) Decrypt(globalSalt, _ []byte) (key2 []byte, err error) { +func (m metaPBE) Decrypt(globalSalt []byte) (key2 []byte, err error) { k := sha1.Sum(globalSalt) key := pbkdf2.Key(k[:], m.salt(), m.iterationCount(), m.keySize(), sha256.New) iv := append([]byte{4, 14}, m.iv()...) @@ -177,7 +186,7 @@ type loginPBE struct { Encrypted []byte } -func (l loginPBE) Decrypt(globalSalt, _ []byte) (key []byte, err error) { +func (l loginPBE) Decrypt(globalSalt []byte) (key []byte, err error) { return des3Decrypt(globalSalt, l.iv(), l.encrypted()) } diff --git a/firefox.go b/firefox.go index 1196360..7e429db 100644 --- a/firefox.go +++ b/firefox.go @@ -1,19 +1,168 @@ package hackbrowserdata +import ( + "bytes" + "database/sql" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + _ "github.com/mattn/go-sqlite3" + + "github.com/moond4rk/hackbrowserdata/crypto" + "github.com/moond4rk/hackbrowserdata/utils/fileutil" +) + type firefox struct { - name string - storage string - profilePath string - enableAllUser bool - masterKey []byte + name browser + storage string + profilePath string + profilePaths []string + profilePathKeys map[string][]byte + disableFindAllUser bool + firefoxPassword []byte + supportedData []browserDataType + supportedDataMap map[browserDataType]struct{} } func (f *firefox) Init() error { + if err := f.initBrowserData(); err != nil { + return err + } + if err := f.initProfile(); err != nil { + return fmt.Errorf("profile path '%s' does not exist %w", f.profilePath, ErrBrowserNotExists) + } + return f.initMasterKey() +} + +func (f *firefox) initBrowserData() error { + if f.supportedDataMap == nil { + f.supportedDataMap = make(map[browserDataType]struct{}) + } + for _, v := range f.supportedData { + f.supportedDataMap[v] = struct{}{} + } + return nil +} + +func (f *firefox) initProfile() error { + if !fileutil.IsDirExists(f.profilePath) { + return ErrBrowserNotExists + } + if !f.disableFindAllUser { + profilesPaths, err := f.findAllProfiles() + if err != nil { + return err + } + f.profilePaths = profilesPaths + } else { + f.profilePaths = []string{f.profilePath} + } + f.profilePathKeys = make(map[string][]byte) return nil } -func (f *firefox) setEnableAllUsers(e bool) { - f.enableAllUser = e +func (f *firefox) findAllProfiles() ([]string, error) { + var profiles []string + root := fileutil.ParentDir(f.profilePath) + + err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if strings.HasSuffix(path, "key4.db") { + profiles = append(profiles, filepath.Dir(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 >= 3 { + return filepath.SkipDir + } + return err + }) + if err != nil { + return nil, err + } + return profiles, err +} + +func (f *firefox) initMasterKey() error { + for _, profile := range f.profilePaths { + key, err := f.findMasterKey(profile) + if err != nil { + return err + } + f.profilePathKeys[profile] = key + } + return nil +} + +func (f *firefox) findMasterKey(profile string) ([]byte, error) { + keyPath := filepath.Join(profile, "key4.db") + if !fileutil.IsFileExists(keyPath) { + return nil, ErrBrowserNotExists + } + if err := fileutil.CopyFile(keyPath, "key4-copy.db"); err != nil { + return nil, err + } + defer os.Remove("key4-copy.db") + globalSalt, metaBytes, nssA11, nssA102, err := getFirefoxDecryptKey("key4-copy.db") + if err != nil { + return nil, err + } + metaPBE, err := crypto.NewASN1PBE(metaBytes) + if err != nil { + return nil, err + } + + k, err := metaPBE.Decrypt(globalSalt) + 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 + } + } + return nil, 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() + + 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) + } + + if err = db.QueryRow(queryNssPrivate).Scan(&a11, &a102); err != nil { + return nil, nil, nil, nil, fmt.Errorf("query nssPrivate failed: %w, query: %s", err, queryNssPrivate) + } + return item1, item2, a11, a102, nil +} + +func (f *firefox) setDisableAllUsers(e bool) { + f.disableFindAllUser = e } func (f *firefox) setProfilePath(p string) { diff --git a/firefox_test.go b/firefox_test.go new file mode 100644 index 0000000..ee1e159 --- /dev/null +++ b/firefox_test.go @@ -0,0 +1,18 @@ +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) +} diff --git a/options.go b/options.go index 4092484..07c8e81 100644 --- a/options.go +++ b/options.go @@ -9,7 +9,7 @@ type BrowserOption func(browserOptionsSetter) type browserOptionsSetter interface { setProfilePath(string) - setEnableAllUsers(bool) + setDisableAllUsers(bool) setStorageName(string) } @@ -20,9 +20,9 @@ func WithProfilePath(p string) BrowserOption { } } -func WithEnableAllUsers(e bool) BrowserOption { +func WithDisableAllUsers(e bool) BrowserOption { return func(b browserOptionsSetter) { - b.setEnableAllUsers(e) + b.setDisableAllUsers(e) } } diff --git a/password.go b/password.go index 150779d..6756762 100644 --- a/password.go +++ b/password.go @@ -2,13 +2,16 @@ 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" @@ -29,13 +32,13 @@ type Password struct { 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) { - fmt.Println(passFile) return nil, errors.New("password file does not exist") } if err := fileutil.CopyFile(passFile, item.TempChromiumPassword); err != nil { @@ -104,8 +107,79 @@ const ( ) func (f *firefox) Passwords() ([]Password, error) { - if f.masterKey != nil { - return nil, nil + 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 } - return nil, nil + 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 }