browserforensics
Back to all articles

Decrypting Firefox passwords: NSS, key4.db and logins.json

2026-06-17 · 3 min

Chrome leans on the OS keystore — DPAPI on Windows, the Keychain on macOS. Firefox does its own thing: saved passwords are protected by NSS (Network Security Services), the same crypto library Firefox uses for TLS. Nothing is tied to your Windows account, which makes Firefox passwords more recoverable offline than Chrome's — you just need two files and (sometimes) the primary password.

The two files

FileHoldsFormat
logins.jsonthe encrypted usernames & passwordsJSON
key4.dbthe key that decrypts themSQLite

Both sit in the profile root (see Firefox file locations). Cookies are a separate story — cookies.sqlite is plaintext, so it needs no key at all (unlike Chrome cookies).

logins.json is an array of entries:

{
  "logins": [
    {
      "hostname": "https://example.com",
      "encryptedUsername": "MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECC4...",
      "encryptedPassword": "MEIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECF...",
      "timeCreated": 1700000000000,
      "timeLastUsed": 1701234567890
    }
  ]
}

Each encrypted* value is base64 of an ASN.1 structure: a key id, a cipher

  • IV, and the ciphertext. The cipher for the entries themselves is always 3DES-CBC — but the key comes from key4.db.

Inside key4.db

key4.db (SQLite; it replaced the old Berkeley-DB key3.db around 2018) has two tables that matter:

  • metadata — the row id = 'password' carries item1 (the global salt) and item2 (a password-check blob).
  • nssPrivate — the row whose a102 equals the well-known id f8000000000000000000000000000001 carries a11, the encrypted key.

item2 and a11 are both wrapped the same way, by a key derived from the global salt and your primary password (empty by default). Which wrapping is used is announced by the algorithm OID in the ASN.1:

OIDScheme
1.2.840.113549.1.12.5.1.3pbeWithSha1And3KeyTripleDES-CBC (legacy 3DES)
1.2.840.113549.1.5.13PBES2 → PBKDF2-HMAC-SHA256 + AES-256-CBC (modern)

Key derivation

Legacy 3DES PBE. A SHA-1/HMAC-SHA1 ladder over the salts:

hp  = SHA1(globalSalt || primaryPassword)
chp = SHA1(hp || entrySalt)
k1  = HMAC-SHA1(chp, pes || entrySalt)      # pes = entrySalt padded to 20 bytes
tk  = HMAC-SHA1(chp, pes)
k2  = HMAC-SHA1(chp, tk || entrySalt)
k   = k1 || k2 ;  key = k[:24] ;  iv = k[-8:]

Modern PBES2. Cleaner, and the common case today:

k   = SHA1(globalSalt || primaryPassword)
key = PBKDF2-HMAC-SHA256(k, entrySalt, iterations, 32)
iv  = 0x04 0x0e || <14-byte stored IV>      # an NSS quirk: real IV is 16 bytes

Either way the primary password is mixed in only via that first SHA1(globalSalt || password). No password set → the password is the empty string, and everything decrypts with no secret at all.

The self-check that makes it trustworthy

You never have to guess whether the derivation worked. Decrypt item2 and the plaintext must be exactly:

password-check\x02\x02

(the literal string password-check plus its two PKCS#7 padding bytes). If it matches, the primary password and the whole derivation are correct — so a wrong password fails loudly instead of silently producing garbage. This is the same "decrypt-proves-itself" property as the DPAPI masterkey HMAC.

Once verified, decrypt a11 to recover the 24-byte 3DES key, then decrypt each logins.json entry with it (3DES-CBC, strip PKCS#7) to get the cleartext username and password.

Why it runs in a browser tab

The whole chain is SHA-1, HMAC-SHA1, PBKDF2, 3DES and AES — no OS calls, no keystore. That makes it a clean fit for a WebAssembly module: our decryptor is a small Rust crate (RustCrypto + a tiny ASN.1 reader) compiled to WASM. The page reads key4.db and logins.json, hands the raw blobs to the WASM, and shows the decrypted logins — with nothing uploaded. Drop both files into the in-browser parser, enter the primary password (or leave it blank), and decrypt.

Pitfalls

  • Primary password. If the user set one and you don't have it, the password-check fails and there is no shortcut — it gates everything.
  • key3.db. Very old profiles use the Berkeley-DB key3.db instead of key4.db; the scheme is similar but the container differs.
  • Don't forget the salt source. The global salt is in key4.db, not in logins.json — you need both files, every time.

Further reading