Files
HackBrowserData/browser/firefox/masterkey.go
Roger 2860bb82f9 docs: cross-host decryption guide and comment cleanup (#614)
* docs(readme): document cross-host decryption workflow

* docs: drop RFC citations and what-comments
2026-06-13 21:17:00 +08:00

208 lines
6.2 KiB
Go

package firefox
import (
"bytes"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"os"
"github.com/tidwall/gjson"
_ "modernc.org/sqlite"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/log"
)
// key4DB holds the parsed contents of Firefox's key4.db NSS key storage.
//
// Firefox stores the master encryption key in key4.db using two SQLite tables:
// - metaData: contains the global salt and an encrypted "password-check" marker
// - nssPrivate: contains one or more encrypted master key candidates
//
// Reference: https://searchfox.org/mozilla-central/source/security/nss/lib/softoken/
type key4DB struct {
globalSalt []byte // metaData.item1: salt used as PBE decryption input
passwordCheck []byte // metaData.item2: encrypted marker to verify DB integrity
privateKeys []privateKey // nssPrivate rows: encrypted master key candidates
}
// privateKey is a single encrypted master key entry from nssPrivate.
type privateKey struct {
encrypted []byte // a11: PBE-encrypted master key blob
typeTag []byte // a102: key type identifier (must match nssKeyTypeTag)
}
// nssKeyTypeTag identifies valid master key entries in key4.db.
// Only nssPrivate rows where a102 matches this tag contain actual master keys;
// other rows may be certificates or other NSS objects.
// See: https://searchfox.org/mozilla-central/source/security/nss/lib/softoken/pkcs11i.h
var nssKeyTypeTag = []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
// readKey4DB opens key4.db and parses it into a structured key4DB.
func readKey4DB(path string) (*key4DB, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("open key4.db: %w", err)
}
defer db.Close()
var record key4DB
const metaQuery = `SELECT item1, item2 FROM metaData WHERE id = 'password'`
if err := db.QueryRow(metaQuery).Scan(&record.globalSalt, &record.passwordCheck); err != nil {
return nil, fmt.Errorf("query metaData: %w", err)
}
const nssQuery = `SELECT a11, a102 FROM nssPrivate`
rows, err := db.Query(nssQuery)
if err != nil {
return nil, fmt.Errorf("query nssPrivate: %w", err)
}
defer rows.Close()
for rows.Next() {
var pk privateKey
if err := rows.Scan(&pk.encrypted, &pk.typeTag); err != nil {
return nil, fmt.Errorf("scan nssPrivate row: %w", err)
}
record.privateKeys = append(record.privateKeys, pk)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate nssPrivate: %w", err)
}
if len(record.privateKeys) == 0 {
return nil, errors.New("nssPrivate table is empty")
}
return &record, nil
}
// deriveKeys verifies the database integrity via the password-check marker,
// then decrypts all valid master key candidates.
func (k *key4DB) deriveKeys() ([][]byte, error) {
if err := k.verifyPasswordCheck(); err != nil {
return nil, err
}
var keys [][]byte
for _, pk := range k.privateKeys {
if !bytes.Equal(pk.typeTag, nssKeyTypeTag) {
continue
}
key, err := k.decryptPrivateKey(pk)
if err != nil {
log.Debugf("decrypt nss private key: %v", err)
continue
}
keys = append(keys, key)
}
return keys, nil
}
// verifyPasswordCheck decrypts the password-check marker from metaData
// to confirm the database is valid and accessible.
func (k *key4DB) verifyPasswordCheck() error {
pbe, err := crypto.NewASN1PBE(k.passwordCheck)
if err != nil {
return fmt.Errorf("parse password check: %w", err)
}
plain, err := pbe.Decrypt(k.globalSalt)
if err != nil {
return fmt.Errorf("decrypt password check: %w", err)
}
if !bytes.Contains(plain, []byte("password-check")) {
return errors.New("password check verification failed")
}
return nil
}
// decryptPrivateKey decrypts a single master key candidate using the global salt.
func (k *key4DB) decryptPrivateKey(pk privateKey) ([]byte, error) {
pbe, err := crypto.NewASN1PBE(pk.encrypted)
if err != nil {
return nil, fmt.Errorf("parse private key: %w", err)
}
derivedKey, err := pbe.Decrypt(k.globalSalt)
if err != nil {
return nil, fmt.Errorf("decrypt private key: %w", err)
}
if len(derivedKey) < 24 {
return nil, fmt.Errorf("derived key too short: %d bytes (need >= 24)", len(derivedKey))
}
// Firefox 144+ uses AES-256-CBC instead of 3DES; the full derived key
// must be preserved to support modern cipher suites.
return derivedKey, nil
}
// encryptedLogin holds PBE-encrypted credentials from logins.json,
// used as test samples for master key validation.
type encryptedLogin struct {
username []byte // PBE-encrypted username blob
password []byte // PBE-encrypted password blob
}
// validateKeyWithLogins reads logins.json and returns the first key that
// can successfully decrypt an actual login entry. Returns nil if no key matches.
func validateKeyWithLogins(keys [][]byte, loginsPath string) []byte {
raw, err := os.ReadFile(loginsPath)
if err != nil {
return nil
}
samples := sampleEncryptedLogins(raw)
if len(samples) == 0 {
return nil
}
for _, key := range keys {
if tryDecryptLogins(key, samples) {
return key
}
}
return nil
}
// sampleEncryptedLogins extracts up to 5 encrypted login entries from
// logins.json as test samples for master key validation.
func sampleEncryptedLogins(raw []byte) []encryptedLogin {
arr := gjson.GetBytes(raw, "logins").Array()
var samples []encryptedLogin
for _, v := range arr {
userRaw, err := base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String())
if err != nil {
continue
}
pwdRaw, err := base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String())
if err != nil {
continue
}
samples = append(samples, encryptedLogin{username: userRaw, password: pwdRaw})
if len(samples) >= 5 {
break
}
}
return samples
}
// tryDecryptLogins checks if masterKey can decrypt at least one encrypted
// login entry (both username and password).
func tryDecryptLogins(masterKey []byte, samples []encryptedLogin) bool {
for _, login := range samples {
userPBE, err := crypto.NewASN1PBE(login.username)
if err != nil {
continue
}
if _, err := userPBE.Decrypt(masterKey); err != nil {
continue
}
pwdPBE, err := crypto.NewASN1PBE(login.password)
if err != nil {
continue
}
if _, err := pwdPBE.Decrypt(masterKey); err == nil {
return true
}
}
return false
}