From 3a89cb63ce925edf4847a2cf0ad52fa0f3278e9c Mon Sep 17 00:00:00 2001 From: Aquilao Official Date: Tue, 3 Mar 2026 11:56:44 +0800 Subject: [PATCH] feat: enhance firefox 144+ master key retrieval and improve padding validation (#499) * feat: enhance firefox 144+ master key retrieval and improve padding validation * fix: correct SQL query casing in nssPrivate test * fix: reorder import statements in firefox.go for consistency --- browser/firefox/firefox.go | 129 ++++++++++++++++++++++++++++++-- browser/firefox/firefox_test.go | 2 +- crypto/crypto.go | 8 ++ 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/browser/firefox/firefox.go b/browser/firefox/firefox.go index 13791e9..7dea102 100644 --- a/browser/firefox/firefox.go +++ b/browser/firefox/firefox.go @@ -3,12 +3,14 @@ package firefox import ( "bytes" "database/sql" + "encoding/base64" "errors" "fmt" "io/fs" "os" "path/filepath" + "github.com/tidwall/gjson" _ "modernc.org/sqlite" // sqlite3 driver TODO: replace with chooseable driver "github.com/moond4rk/hackbrowserdata/browserdata" @@ -99,12 +101,41 @@ func (f *Firefox) GetMasterKey() ([]byte, error) { return nil, fmt.Errorf("query metadata error: %w", err) } - nssA11, nssA102, err := queryNssPrivate(keyDB) + candidates, err := queryNssPrivateCandidates(keyDB) if err != nil { return nil, fmt.Errorf("query NSS private error: %w", err) } + loginCipherPairs, _ := getFirefoxLoginCipherPairs() - return processMasterKey(metaItem1, metaItem2, nssA11, nssA102) + var ( + fallbackKey []byte + lastErr error + ) + for _, c := range candidates { + masterKey, err := processMasterKey(metaItem1, metaItem2, c.a11, c.a102) + if err != nil { + lastErr = err + continue + } + if fallbackKey == nil { + fallbackKey = masterKey + } + + if len(loginCipherPairs) == 0 { + return masterKey, nil + } + if canDecryptAnyLoginCipherPair(masterKey, loginCipherPairs) { + return masterKey, nil + } + } + + if fallbackKey != nil { + return fallbackKey, nil + } + if lastErr != nil { + return nil, lastErr + } + return nil, errors.New("no valid firefox master key found in nssPrivate") } func queryMetaData(db *sql.DB) ([]byte, []byte, error) { @@ -116,14 +147,98 @@ func queryMetaData(db *sql.DB) ([]byte, []byte, error) { return metaItem1, metaItem2, nil } +type nssPrivateCandidate struct { + a11 []byte + a102 []byte +} + +func queryNssPrivateCandidates(db *sql.DB) ([]nssPrivateCandidate, error) { + const query = `SELECT a11, a102 FROM nssPrivate` + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var candidates []nssPrivateCandidate + for rows.Next() { + var c nssPrivateCandidate + if err := rows.Scan(&c.a11, &c.a102); err != nil { + return nil, err + } + candidates = append(candidates, c) + } + if err := rows.Err(); err != nil { + return nil, err + } + if len(candidates) == 0 { + return nil, errors.New("nssPrivate is empty") + } + return candidates, nil +} + func queryNssPrivate(db *sql.DB) ([]byte, []byte, error) { - // To ensure compatibility with newer profiles, always select the newest key. - const query = `SELECT a11, a102 from nssPrivate ORDER BY id DESC LIMIT 1` - var nssA11, nssA102 []byte - if err := db.QueryRow(query).Scan(&nssA11, &nssA102); err != nil { + // Keep this helper for backward compatibility in tests. + candidates, err := queryNssPrivateCandidates(db) + if err != nil { return nil, nil, err } - return nssA11, nssA102, nil + return candidates[0].a11, candidates[0].a102, nil +} + +type loginCipherPair struct { + username []byte + password []byte +} + +func getFirefoxLoginCipherPairs() ([]loginCipherPair, error) { + raw, err := os.ReadFile(types.FirefoxPassword.TempFilename()) + if err != nil { + return nil, err + } + arr := gjson.GetBytes(raw, "logins").Array() + pairs := make([]loginCipherPair, 0, len(arr)) + for _, v := range arr { + uEnc := v.Get("encryptedUsername").String() + pEnc := v.Get("encryptedPassword").String() + if uEnc == "" || pEnc == "" { + continue + } + uRaw, err := base64.StdEncoding.DecodeString(uEnc) + if err != nil { + continue + } + pRaw, err := base64.StdEncoding.DecodeString(pEnc) + if err != nil { + continue + } + pairs = append(pairs, loginCipherPair{username: uRaw, password: pRaw}) + if len(pairs) >= 5 { + break + } + } + return pairs, nil +} + +func canDecryptAnyLoginCipherPair(masterKey []byte, pairs []loginCipherPair) bool { + for _, pair := range pairs { + uPBE, err := crypto.NewASN1PBE(pair.username) + if err != nil { + continue + } + if _, err := uPBE.Decrypt(masterKey); err != nil { + continue + } + + pPBE, err := crypto.NewASN1PBE(pair.password) + if err != nil { + continue + } + if _, err := pPBE.Decrypt(masterKey); err == nil { + return true + } + } + return false } // processMasterKey process master key of Firefox. diff --git a/browser/firefox/firefox_test.go b/browser/firefox/firefox_test.go index 74c9dac..540a771 100644 --- a/browser/firefox/firefox_test.go +++ b/browser/firefox/firefox_test.go @@ -29,7 +29,7 @@ func TestQueryNssPrivate(t *testing.T) { rows := sqlmock.NewRows([]string{"a11", "a102"}). AddRow([]byte("nssA11"), []byte("nssA102")) - mock.ExpectQuery("SELECT a11, a102 from nssPrivate").WillReturnRows(rows) + mock.ExpectQuery("SELECT a11, a102 FROM nssPrivate").WillReturnRows(rows) nssA11, nssA102, err := queryNssPrivate(db) assert.NoError(t, err) diff --git a/crypto/crypto.go b/crypto/crypto.go index 6a5d19a..ab32c2b 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -143,6 +143,14 @@ func pkcs5UnPadding(src []byte) ([]byte, error) { if padding < 1 || padding > aes.BlockSize { return nil, errors.New("pkcs5UnPadding: invalid padding size") } + if padding > length { + return nil, errors.New("pkcs5UnPadding: invalid padding length") + } + for _, b := range src[length-padding:] { + if int(b) != padding { + return nil, errors.New("pkcs5UnPadding: invalid padding content") + } + } return src[:length-padding], nil }