diff --git a/browser/firefox/firefox.go b/browser/firefox/firefox.go index 31778e7..7bde542 100644 --- a/browser/firefox/firefox.go +++ b/browser/firefox/firefox.go @@ -1,12 +1,18 @@ package firefox import ( + "bytes" + "database/sql" "errors" "fmt" "io/fs" + "os" "path/filepath" + _ "modernc.org/sqlite" // sqlite3 driver TODO: replace with chooseable driver + "github.com/moond4rk/hackbrowserdata/browsingdata" + "github.com/moond4rk/hackbrowserdata/crypto" "github.com/moond4rk/hackbrowserdata/item" "github.com/moond4rk/hackbrowserdata/utils/fileutil" "github.com/moond4rk/hackbrowserdata/utils/typeutil" @@ -68,8 +74,82 @@ func firefoxWalkFunc(items []item.Item, multiItemPaths map[string]map[item.Item] } } +// GetMasterKey returns master key of Firefox. from key4.db func (f *Firefox) GetMasterKey() ([]byte, error) { - return f.masterKey, nil + tempFilename := item.FirefoxKey4.TempFilename() + + // Open and defer close of the database. + keyDB, err := sql.Open("sqlite", tempFilename) + if err != nil { + return nil, fmt.Errorf("open key4.db error: %w", err) + } + defer os.Remove(tempFilename) + defer keyDB.Close() + + globalSalt, metaBytes, err := queryMetaData(keyDB) + if err != nil { + return nil, fmt.Errorf("query metadata error: %w", err) + } + + nssA11, nssA102, err := queryNssPrivate(keyDB) + if err != nil { + return nil, fmt.Errorf("query NSS private error: %w", err) + } + + return processMasterKey(globalSalt, metaBytes, nssA11, nssA102) +} + +func queryMetaData(db *sql.DB) ([]byte, []byte, error) { + const query = `SELECT item1, item2 FROM metaData WHERE id = 'password'` + var globalSalt, metaBytes []byte + if err := db.QueryRow(query).Scan(&globalSalt, &metaBytes); err != nil { + return nil, nil, err + } + return globalSalt, metaBytes, nil +} + +func queryNssPrivate(db *sql.DB) ([]byte, []byte, error) { + const query = `SELECT a11, a102 from nssPrivate` + var nssA11, nssA102 []byte + if err := db.QueryRow(query).Scan(&nssA11, &nssA102); err != nil { + return nil, nil, err + } + return nssA11, nssA102, nil +} + +// processMasterKey process master key of Firefox. +// Process the metaBytes and nssA11 with the corresponding cryptographic operations. +func processMasterKey(globalSalt, metaBytes, nssA11, nssA102 []byte) ([]byte, error) { + 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")) { + return nil, errors.New("password-check not found") + } + keyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + if !bytes.Equal(nssA102, keyLin) { + return nil, errors.New("nssA102 not equal keyLin") + } + nssPBE, err := crypto.NewASN1PBE(nssA11) + if err != nil { + return nil, err + } + finallyKey, err := nssPBE.Decrypt(globalSalt) + if err != nil { + return nil, err + } + if len(finallyKey) < 24 { + return nil, errors.New("finallyKey length less than 24") + } + finallyKey = finallyKey[:24] + return finallyKey, nil } func (f *Firefox) Name() string { diff --git a/browser/firefox/firefox_test.go b/browser/firefox/firefox_test.go new file mode 100644 index 0000000..74c9dac --- /dev/null +++ b/browser/firefox/firefox_test.go @@ -0,0 +1,38 @@ +package firefox + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestQueryMetaData(t *testing.T) { + db, mock, err := sqlmock.New() + assert.NoError(t, err) + defer db.Close() + + rows := sqlmock.NewRows([]string{"item1", "item2"}). + AddRow([]byte("globalSalt"), []byte("metaBytes")) + mock.ExpectQuery("SELECT item1, item2 FROM metaData WHERE id = 'password'").WillReturnRows(rows) + + globalSalt, metaBytes, err := queryMetaData(db) + assert.NoError(t, err) + assert.Equal(t, []byte("globalSalt"), globalSalt) + assert.Equal(t, []byte("metaBytes"), metaBytes) +} + +func TestQueryNssPrivate(t *testing.T) { + db, mock, err := sqlmock.New() + assert.NoError(t, err) + defer db.Close() + + rows := sqlmock.NewRows([]string{"a11", "a102"}). + AddRow([]byte("nssA11"), []byte("nssA102")) + mock.ExpectQuery("SELECT a11, a102 from nssPrivate").WillReturnRows(rows) + + nssA11, nssA102, err := queryNssPrivate(db) + assert.NoError(t, err) + assert.Equal(t, []byte("nssA11"), nssA11) + assert.Equal(t, []byte("nssA102"), nssA102) +} diff --git a/browsingdata/password/password.go b/browsingdata/password/password.go index 2915e8f..6bf3192 100644 --- a/browsingdata/password/password.go +++ b/browsingdata/password/password.go @@ -1,7 +1,6 @@ package password import ( - "bytes" "database/sql" "encoding/base64" "log/slog" @@ -169,87 +168,42 @@ const ( ) func (f *FirefoxPassword) Parse(masterKey []byte) error { - globalSalt, metaBytes, nssA11, nssA102, err := getFirefoxDecryptKey(item.FirefoxKey4.TempFilename()) - if err != nil { - return err - } - metaPBE, err := crypto.NewASN1PBE(metaBytes) + logins, err := getFirefoxLoginData() if err != nil { return err } - k, err := metaPBE.Decrypt(globalSalt, masterKey) - if err != nil { - return 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 err - } - finallyKey, err := nssPBE.Decrypt(globalSalt, masterKey) - if err != nil { - return err - } - - finallyKey = finallyKey[:24] - logins, err := getFirefoxLoginData() - if err != nil { - return err - } - - for _, v := range logins { - userPBE, err := crypto.NewASN1PBE(v.encryptUser) - if err != nil { - return err - } - pwdPBE, err := crypto.NewASN1PBE(v.encryptPass) - if err != nil { - return err - } - user, err := userPBE.Decrypt(finallyKey, masterKey) - if err != nil { - return err - } - pwd, err := pwdPBE.Decrypt(finallyKey, masterKey) - if err != nil { - return err - } - *f = append(*f, loginData{ - LoginURL: v.LoginURL, - UserName: string(user), - Password: string(pwd), - CreateDate: v.CreateDate, - }) - } + for _, v := range logins { + userPBE, err := crypto.NewASN1PBE(v.encryptUser) + if err != nil { + return err } + pwdPBE, err := crypto.NewASN1PBE(v.encryptPass) + if err != nil { + return err + } + user, err := userPBE.Decrypt(masterKey) + if err != nil { + return err + } + pwd, err := pwdPBE.Decrypt(masterKey) + if err != nil { + return err + } + *f = append(*f, loginData{ + LoginURL: v.LoginURL, + UserName: string(user), + Password: string(pwd), + CreateDate: v.CreateDate, + }) } + sort.Slice(*f, func(i, j int) bool { return (*f)[i].CreateDate.After((*f)[j].CreateDate) }) return nil } -func getFirefoxDecryptKey(key4file string) (item1, item2, a11, a102 []byte, err error) { - keyDB, err := sql.Open("sqlite", key4file) - if err != nil { - return nil, nil, nil, nil, err - } - defer os.Remove(key4file) - defer keyDB.Close() - - if err = keyDB.QueryRow(queryMetaData).Scan(&item1, &item2); err != nil { - return nil, nil, nil, nil, err - } - - if err = keyDB.QueryRow(queryNssPrivate).Scan(&a11, &a102); err != nil { - return nil, nil, nil, nil, err - } - return item1, item2, a11, a102, nil -} - func getFirefoxLoginData() ([]loginData, error) { s, err := os.ReadFile(item.FirefoxPassword.TempFilename()) if err != nil { diff --git a/crypto/crypto.go b/crypto/crypto.go index 04d47d9..457f92a 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -20,7 +20,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) { @@ -60,9 +60,8 @@ type nssPBE struct { Encrypted []byte } -func (n nssPBE) Decrypt(globalSalt, masterPwd []byte) (key []byte, err error) { - glmp := append(globalSalt, masterPwd...) - hp := sha1.Sum(glmp) +func (n nssPBE) Decrypt(globalSalt []byte) (key []byte, err error) { + hp := sha1.Sum(globalSalt) s := append(hp[:], n.salt()...) chp := sha1.Sum(s) pes := paddingZero(n.salt(), 20) @@ -134,7 +133,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 +176,7 @@ type loginPBE struct { Encrypted []byte } -func (l loginPBE) Decrypt(globalSalt, _ []byte) (key []byte, err error) { +func (l loginPBE) Decrypt(globalSalt []byte) (key []byte, err error) { return des3Decrypt(globalSalt, l.iv(), l.encrypted()) } diff --git a/go.mod b/go.mod index c75893a..c77c977 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/moond4rk/hackbrowserdata go 1.21 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a github.com/godbus/dbus/v5 v5.1.0 github.com/otiai10/copy v1.14.0 diff --git a/go.sum b/go.sum index c4f10d9..8d75f8f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -23,6 +25,7 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=