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

@ -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{},
}

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

@ -9,6 +9,8 @@ import (
"crypto/sha256"
"encoding/asn1"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/pbkdf2"
)
@ -20,7 +22,7 @@ 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) {
@ -28,17 +30,24 @@ func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
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())
}

@ -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
name browser
storage string
profilePath string
enableAllUser bool
masterKey []byte
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) 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) setEnableAllUsers(e bool) {
f.enableAllUser = e
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) {

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

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