From 0d1199301d89dd1ec441c5775f0ef1aea674a004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8D=E1=B4=8F=E1=B4=8F=C9=B4D4=CA=80=E1=B4=8B?= Date: Sat, 20 Jan 2024 16:20:50 +0800 Subject: [PATCH 1/4] chore: remove unused config file (#294) * docs: Remove unnecessary file. * docs: Refine CONTRIBUTING.md contribution guidelines. --- CONTRIBUTING.md | 2 +- typos.toml | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 typos.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f627b0f..a288010b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ Please adhere to the existing coding style for consistency. ## Questions -If you have any questions or need further guidance, please feel free to ask in the issue or PR, or [reach out to the maintainers](mailto:i@moond4rk.com). +If you have any questions or need further guidance, please feel free to ask in the issue or PR, or [reach out to the maintainers](mailto:i@moond4rk.com). We will reply to you as soon as possible. Thank you for your contribution! diff --git a/typos.toml b/typos.toml deleted file mode 100644 index 65fa5e39..00000000 --- a/typos.toml +++ /dev/null @@ -1,7 +0,0 @@ -# See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos -[default.extend-words] -Readed = "Readed" -Sie = "Sie" -OT = "OT" -[files] -extend-exclude = ["go.mod", "go.sum"] \ No newline at end of file From 3633c8c9d25e00024866b3438d5603dccb48ae56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8D=E1=B4=8F=E1=B4=8F=C9=B4D4=CA=80=E1=B4=8B?= Date: Wed, 24 Jan 2024 16:29:33 +0800 Subject: [PATCH 2/4] feat: refactor firefox get master key retrieval and decryption functions. (#300) * refactor: Simplify Firefox master key retrieval and decryption functions. - Simplify variable names and functions in browsingdata/password/password.go - Modify Decrypt function for each PBE type to have only one parameter named globalSalt in crypto/crypto.go - Implement functions to retrieve master key from Firefox's key4.db file and query metadata and private NSS data in browser/firefox/firefox.go * chore: Add dependencies and tests to Firefox package. - Add go-sqlmock and github.com/kisielk/sqlstruct dependencies - Add tests for Firefox package metadata and nssPrivate query - Add test for Firefox's processMasterKey function (currently commented out) * refactor: Refactor Firefox test functions, remove unused code - Remove unused test function in firefox_test.go file - Clean up code by removing unnecessary changes - Simplify file structure for easier maintenance and readability --- browser/firefox/firefox.go | 82 ++++++++++++++++++++++++++- browser/firefox/firefox_test.go | 38 +++++++++++++ browsingdata/password/password.go | 94 ++++++++----------------------- crypto/crypto.go | 11 ++-- go.mod | 1 + go.sum | 3 + 6 files changed, 152 insertions(+), 77 deletions(-) create mode 100644 browser/firefox/firefox_test.go diff --git a/browser/firefox/firefox.go b/browser/firefox/firefox.go index 31778e7e..7bde542e 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 00000000..74c9dacd --- /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 2915e8f6..6bf31923 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()) + logins, err := getFirefoxLoginData() if err != nil { return err } - metaPBE, err := crypto.NewASN1PBE(metaBytes) - 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 04d47d9d..457f92a7 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 c75893a2..c77c977e 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 c4f10d9f..8d75f8f5 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= From daa4bf4186f7a72b7e6a963c2d8cd6aa3c36079e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8D=E1=B4=8F=E1=B4=8F=C9=B4D4=CA=80=E1=B4=8B?= Date: Sat, 27 Jan 2024 22:30:28 +0800 Subject: [PATCH 3/4] feat: Refactor crypto decryption functions for consistency and error handling (#302) * feat: Refactor crypto decryption functions for consistency and error handling - Close #301 - Refactored and renamed decryption functions across multiple files for consistency - Updated cookie sorting method to sort in descending order - Added new encryption functions for AES in CBC and GCM modes and DES in CBC mode - Added error handling to decryption functions and created new error variables for invalid ciphertext length and decode failures - Test cases added for encryption and decryption functions - Removed unused code and imports. * chore: Add new words to .typos.toml dictionary - Add new terms to `.typos.toml` dictionary - Improve code formatting and readability - Refactor functions for better performance - Update comments and documentation - Resolve minor bugs and errors * refactor: Refactor crypto package for better structure and readability - Refactored and cleaned up crypto package code for better readability - Renamed `ToByteArray` method to `bytes` for consistency - Modified `DecryptWithDPAPI` method to use `outBlob.bytes()` for efficiency - Added comments and removed unused methods in `loginPBE` - Refactored `nssPBE` and `metaPBE` Decrypt methods to use `deriveKeyAndIV` helper method - Improved overall maintainability and organization of codebase * refactor: Refactor firefox password encryption and decryption. - Implement ASN1PBE interface with various PBE struct types and encryption/decryption methods - Fix naming and remove unused variables in browsingdata and crypto files - Add tests for ASN1PBE implementation using external assertion package - Refactor and improve error handling in firefox file functions related to master key retrieval - Add input validation and AES-GCM encryption function to crypto file --- .typos.toml | 2 + browser/chromium/chromium_windows.go | 2 +- browser/firefox/firefox.go | 45 ++-- browsingdata/cookie/cookie.go | 4 +- browsingdata/creditcard/creditcard.go | 8 +- browsingdata/password/password.go | 19 +- crypto/asn1pbe.go | 194 +++++++++++++++++ crypto/asn1pbe_test.go | 297 ++++++++++++++++++++++++++ crypto/crypto.go | 284 +++++++++--------------- crypto/crypto_darwin.go | 13 +- crypto/crypto_linux.go | 11 +- crypto/crypto_test.go | 72 +++++++ crypto/crypto_windows.go | 77 ++++--- 13 files changed, 750 insertions(+), 278 deletions(-) create mode 100644 crypto/asn1pbe.go create mode 100644 crypto/asn1pbe_test.go create mode 100644 crypto/crypto_test.go diff --git a/.typos.toml b/.typos.toml index 65fa5e39..1795e74a 100644 --- a/.typos.toml +++ b/.typos.toml @@ -3,5 +3,7 @@ Readed = "Readed" Sie = "Sie" OT = "OT" +Encrypter = "Encrypter" +Decrypter = "Decrypter" [files] extend-exclude = ["go.mod", "go.sum"] \ No newline at end of file diff --git a/browser/chromium/chromium_windows.go b/browser/chromium/chromium_windows.go index d0e68413..7b45191d 100644 --- a/browser/chromium/chromium_windows.go +++ b/browser/chromium/chromium_windows.go @@ -33,7 +33,7 @@ func (c *Chromium) GetMasterKey() ([]byte, error) { if err != nil { return nil, errDecodeMasterKeyFailed } - c.masterKey, err = crypto.DPAPI(key[5:]) + c.masterKey, err = crypto.DecryptWithDPAPI(key[5:]) if err != nil { slog.Error("decrypt master key failed", "err", err) return nil, err diff --git a/browser/firefox/firefox.go b/browser/firefox/firefox.go index 7bde542e..f8240e10 100644 --- a/browser/firefox/firefox.go +++ b/browser/firefox/firefox.go @@ -86,7 +86,7 @@ func (f *Firefox) GetMasterKey() ([]byte, error) { defer os.Remove(tempFilename) defer keyDB.Close() - globalSalt, metaBytes, err := queryMetaData(keyDB) + metaItem1, metaItem2, err := queryMetaData(keyDB) if err != nil { return nil, fmt.Errorf("query metadata error: %w", err) } @@ -96,16 +96,16 @@ func (f *Firefox) GetMasterKey() ([]byte, error) { return nil, fmt.Errorf("query NSS private error: %w", err) } - return processMasterKey(globalSalt, metaBytes, nssA11, nssA102) + return processMasterKey(metaItem1, metaItem2, 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 { + var metaItem1, metaItem2 []byte + if err := db.QueryRow(query).Scan(&metaItem1, &metaItem2); err != nil { return nil, nil, err } - return globalSalt, metaBytes, nil + return metaItem1, metaItem2, nil } func queryNssPrivate(db *sql.DB) ([]byte, []byte, error) { @@ -119,37 +119,40 @@ func queryNssPrivate(db *sql.DB) ([]byte, []byte, error) { // 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) +func processMasterKey(metaItem1, metaItem2, nssA11, nssA102 []byte) ([]byte, error) { + metaPBE, err := crypto.NewASN1PBE(metaItem2) if err != nil { - return nil, err + return nil, fmt.Errorf("error creating ASN1PBE from metaItem2: %w", err) } - k, err := metaPBE.Decrypt(globalSalt) + flag, err := metaPBE.Decrypt(metaItem1) if err != nil { - return nil, err + return nil, fmt.Errorf("error decrypting master key: %w", err) } + const passwordCheck = "password-check" - if !bytes.Contains(k, []byte("password-check")) { - return nil, errors.New("password-check not found") + if !bytes.Contains(flag, []byte(passwordCheck)) { + return nil, errors.New("flag verification failed: password-check not found") } - keyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + + var 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") + return nil, errors.New("master key verification failed: nssA102 not equal to expected value") } - nssPBE, err := crypto.NewASN1PBE(nssA11) + + nssA11PBE, err := crypto.NewASN1PBE(nssA11) if err != nil { - return nil, err + return nil, fmt.Errorf("error creating ASN1PBE from nssA11: %w", err) } - finallyKey, err := nssPBE.Decrypt(globalSalt) + + finallyKey, err := nssA11PBE.Decrypt(metaItem1) if err != nil { - return nil, err + return nil, fmt.Errorf("error decrypting final key: %w", err) } if len(finallyKey) < 24 { - return nil, errors.New("finallyKey length less than 24") + return nil, errors.New("length of final key is less than 24 bytes") } - finallyKey = finallyKey[:24] - return finallyKey, nil + return finallyKey[:24], nil } func (f *Firefox) Name() string { diff --git a/browsingdata/cookie/cookie.go b/browsingdata/cookie/cookie.go index b0141a4d..d1ecc3de 100644 --- a/browsingdata/cookie/cookie.go +++ b/browsingdata/cookie/cookie.go @@ -72,9 +72,9 @@ func (c *ChromiumCookie) Parse(masterKey []byte) error { } if len(encryptValue) > 0 { if len(masterKey) == 0 { - value, err = crypto.DPAPI(encryptValue) + value, err = crypto.DecryptWithDPAPI(encryptValue) } else { - value, err = crypto.DecryptPass(masterKey, encryptValue) + value, err = crypto.DecryptWithChromium(masterKey, encryptValue) } if err != nil { slog.Error("decrypt chromium cookie error", "err", err) diff --git a/browsingdata/creditcard/creditcard.go b/browsingdata/creditcard/creditcard.go index f893aea7..fe72f6e1 100644 --- a/browsingdata/creditcard/creditcard.go +++ b/browsingdata/creditcard/creditcard.go @@ -59,9 +59,9 @@ func (c *ChromiumCreditCard) Parse(masterKey []byte) error { } if len(encryptValue) > 0 { if len(masterKey) == 0 { - value, err = crypto.DPAPI(encryptValue) + value, err = crypto.DecryptWithDPAPI(encryptValue) } else { - value, err = crypto.DecryptPass(masterKey, encryptValue) + value, err = crypto.DecryptWithChromium(masterKey, encryptValue) } if err != nil { slog.Error("decrypt chromium credit card error", "err", err) @@ -114,9 +114,9 @@ func (c *YandexCreditCard) Parse(masterKey []byte) error { } if len(encryptValue) > 0 { if len(masterKey) == 0 { - value, err = crypto.DPAPI(encryptValue) + value, err = crypto.DecryptWithDPAPI(encryptValue) } else { - value, err = crypto.DecryptPass(masterKey, encryptValue) + value, err = crypto.DecryptWithChromium(masterKey, encryptValue) } if err != nil { slog.Error("decrypt chromium credit card error", "err", err) diff --git a/browsingdata/password/password.go b/browsingdata/password/password.go index 6bf31923..96067104 100644 --- a/browsingdata/password/password.go +++ b/browsingdata/password/password.go @@ -61,9 +61,9 @@ func (c *ChromiumPassword) Parse(masterKey []byte) error { } if len(pwd) > 0 { if len(masterKey) == 0 { - password, err = crypto.DPAPI(pwd) + password, err = crypto.DecryptWithDPAPI(pwd) } else { - password, err = crypto.DecryptPass(masterKey, pwd) + password, err = crypto.DecryptWithChromium(masterKey, pwd) } if err != nil { slog.Error("decrypt chromium password error", "err", err) @@ -129,9 +129,9 @@ func (c *YandexPassword) Parse(masterKey []byte) error { if len(pwd) > 0 { if len(masterKey) == 0 { - password, err = crypto.DPAPI(pwd) + password, err = crypto.DecryptWithDPAPI(pwd) } else { - password, err = crypto.DecryptPass(masterKey, pwd) + password, err = crypto.DecryptWithChromium(masterKey, pwd) } if err != nil { slog.Error("decrypt yandex password error", "err", err) @@ -162,12 +162,7 @@ func (c *YandexPassword) Len() int { type FirefoxPassword []loginData -const ( - queryMetaData = `SELECT item1, item2 FROM metaData WHERE id = 'password'` - queryNssPrivate = `SELECT a11, a102 from nssPrivate` -) - -func (f *FirefoxPassword) Parse(masterKey []byte) error { +func (f *FirefoxPassword) Parse(globalSalt []byte) error { logins, err := getFirefoxLoginData() if err != nil { return err @@ -182,11 +177,11 @@ func (f *FirefoxPassword) Parse(masterKey []byte) error { if err != nil { return err } - user, err := userPBE.Decrypt(masterKey) + user, err := userPBE.Decrypt(globalSalt) if err != nil { return err } - pwd, err := pwdPBE.Decrypt(masterKey) + pwd, err := pwdPBE.Decrypt(globalSalt) if err != nil { return err } diff --git a/crypto/asn1pbe.go b/crypto/asn1pbe.go new file mode 100644 index 00000000..beb9eb27 --- /dev/null +++ b/crypto/asn1pbe.go @@ -0,0 +1,194 @@ +package crypto + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/asn1" + "errors" + + "golang.org/x/crypto/pbkdf2" +) + +type ASN1PBE interface { + Decrypt(globalSalt []byte) ([]byte, error) + + Encrypt(globalSalt, plaintext []byte) ([]byte, error) +} + +func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) { + var ( + nss nssPBE + meta metaPBE + login loginPBE + ) + if _, err := asn1.Unmarshal(b, &nss); err == nil { + return nss, nil + } + if _, err := asn1.Unmarshal(b, &meta); err == nil { + return meta, nil + } + if _, err := asn1.Unmarshal(b, &login); err == nil { + return login, nil + } + return nil, ErrDecodeASN1Failed +} + +var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed") + +// nssPBE Struct +// +// SEQUENCE (2 elem) +// OBJECT IDENTIFIER +// SEQUENCE (2 elem) +// OCTET STRING (20 byte) +// INTEGER 1 +// OCTET STRING (16 byte) +type nssPBE struct { + AlgoAttr struct { + asn1.ObjectIdentifier + SaltAttr struct { + EntrySalt []byte + Len int + } + } + Encrypted []byte +} + +// Decrypt decrypts the encrypted password with the global salt. +func (n nssPBE) Decrypt(globalSalt []byte) ([]byte, error) { + key, iv := n.deriveKeyAndIV(globalSalt) + + return DES3Decrypt(key, iv, n.Encrypted) +} + +func (n nssPBE) Encrypt(globalSalt []byte, plaintext []byte) ([]byte, error) { + key, iv := n.deriveKeyAndIV(globalSalt) + + return DES3Encrypt(key, iv, plaintext) +} + +// deriveKeyAndIV derives the key and initialization vector (IV) +// from the global salt and entry salt. +func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { + salt := n.AlgoAttr.SaltAttr.EntrySalt + hashPrefix := sha1.Sum(globalSalt) + compositeHash := sha1.Sum(append(hashPrefix[:], salt...)) + paddedEntrySalt := paddingZero(salt, 20) + + hmacProcessor := hmac.New(sha1.New, compositeHash[:]) + hmacProcessor.Write(paddedEntrySalt) + + paddedEntrySalt = append(paddedEntrySalt, salt...) + keyComponent1 := hmac.New(sha1.New, compositeHash[:]) + keyComponent1.Write(paddedEntrySalt) + + hmacWithSalt := append(hmacProcessor.Sum(nil), salt...) + keyComponent2 := hmac.New(sha1.New, compositeHash[:]) + keyComponent2.Write(hmacWithSalt) + + key := append(keyComponent1.Sum(nil), keyComponent2.Sum(nil)...) + iv := key[len(key)-8:] + return key[:24], iv +} + +// MetaPBE Struct +// +// SEQUENCE (2 elem) +// OBJECT IDENTIFIER +// SEQUENCE (2 elem) +// SEQUENCE (2 elem) +// OBJECT IDENTIFIER +// SEQUENCE (4 elem) +// OCTET STRING (32 byte) +// INTEGER 1 +// INTEGER 32 +// SEQUENCE (1 elem) +// OBJECT IDENTIFIER +// SEQUENCE (2 elem) +// OBJECT IDENTIFIER +// OCTET STRING (14 byte) +// OCTET STRING (16 byte) +type metaPBE struct { + AlgoAttr algoAttr + Encrypted []byte +} + +type algoAttr struct { + asn1.ObjectIdentifier + Data struct { + Data struct { + asn1.ObjectIdentifier + SlatAttr slatAttr + } + IVData ivAttr + } +} + +type ivAttr struct { + asn1.ObjectIdentifier + IV []byte +} + +type slatAttr struct { + EntrySalt []byte + IterationCount int + KeySize int + Algorithm struct { + asn1.ObjectIdentifier + } +} + +func (m metaPBE) Decrypt(globalSalt []byte) ([]byte, error) { + key, iv := m.deriveKeyAndIV(globalSalt) + + return AES128CBCDecrypt(key, iv, m.Encrypted) +} + +func (m metaPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) { + key, iv := m.deriveKeyAndIV(globalSalt) + + return AES128CBCEncrypt(key, iv, plaintext) +} + +func (m metaPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { + password := sha1.Sum(globalSalt) + + salt := m.AlgoAttr.Data.Data.SlatAttr.EntrySalt + iter := m.AlgoAttr.Data.Data.SlatAttr.IterationCount + keyLen := m.AlgoAttr.Data.Data.SlatAttr.KeySize + + key := pbkdf2.Key(password[:], salt, iter, keyLen, sha256.New) + iv := append([]byte{4, 14}, m.AlgoAttr.Data.IVData.IV...) + return key, iv +} + +// loginPBE Struct +// +// OCTET STRING (16 byte) +// SEQUENCE (2 elem) +// OBJECT IDENTIFIER +// OCTET STRING (8 byte) +// OCTET STRING (16 byte) +type loginPBE struct { + CipherText []byte + Data struct { + asn1.ObjectIdentifier + IV []byte + } + Encrypted []byte +} + +func (l loginPBE) Decrypt(globalSalt []byte) ([]byte, error) { + key, iv := l.deriveKeyAndIV(globalSalt) + return DES3Decrypt(key, iv, l.Encrypted) +} + +func (l loginPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) { + key, iv := l.deriveKeyAndIV(globalSalt) + return DES3Encrypt(key, iv, plaintext) +} + +func (l loginPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { + return globalSalt, l.Data.IV +} diff --git a/crypto/asn1pbe_test.go b/crypto/asn1pbe_test.go new file mode 100644 index 00000000..a45d231f --- /dev/null +++ b/crypto/asn1pbe_test.go @@ -0,0 +1,297 @@ +package crypto + +import ( + "bytes" + "encoding/asn1" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + pbeIV = []byte("01234567") // 8 bytes + pbePlaintext = []byte("Hello, World!") + pbeCipherText = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1} + objWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3} + objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46} + objWithSHA1AndAES = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} + nssPBETestCases = []struct { + RawHexPBE string + GlobalSalt []byte + Encrypted []byte + IterationCount int + Len int + Plaintext []byte + ObjectIdentifier asn1.ObjectIdentifier + }{ + { + RawHexPBE: "303e302a06092a864886f70d01050d301d04186d6f6f6e6434726b6d6f6f6e6434726b6d6f6f6e6434726b020114041095183a14c752e7b1d0aaa47f53e05097", + GlobalSalt: bytes.Repeat([]byte(baseKey), 3), + Encrypted: []byte{0x95, 0x18, 0x3a, 0x14, 0xc7, 0x52, 0xe7, 0xb1, 0xd0, 0xaa, 0xa4, 0x7f, 0x53, 0xe0, 0x50, 0x97}, + Plaintext: pbePlaintext, + IterationCount: 1, + Len: 32, + ObjectIdentifier: objWithSHA1AndAES, + }, + } + metaPBETestCases = []struct { + RawHexPBE string + GlobalSalt []byte + Encrypted []byte + IV []byte + Plaintext []byte + ObjectIdentifier asn1.ObjectIdentifier + }{ + { + RawHexPBE: "307a3066060960864801650304012e3059303a060960864801650304012e302d04186d6f6f6e6434726b6d6f6f6e6434726b6d6f6f6e6434726b020101020120300b060960864801650304012e301b060960864801650304012e040e303132333435363730313233343504100474679f2e6256518b7adb877beaa154", + GlobalSalt: bytes.Repeat([]byte(baseKey), 3), + Encrypted: []byte{0x4, 0x74, 0x67, 0x9f, 0x2e, 0x62, 0x56, 0x51, 0x8b, 0x7a, 0xdb, 0x87, 0x7b, 0xea, 0xa1, 0x54}, + IV: bytes.Repeat(pbeIV, 2)[:14], + Plaintext: pbePlaintext, + ObjectIdentifier: objWithSHA256AndAES, + }, + } + loginPBETestCases = []struct { + RawHexPBE string + GlobalSalt []byte + Encrypted []byte + IV []byte + Plaintext []byte + ObjectIdentifier asn1.ObjectIdentifier + }{ + { + RawHexPBE: "303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45", + Encrypted: []byte{0xfe, 0x96, 0x8b, 0x65, 0x65, 0x14, 0x91, 0x14, 0xea, 0x68, 0x8d, 0xef, 0xd6, 0x68, 0x3e, 0x45}, + GlobalSalt: bytes.Repeat([]byte(baseKey), 3), + IV: pbeIV, + Plaintext: pbePlaintext, + ObjectIdentifier: objWithMD5AndDESCBC, + }, + } +) + +func TestNewASN1PBE(t *testing.T) { + for _, tc := range nssPBETestCases { + nssRaw, err := hex.DecodeString(tc.RawHexPBE) + assert.Equal(t, nil, err) + pbe, err := NewASN1PBE(nssRaw) + assert.Equal(t, nil, err) + nssPBETC, ok := pbe.(nssPBE) + assert.Equal(t, true, ok) + assert.Equal(t, nssPBETC.Encrypted, tc.Encrypted) + assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt) + assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.Len, 20) + assert.Equal(t, nssPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier) + } +} + +func TestNssPBE_Encrypt(t *testing.T) { + for _, tc := range nssPBETestCases { + nssPBETC := nssPBE{ + Encrypted: tc.Encrypted, + AlgoAttr: struct { + asn1.ObjectIdentifier + SaltAttr struct { + EntrySalt []byte + Len int + } + }{ + ObjectIdentifier: tc.ObjectIdentifier, + SaltAttr: struct { + EntrySalt []byte + Len int + }{ + EntrySalt: tc.GlobalSalt, + Len: 20, + }, + }, + } + encrypted, err := nssPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(encrypted) > 0) + assert.Equal(t, nssPBETC.Encrypted, encrypted) + } +} + +func TestNssPBE_Decrypt(t *testing.T) { + for _, tc := range nssPBETestCases { + nssPBETC := nssPBE{ + Encrypted: tc.Encrypted, + AlgoAttr: struct { + asn1.ObjectIdentifier + SaltAttr struct { + EntrySalt []byte + Len int + } + }{ + ObjectIdentifier: tc.ObjectIdentifier, + SaltAttr: struct { + EntrySalt []byte + Len int + }{ + EntrySalt: tc.GlobalSalt, + Len: 20, + }, + }, + } + decrypted, err := nssPBETC.Decrypt(tc.GlobalSalt) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(decrypted) > 0) + assert.Equal(t, pbePlaintext, decrypted) + } +} + +func TestNewASN1PBE_MetaPBE(t *testing.T) { + for _, tc := range metaPBETestCases { + metaRaw, err := hex.DecodeString(tc.RawHexPBE) + assert.Equal(t, nil, err) + pbe, err := NewASN1PBE(metaRaw) + assert.Equal(t, nil, err) + metaPBETC, ok := pbe.(metaPBE) + assert.Equal(t, true, ok) + assert.Equal(t, metaPBETC.Encrypted, tc.Encrypted) + assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.IV, tc.IV) + assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.ObjectIdentifier, objWithSHA256AndAES) + } +} + +func TestMetaPBE_Encrypt(t *testing.T) { + for _, tc := range metaPBETestCases { + metaPBETC := metaPBE{ + AlgoAttr: algoAttr{ + ObjectIdentifier: tc.ObjectIdentifier, + Data: struct { + Data struct { + asn1.ObjectIdentifier + SlatAttr slatAttr + } + IVData ivAttr + }{ + Data: struct { + asn1.ObjectIdentifier + SlatAttr slatAttr + }{ + ObjectIdentifier: tc.ObjectIdentifier, + SlatAttr: slatAttr{ + EntrySalt: tc.GlobalSalt, + IterationCount: 1, + KeySize: 32, + Algorithm: struct { + asn1.ObjectIdentifier + }{ + ObjectIdentifier: tc.ObjectIdentifier, + }, + }, + }, + IVData: ivAttr{ + ObjectIdentifier: tc.ObjectIdentifier, + IV: tc.IV, + }, + }, + }, + Encrypted: tc.Encrypted, + } + encrypted, err := metaPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(encrypted) > 0) + assert.Equal(t, metaPBETC.Encrypted, encrypted) + } +} + +func TestMetaPBE_Decrypt(t *testing.T) { + for _, tc := range metaPBETestCases { + metaPBETC := metaPBE{ + AlgoAttr: algoAttr{ + ObjectIdentifier: tc.ObjectIdentifier, + Data: struct { + Data struct { + asn1.ObjectIdentifier + SlatAttr slatAttr + } + IVData ivAttr + }{ + Data: struct { + asn1.ObjectIdentifier + SlatAttr slatAttr + }{ + ObjectIdentifier: tc.ObjectIdentifier, + SlatAttr: slatAttr{ + EntrySalt: tc.GlobalSalt, + IterationCount: 1, + KeySize: 32, + Algorithm: struct { + asn1.ObjectIdentifier + }{ + ObjectIdentifier: tc.ObjectIdentifier, + }, + }, + }, + IVData: ivAttr{ + ObjectIdentifier: tc.ObjectIdentifier, + IV: tc.IV, + }, + }, + }, + Encrypted: tc.Encrypted, + } + decrypted, err := metaPBETC.Decrypt(tc.GlobalSalt) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(decrypted) > 0) + assert.Equal(t, pbePlaintext, decrypted) + } +} + +func TestNewASN1PBE_LoginPBE(t *testing.T) { + for _, tc := range loginPBETestCases { + loginRaw, err := hex.DecodeString(tc.RawHexPBE) + assert.Equal(t, nil, err) + pbe, err := NewASN1PBE(loginRaw) + assert.Equal(t, nil, err) + loginPBETC, ok := pbe.(loginPBE) + assert.Equal(t, true, ok) + assert.Equal(t, loginPBETC.Encrypted, tc.Encrypted) + assert.Equal(t, loginPBETC.Data.IV, tc.IV) + assert.Equal(t, loginPBETC.Data.ObjectIdentifier, objWithMD5AndDESCBC) + } +} + +func TestLoginPBE_Encrypt(t *testing.T) { + for _, tc := range loginPBETestCases { + loginPBETC := loginPBE{ + CipherText: pbeCipherText, + Data: struct { + asn1.ObjectIdentifier + IV []byte + }{ + ObjectIdentifier: tc.ObjectIdentifier, + IV: tc.IV, + }, + Encrypted: tc.Encrypted, + } + encrypted, err := loginPBETC.Encrypt(tc.GlobalSalt, plainText) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(encrypted) > 0) + assert.Equal(t, loginPBETC.Encrypted, encrypted) + } +} + +func TestLoginPBE_Decrypt(t *testing.T) { + for _, tc := range loginPBETestCases { + loginPBETC := loginPBE{ + CipherText: pbeCipherText, + Data: struct { + asn1.ObjectIdentifier + IV []byte + }{ + ObjectIdentifier: tc.ObjectIdentifier, + IV: tc.IV, + }, + Encrypted: tc.Encrypted, + } + decrypted, err := loginPBETC.Decrypt(tc.GlobalSalt) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(decrypted) > 0) + assert.Equal(t, pbePlaintext, decrypted) + } +} diff --git a/crypto/crypto.go b/crypto/crypto.go index 457f92a7..9638345e 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -1,238 +1,150 @@ package crypto import ( + "bytes" "crypto/aes" "crypto/cipher" "crypto/des" - "crypto/hmac" - "crypto/sha1" - "crypto/sha256" - "encoding/asn1" "errors" - - "golang.org/x/crypto/pbkdf2" + "fmt" ) var ( - errPasswordIsEmpty = errors.New("password is empty") - errDecodeASN1Failed = errors.New("decode ASN1 data failed") - errEncryptedLength = errors.New("length of encrypted password less than block size") + ErrCiphertextLengthIsInvalid = errors.New("ciphertext length is invalid") ) -type ASN1PBE interface { - Decrypt(globalSalt []byte) (key []byte, err error) -} - -func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) { - var ( - n nssPBE - m metaPBE - l loginPBE - ) - if _, err := asn1.Unmarshal(b, &n); err == nil { - return n, nil +func AES128CBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err } - if _, err := asn1.Unmarshal(b, &m); err == nil { - return m, nil + // Check ciphertext length + if len(ciphertext) < aes.BlockSize { + return nil, errors.New("AES128CBCDecrypt: ciphertext too short") } - if _, err := asn1.Unmarshal(b, &l); err == nil { - return l, nil + if len(ciphertext)%aes.BlockSize != 0 { + return nil, errors.New("AES128CBCDecrypt: ciphertext is not a multiple of the block size") } - return nil, errDecodeASN1Failed -} - -// nssPBE Struct -// -// SEQUENCE (2 elem) -// OBJECT IDENTIFIER -// SEQUENCE (2 elem) -// OCTET STRING (20 byte) -// INTEGER 1 -// OCTET STRING (16 byte) -type nssPBE struct { - AlgoAttr struct { - asn1.ObjectIdentifier - SaltAttr struct { - EntrySalt []byte - Len int - } - } - Encrypted []byte -} - -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) - tk := hmac.New(sha1.New, chp[:]) - tk.Write(pes) - pes = append(pes, n.salt()...) - k1 := hmac.New(sha1.New, chp[:]) - k1.Write(pes) - tkPlus := append(tk.Sum(nil), n.salt()...) - k2 := hmac.New(sha1.New, chp[:]) - k2.Write(tkPlus) - k := append(k1.Sum(nil), k2.Sum(nil)...) - iv := k[len(k)-8:] - return des3Decrypt(k[:24], iv, n.encrypted()) -} - -func (n nssPBE) salt() []byte { - return n.AlgoAttr.SaltAttr.EntrySalt -} - -func (n nssPBE) encrypted() []byte { - return n.Encrypted -} -// MetaPBE Struct -// -// SEQUENCE (2 elem) -// OBJECT IDENTIFIER -// SEQUENCE (2 elem) -// SEQUENCE (2 elem) -// OBJECT IDENTIFIER -// SEQUENCE (4 elem) -// OCTET STRING (32 byte) -// INTEGER 1 -// INTEGER 32 -// SEQUENCE (1 elem) -// OBJECT IDENTIFIER -// SEQUENCE (2 elem) -// OBJECT IDENTIFIER -// OCTET STRING (14 byte) -// OCTET STRING (16 byte) -type metaPBE struct { - AlgoAttr algoAttr - Encrypted []byte -} + decryptedData := make([]byte, len(ciphertext)) + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(decryptedData, ciphertext) -type algoAttr struct { - asn1.ObjectIdentifier - Data struct { - Data struct { - asn1.ObjectIdentifier - SlatAttr slatAttr - } - IVData ivAttr + // unpad the decrypted data and handle potential padding errors + decryptedData, err = pkcs5UnPadding(decryptedData) + if err != nil { + return nil, fmt.Errorf("AES128CBCDecrypt: %w", err) } -} -type ivAttr struct { - asn1.ObjectIdentifier - IV []byte + return decryptedData, nil } -type slatAttr struct { - EntrySalt []byte - IterationCount int - KeySize int - Algorithm struct { - asn1.ObjectIdentifier +func AES128CBCEncrypt(key, iv, plaintext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err } -} -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()...) - return aes128CBCDecrypt(key, iv, m.encrypted()) -} + if len(iv) != aes.BlockSize { + return nil, errors.New("AES128CBCEncrypt: iv length is invalid, must equal block size") + } -func (m metaPBE) salt() []byte { - return m.AlgoAttr.Data.Data.SlatAttr.EntrySalt -} + plaintext = pkcs5Padding(plaintext, block.BlockSize()) + encryptedData := make([]byte, len(plaintext)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(encryptedData, plaintext) -func (m metaPBE) iterationCount() int { - return m.AlgoAttr.Data.Data.SlatAttr.IterationCount + return encryptedData, nil } -func (m metaPBE) keySize() int { - return m.AlgoAttr.Data.Data.SlatAttr.KeySize -} +func DES3Decrypt(key, iv []byte, ciphertext []byte) ([]byte, error) { + block, err := des.NewTripleDESCipher(key) + if err != nil { + return nil, err + } + if len(ciphertext) < des.BlockSize { + return nil, errors.New("DES3Decrypt: ciphertext too short") + } + if len(ciphertext)%block.BlockSize() != 0 { + return nil, errors.New("DES3Decrypt: ciphertext is not a multiple of the block size") + } -func (m metaPBE) iv() []byte { - return m.AlgoAttr.Data.IVData.IV -} + blockMode := cipher.NewCBCDecrypter(block, iv) + sq := make([]byte, len(ciphertext)) + blockMode.CryptBlocks(sq, ciphertext) -func (m metaPBE) encrypted() []byte { - return m.Encrypted + return pkcs5UnPadding(sq) } -// loginPBE Struct -// -// OCTET STRING (16 byte) -// SEQUENCE (2 elem) -// OBJECT IDENTIFIER -// OCTET STRING (8 byte) -// OCTET STRING (16 byte) -type loginPBE struct { - CipherText []byte - Data struct { - asn1.ObjectIdentifier - IV []byte - } - Encrypted []byte -} +func DES3Encrypt(key, iv []byte, plaintext []byte) ([]byte, error) { + block, err := des.NewTripleDESCipher(key) + if err != nil { + return nil, err + } -func (l loginPBE) Decrypt(globalSalt []byte) (key []byte, err error) { - return des3Decrypt(globalSalt, l.iv(), l.encrypted()) -} + plaintext = pkcs5Padding(plaintext, block.BlockSize()) + dst := make([]byte, len(plaintext)) + blockMode := cipher.NewCBCEncrypter(block, iv) + blockMode.CryptBlocks(dst, plaintext) -func (l loginPBE) iv() []byte { - return l.Data.IV + return dst, nil } -func (l loginPBE) encrypted() []byte { - return l.Encrypted +// AESGCMDecrypt chromium > 80 https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_win.cc +func AESGCMDecrypt(key, nounce, ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockMode, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + origData, err := blockMode.Open(nil, nounce, ciphertext, nil) + if err != nil { + return nil, err + } + return origData, nil } -func aes128CBCDecrypt(key, iv, encryptPass []byte) ([]byte, error) { +// AESGCMEncrypt encrypts plaintext using AES encryption in GCM mode. +func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - encryptLen := len(encryptPass) - if encryptLen < block.BlockSize() { - return nil, errEncryptedLength + blockMode, err := cipher.NewGCM(block) + if err != nil { + return nil, err } - - dst := make([]byte, encryptLen) - mode := cipher.NewCBCDecrypter(block, iv) - mode.CryptBlocks(dst, encryptPass) - dst = pkcs5UnPadding(dst, block.BlockSize()) - return dst, nil + // The first parameter is the prefix for the output, we can leave it nil. + // The Seal method encrypts and authenticates the data, appending the result to the dst. + encryptedData := blockMode.Seal(nil, nonce, plaintext, nil) + return encryptedData, nil } -func pkcs5UnPadding(src []byte, blockSize int) []byte { - n := len(src) - paddingNum := int(src[n-1]) - if n < paddingNum || paddingNum > blockSize { +func paddingZero(src []byte, length int) []byte { + padding := length - len(src) + if padding <= 0 { return src } - return src[:n-paddingNum] + return append(src, make([]byte, padding)...) } -// des3Decrypt use for decrypt firefox PBE -func des3Decrypt(key, iv []byte, src []byte) ([]byte, error) { - block, err := des.NewTripleDESCipher(key) - if err != nil { - return nil, err +func pkcs5UnPadding(src []byte) ([]byte, error) { + length := len(src) + if length == 0 { + return nil, errors.New("pkcs5UnPadding: src should not be empty") } - blockMode := cipher.NewCBCDecrypter(block, iv) - sq := make([]byte, len(src)) - blockMode.CryptBlocks(sq, src) - return pkcs5UnPadding(sq, block.BlockSize()), nil + padding := int(src[length-1]) + if padding < 1 || padding > aes.BlockSize { + return nil, errors.New("pkcs5UnPadding: invalid padding size") + } + return src[:length-padding], nil } -func paddingZero(s []byte, l int) []byte { - h := l - len(s) - if h <= 0 { - return s - } - for i := len(s); i < l; i++ { - s = append(s, 0) - } - return s +func pkcs5Padding(src []byte, blocksize int) []byte { + padding := blocksize - (len(src) % blocksize) + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(src, padText...) } diff --git a/crypto/crypto_darwin.go b/crypto/crypto_darwin.go index f3aacef4..691a065a 100644 --- a/crypto/crypto_darwin.go +++ b/crypto/crypto_darwin.go @@ -2,15 +2,14 @@ package crypto -var iv = []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32} - -func DecryptPass(key, encryptPass []byte) ([]byte, error) { - if len(encryptPass) <= 3 { - return nil, errPasswordIsEmpty +func DecryptWithChromium(key, password []byte) ([]byte, error) { + if len(password) <= 3 { + return nil, ErrCiphertextLengthIsInvalid } - return aes128CBCDecrypt(key, iv, encryptPass[3:]) + var iv = []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32} + return AES128CBCDecrypt(key, iv, password[3:]) } -func DPAPI(_ []byte) ([]byte, error) { +func DecryptWithDPAPI(_ []byte) ([]byte, error) { return nil, nil } diff --git a/crypto/crypto_linux.go b/crypto/crypto_linux.go index cac4118a..81e85877 100644 --- a/crypto/crypto_linux.go +++ b/crypto/crypto_linux.go @@ -2,15 +2,14 @@ package crypto -var iv = []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32} - -func DecryptPass(key, encryptPass []byte) ([]byte, error) { +func DecryptWithChromium(key, encryptPass []byte) ([]byte, error) { if len(encryptPass) < 3 { - return nil, errPasswordIsEmpty + return nil, ErrCiphertextLengthIsInvalid } - return aes128CBCDecrypt(key, iv, encryptPass[3:]) + var iv = []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32} + return AES128CBCDecrypt(key, iv, encryptPass[3:]) } -func DPAPI(_ []byte) ([]byte, error) { +func DecryptWithDPAPI(_ []byte) ([]byte, error) { return nil, nil } diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go new file mode 100644 index 00000000..e1c596a5 --- /dev/null +++ b/crypto/crypto_test.go @@ -0,0 +1,72 @@ +package crypto + +import ( + "bytes" + "crypto/sha1" + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +const baseKey = "moond4rk" + +var ( + aesKey = bytes.Repeat([]byte(baseKey), 2) // 16 bytes + aesIV = []byte("01234567abcdef01") // 16 bytes + plainText = []byte("Hello, World!") + aes128Ciphertext = "19381468ecf824c0bfc7a89eed9777d2" + + des3Key = sha1.New().Sum(aesKey)[:24] + des3IV = aesIV[:8] + des3Ciphertext = "a4492f31bc404fae18d53a46ca79282e" + + aesGCMNonce = aesKey[:12] + aesGCMCiphertext = "6c49dac89992639713edab3a114c450968a08b53556872cea3919e2e9a" +) + +func TestAES128CBCEncrypt(t *testing.T) { + encrypted, err := AES128CBCEncrypt(aesKey, aesIV, plainText) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(encrypted) > 0) + assert.Equal(t, aes128Ciphertext, fmt.Sprintf("%x", encrypted)) +} + +func TestAES128CBCDecrypt(t *testing.T) { + ciphertext, _ := hex.DecodeString(aes128Ciphertext) + decrypted, err := AES128CBCDecrypt(aesKey, aesIV, ciphertext) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(decrypted) > 0) + assert.Equal(t, plainText, decrypted) +} + +func TestDES3Encrypt(t *testing.T) { + encrypted, err := DES3Encrypt(des3Key, des3IV, plainText) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(encrypted) > 0) + assert.Equal(t, des3Ciphertext, fmt.Sprintf("%x", encrypted)) +} + +func TestDES3Decrypt(t *testing.T) { + ciphertext, _ := hex.DecodeString(des3Ciphertext) + decrypted, err := DES3Decrypt(des3Key, des3IV, ciphertext) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(decrypted) > 0) + assert.Equal(t, plainText, decrypted) +} + +func TestAESGCMEncrypt(t *testing.T) { + encrypted, err := AESGCMEncrypt(aesKey, aesGCMNonce, plainText) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(encrypted) > 0) + assert.Equal(t, aesGCMCiphertext, fmt.Sprintf("%x", encrypted)) +} + +func TestAESGCMDecrypt(t *testing.T) { + ciphertext, _ := hex.DecodeString(aesGCMCiphertext) + decrypted, err := AESGCMDecrypt(aesKey, aesGCMNonce, ciphertext) + assert.Equal(t, nil, err) + assert.Equal(t, true, len(decrypted) > 0) + assert.Equal(t, plainText, decrypted) +} diff --git a/crypto/crypto_windows.go b/crypto/crypto_windows.go index 33218983..fdf5b156 100644 --- a/crypto/crypto_windows.go +++ b/crypto/crypto_windows.go @@ -3,47 +3,41 @@ package crypto import ( - "crypto/aes" - "crypto/cipher" + "fmt" "syscall" "unsafe" ) -func DecryptPass(key, encryptPass []byte) ([]byte, error) { - if len(encryptPass) < 15 { - return nil, errPasswordIsEmpty +const ( + // Assuming the nonce size is 12 bytes and the minimum encrypted data size is 3 bytes + minEncryptedDataSize = 15 + nonceSize = 12 +) + +func DecryptWithChromium(key, ciphertext []byte) ([]byte, error) { + if len(ciphertext) < minEncryptedDataSize { + return nil, ErrCiphertextLengthIsInvalid } - return aesGCMDecrypt(encryptPass[15:], key, encryptPass[3:15]) + nonce := ciphertext[3 : 3+nonceSize] + encryptedPassword := ciphertext[3+nonceSize:] + + return AESGCMDecrypt(key, nonce, encryptedPassword) } -func DecryptPassForYandex(key, encryptPass []byte) ([]byte, error) { - if len(encryptPass) < 3 { - return nil, errPasswordIsEmpty +// DecryptWithYandex decrypts the password with AES-GCM +func DecryptWithYandex(key, ciphertext []byte) ([]byte, error) { + if len(ciphertext) < minEncryptedDataSize { + return nil, ErrCiphertextLengthIsInvalid } // remove Prefix 'v10' // gcmBlockSize = 16 // gcmTagSize = 16 // gcmMinimumTagSize = 12 // NIST SP 800-38D recommends tags with 12 or more bytes. // gcmStandardNonceSize = 12 - return aesGCMDecrypt(encryptPass[12:], key, encryptPass[0:12]) -} - -// chromium > 80 https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_win.cc -func aesGCMDecrypt(encrypted, key, nounce []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - blockMode, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - origData, err := blockMode.Open(nil, nounce, encrypted, nil) - if err != nil { - return nil, err - } - return origData, nil + nonce := ciphertext[3 : 3+nonceSize] + encryptedPassword := ciphertext[3+nonceSize:] + return AESGCMDecrypt(key, nonce, encryptedPassword) } type dataBlob struct { @@ -61,27 +55,32 @@ func newBlob(d []byte) *dataBlob { } } -func (b *dataBlob) ToByteArray() []byte { +func (b *dataBlob) bytes() []byte { d := make([]byte, b.cbData) copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:]) return d } -// DPAPI (Data Protection Application Programming Interface) +// DecryptWithDPAPI (Data Protection Application Programming Interface) // is a simple cryptographic application programming interface // available as a built-in component in Windows 2000 and // later versions of Microsoft Windows operating systems -// chrome < 80 https://chromium.googlesource.com/chromium/src/+/76f496a7235c3432983421402951d73905c8be96/components/os_crypt/os_crypt_win.cc#82 -func DPAPI(data []byte) ([]byte, error) { - dllCrypt := syscall.NewLazyDLL("Crypt32.dll") - dllKernel := syscall.NewLazyDLL("Kernel32.dll") - procDecryptData := dllCrypt.NewProc("CryptUnprotectData") - procLocalFree := dllKernel.NewProc("LocalFree") +func DecryptWithDPAPI(ciphertext []byte) ([]byte, error) { + crypt32 := syscall.NewLazyDLL("Crypt32.dll") + kernel32 := syscall.NewLazyDLL("Kernel32.dll") + unprotectDataProc := crypt32.NewProc("CryptUnprotectData") + localFreeProc := kernel32.NewProc("LocalFree") + var outBlob dataBlob - r, _, err := procDecryptData.Call(uintptr(unsafe.Pointer(newBlob(data))), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&outBlob))) + r, _, err := unprotectDataProc.Call( + uintptr(unsafe.Pointer(newBlob(ciphertext))), + 0, 0, 0, 0, 0, + uintptr(unsafe.Pointer(&outBlob)), + ) if r == 0 { - return nil, err + return nil, fmt.Errorf("CryptUnprotectData failed with error %w", err) } - defer procLocalFree.Call(uintptr(unsafe.Pointer(outBlob.pbData))) - return outBlob.ToByteArray(), nil + + defer localFreeProc.Call(uintptr(unsafe.Pointer(outBlob.pbData))) + return outBlob.bytes(), nil } From 7795f3a98b6893698efd67d65086e4125baef9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8D=E1=B4=8F=E1=B4=8F=C9=B4D4=CA=80=E1=B4=8B?= Date: Sat, 27 Jan 2024 22:54:19 +0800 Subject: [PATCH 4/4] feat: Upgrade GitHub Actions to v4 for greater stability (#304) * ci: Upgrade GitHub Actions to v4 for greater stability - Close #303 - Upgrade GitHub Actions to v4 in all workflows - Fix zipping and uploading of Linux arm64 assets in release workflow - Simplify and remove unnecessary steps in build workflow --- .github/workflows/build.yml | 11 +++-------- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/unittest.yml | 10 +++++----- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2eef248a..2e687d9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,16 +18,13 @@ jobs: steps: - name: Set up Go ${{ matrix.goVer }} - uses: actions/setup-go@v1 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.goVer }} id: go - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Set Git to handle line endings consistently - run: git config --global core.autocrlf false + uses: actions/checkout@v4 - name: Format Check if: matrix.os != 'windows-latest' @@ -35,9 +32,7 @@ jobs: diff -u <(echo -n) <(gofmt -d .) - name: Get dependencies - run: | - go get -v -t -d ./... - go get gopkg.in/check.v1 + run: go get -v ./... - name: Build run: go build -v ./... diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1e053657..bdee8309 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Set Golang - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: "1.21.x" - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check spelling with custom config file uses: crate-ci/typos@master diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f97f5b6d..5c2da141 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,10 +8,10 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Golang - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: "1.21.x" diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index b90d3cc2..ba65018a 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -5,7 +5,7 @@ on: - main workflow_dispatch: -name: unit tests +name: Tests jobs: test: strategy: @@ -16,11 +16,11 @@ jobs: steps: - name: Install Go if: success() - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Run tests run: go test -v ./... -covermode=count @@ -29,11 +29,11 @@ jobs: steps: - name: Install Go if: success() - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: "1.21.x" - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Calc coverage run: | go test -v ./... -covermode=count -coverprofile=coverage.out