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
feat/library
moonD4rk 1 year ago
parent d87ef03ae0
commit 48d38ef39c
No known key found for this signature in database
GPG Key ID: 5AB6217E08D39ABA
  1. 8
      browingdata/password/password.go
  2. 10
      browser.go
  3. 7
      chromium.go
  4. 21
      crypto/crypto.go
  5. 159
      firefox.go
  6. 18
      firefox_test.go
  7. 6
      options.go
  8. 82
      password.go

@ -179,7 +179,7 @@ func (f *FirefoxPassword) Parse(masterKey []byte) error {
return err return err
} }
k, err := metaPBE.Decrypt(globalSalt, masterKey) k, err := metaPBE.Decrypt(globalSalt)
if err != nil { if err != nil {
return err return err
} }
@ -190,7 +190,7 @@ func (f *FirefoxPassword) Parse(masterKey []byte) error {
if err != nil { if err != nil {
return err return err
} }
finallyKey, err := nssPBE.Decrypt(globalSalt, masterKey) finallyKey, err := nssPBE.Decrypt(globalSalt)
if err != nil { if err != nil {
return err return err
} }
@ -210,11 +210,11 @@ func (f *FirefoxPassword) Parse(masterKey []byte) error {
if err != nil { if err != nil {
return err return err
} }
user, err := userPBE.Decrypt(finallyKey, masterKey) user, err := userPBE.Decrypt(finallyKey)
if err != nil { if err != nil {
return err return err
} }
pwd, err := pwdPBE.Decrypt(finallyKey, masterKey) pwd, err := pwdPBE.Decrypt(finallyKey)
if err != nil { if err != nil {
return err return err
} }

@ -60,9 +60,9 @@ const (
func (b browser) Type() browserType { func (b browser) Type() browserType {
switch b { switch b {
case Firefox: case Firefox:
return browserTypeYandex
case Yandex:
return browserTypeFirefox return browserTypeFirefox
case Yandex:
return browserTypeYandex
default: default:
return browserTypeChromium return browserTypeChromium
} }
@ -76,9 +76,9 @@ var browsers = map[browser]Browser{
supportedData: []browserDataType{TypePassword}, supportedData: []browserDataType{TypePassword},
}, },
Firefox: &firefox{ Firefox: &firefox{
name: "", name: Firefox,
storage: "", profilePath: firefoxProfilePath,
profilePath: "", supportedData: []browserDataType{TypePassword},
}, },
Yandex: &chromium{}, Yandex: &chromium{},
} }

@ -56,6 +56,8 @@ func (c *chromium) initProfile() error {
return err return err
} }
c.profilePaths = profilesPaths c.profilePaths = profilesPaths
} else {
c.profilePaths = []string{c.profilePath}
} }
return nil return nil
} }
@ -81,7 +83,6 @@ func (c *chromium) findAllProfiles() ([]string, error) {
if info.IsDir() && path != root && depth >= 2 { if info.IsDir() && path != root && depth >= 2 {
return filepath.SkipDir return filepath.SkipDir
} }
return err return err
}) })
if err != nil { if err != nil {
@ -114,7 +115,7 @@ func (c *chromium) initMasterKey() error {
salt := []byte("saltysalt") salt := []byte("saltysalt")
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157 // @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) key := pbkdf2.Key(secret, salt, 1003, 16, sha1.New)
if key == nil { if len(key) == 0 {
return ErrWrongSecurityCommand return ErrWrongSecurityCommand
} }
c.masterKey = key c.masterKey = key
@ -125,7 +126,7 @@ func (c *chromium) setProfilePath(p string) {
c.profilePath = p c.profilePath = p
} }
func (c *chromium) setEnableAllUsers(e bool) { func (c *chromium) setDisableAllUsers(e bool) {
c.disableFindAllUser = e c.disableFindAllUser = e
} }

@ -9,6 +9,8 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/asn1" "encoding/asn1"
"errors" "errors"
"fmt"
"strings"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
@ -20,7 +22,7 @@ var (
) )
type ASN1PBE interface { 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) { func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
@ -28,17 +30,24 @@ func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
n nssPBE n nssPBE
m metaPBE m metaPBE
l loginPBE l loginPBE
errs []string
) )
if _, err := asn1.Unmarshal(b, &n); err == nil { if _, err := asn1.Unmarshal(b, &n); err == nil {
return n, nil return n, nil
} else {
errs = append(errs, err.Error())
} }
if _, err := asn1.Unmarshal(b, &m); err == nil { if _, err := asn1.Unmarshal(b, &m); err == nil {
return m, nil return m, nil
} else {
errs = append(errs, err.Error())
} }
if _, err := asn1.Unmarshal(b, &l); err == nil { if _, err := asn1.Unmarshal(b, &l); err == nil {
return l, 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 // nssPBE Struct
@ -60,8 +69,8 @@ type nssPBE struct {
Encrypted []byte Encrypted []byte
} }
func (n nssPBE) Decrypt(globalSalt, masterPwd []byte) (key []byte, err error) { func (n nssPBE) Decrypt(globalSalt []byte) (key []byte, err error) {
glmp := append(globalSalt, masterPwd...) glmp := globalSalt
hp := sha1.Sum(glmp) hp := sha1.Sum(glmp)
s := append(hp[:], n.salt()...) s := append(hp[:], n.salt()...)
chp := sha1.Sum(s) 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) k := sha1.Sum(globalSalt)
key := pbkdf2.Key(k[:], m.salt(), m.iterationCount(), m.keySize(), sha256.New) key := pbkdf2.Key(k[:], m.salt(), m.iterationCount(), m.keySize(), sha256.New)
iv := append([]byte{4, 14}, m.iv()...) iv := append([]byte{4, 14}, m.iv()...)
@ -177,7 +186,7 @@ type loginPBE struct {
Encrypted []byte 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()) return des3Decrypt(globalSalt, l.iv(), l.encrypted())
} }

@ -1,19 +1,168 @@
package hackbrowserdata 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 { type firefox struct {
name string name browser
storage string storage string
profilePath string profilePath string
enableAllUser bool profilePaths []string
masterKey []byte profilePathKeys map[string][]byte
disableFindAllUser bool
firefoxPassword []byte
supportedData []browserDataType
supportedDataMap map[browserDataType]struct{}
} }
func (f *firefox) Init() error { 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 return nil
} }
func (f *firefox) setEnableAllUsers(e bool) { func (f *firefox) findAllProfiles() ([]string, error) {
f.enableAllUser = e 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) { func (f *firefox) setProfilePath(p string) {

@ -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)
}

@ -9,7 +9,7 @@ type BrowserOption func(browserOptionsSetter)
type browserOptionsSetter interface { type browserOptionsSetter interface {
setProfilePath(string) setProfilePath(string)
setEnableAllUsers(bool) setDisableAllUsers(bool)
setStorageName(string) 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) { return func(b browserOptionsSetter) {
b.setEnableAllUsers(e) b.setDisableAllUsers(e)
} }
} }

@ -2,13 +2,16 @@ package hackbrowserdata
import ( import (
"database/sql" "database/sql"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"time" "time"
// import sqlite3 driver // import sqlite3 driver
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/tidwall/gjson"
"github.com/moond4rk/hackbrowserdata/crypto" "github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/item" "github.com/moond4rk/hackbrowserdata/item"
@ -29,13 +32,13 @@ type Password struct {
func (c *chromium) Passwords() ([]Password, error) { func (c *chromium) Passwords() ([]Password, error) {
if _, ok := c.supportedDataMap[TypePassword]; !ok { if _, ok := c.supportedDataMap[TypePassword]; !ok {
// TODO: Error handle more gracefully
return nil, errors.New("password for c.name is not supported") return nil, errors.New("password for c.name is not supported")
} }
var fullPass []Password var fullPass []Password
for _, profile := range c.profilePaths { for _, profile := range c.profilePaths {
passFile := filepath.Join(profile, TypePassword.Filename(c.name)) passFile := filepath.Join(profile, TypePassword.Filename(c.name))
if !fileutil.IsFileExists(passFile) { if !fileutil.IsFileExists(passFile) {
fmt.Println(passFile)
return nil, errors.New("password file does not exist") return nil, errors.New("password file does not exist")
} }
if err := fileutil.CopyFile(passFile, item.TempChromiumPassword); err != nil { if err := fileutil.CopyFile(passFile, item.TempChromiumPassword); err != nil {
@ -104,8 +107,79 @@ const (
) )
func (f *firefox) Passwords() ([]Password, error) { func (f *firefox) Passwords() ([]Password, error) {
if f.masterKey != nil { if _, ok := f.supportedDataMap[TypePassword]; !ok {
return nil, nil // 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
} }
return nil, nil 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
} }

Loading…
Cancel
Save