Skip to content
authgrabber

Browser internals

How the supported browsers store authenticated state.

Reference notes on how the supported browsers store authenticated state on disk, based on live profile inspection and the relevant browser source. See docs/design.md for the phase plan and docs/reference.md for the CLI.

The CLI resolves one profile at a time: Firefox by default, or a Chromium browser via a browser:profile prefix (e.g. vivaldi:Default).

Across all browsers, the same ideas repeat:

  • Tokens are mostly plaintext: OAuth access/refresh/id tokens in web storage and session/SSO cookies are not encrypted at rest.
  • Only the password manager encrypts credentials.
  • Live stores are locked: while the browser runs, its SQLite stores are locked or mid-write.
  • Read a copy of the DB together with its journal/WAL sidecars into a temp dir, then query the copy.
  • Binary blobs: encrypted values are BLOBs with arbitrary bytes; read them with column_blob/column_bytes, not NUL-terminated text.
  • Data root per OS: macOS ~/Library/Application Support/Firefox, Linux ~/.mozilla/firefox, Windows %APPDATA%\Mozilla\Firefox\Profiles.
  • profiles.ini lists profiles; the active one is named by an [Install*] section, else Default=1.
  • Stores:
    • cookies.sqlite table moz_cookies - session/SSO cookies, plaintext.
    • webappsstore2.sqlite - localStorage per origin, plaintext (the SPA token source).
    • logins.json + key4.db - saved logins, encrypted (NSS).
  • Login decryption: the profile’s logins are encrypted with the NSS key in key4.db.
  • We dlopen Firefox’s own libnss3 and call NSS_Init + PK11SDR_Decrypt rather than reimplementing NSS.
  • The key is unlocked via the OS keychain secret (macOS “Firefox Safe Storage”) or a primary password.
  • An empty password works when the keychain secret is unavailable.
  • User agent: derive from the profile’s compatibility.ini LastVersion plus the host OS, or read general.useragent.override from prefs.js.

Chromium family (Vivaldi, Chrome, Edge, Brave, Chromium)

Section titled “Chromium family (Vivaldi, Chrome, Edge, Brave, Chromium)”

The Chromium family shares one code base (os_crypt), so the storage layout is the same. Only the data-dir name and keychain item name differ.

  • Data root per OS:
    • macOS ~/Library/Application Support/<Name>/
    • Linux ~/.config/<name>/
    • Windows %LOCALAPPDATA%\<name>\User Data where <Name> is Vivaldi, Google/Chrome, Microsoft Edge, BraveSoftware/Brave-Browser, or Chromium.
  • No profiles.ini; profiles are subdirs Default, Profile 1, … and the last-used profile is in Local State (profile.last_used).
  • We default to the profile named on the CLI, else Default.
  • Stores (in the profile dir):
    • Cookies (SQLite, table cookies) - session cookies, encrypted values.
    • Login Data (SQLite, table logins) - saved logins; usernames are plaintext, passwords encrypted.
    • Local Storage/leveldb/ - localStorage, plaintext (LevelDB; C5 parses the .log write-ahead logs and the .ldb SSTables (Snappy blocks) structurally).

LevelDB is a simple embedded key-value store: decoding it gives pairs, not organized SQL tables. Each localStorage entry is a key/value pair whose key is the origin-mangled storage key and whose value is the stored value (plaintext). On disk you see two file kinds:

  • *.log - the write-ahead log (most recent, uncompressed writes).
  • *.ldb - SSTables (older data, Snappy-compressed blocks).

When parsed, the pairs look like this (generic example):

key: _https://example.com\x00olk
value: "1786535475143.478"
key: _https://example.com\x00access_token
value: "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
key: META:https://example.com
value: <metadata / account markers>
key: METAACCESS:https://example.com
value: <last access markers>

Notes:

  • The _ prefix + \x00 separator marks a real localStorage key: the origin is the part between _ and \x00, and the rest is the JS key.
  • Cross-origin keys embed the origin pair as _https://a.example/^0https://b.example\x00....
  • META: / METAACCESS: keys are Chromium metadata, not application localStorage.
  • Values are plaintext and unencrypted; OAuth tokens (e.g. eyJ... JWTs) appear directly as values.
  • Internally every key carries an 8-byte trailer (sequence + type) that the parser strips before showing the user-facing key.
  • The encryption key (os_crypt) is stored per platform:
    • macOS: the key is derived from the Keychain item <Name> Safe Storage (e.g. Vivaldi Safe Storage, Chrome Safe Storage).
    • The item holds a random 16-byte secret as base64.
    • Linux: with a basic password store, the key is in Local State under os_crypt.encrypted_key (base64, plaintext after the v10 prefix).
    • With a keyring backend, it is in the keyring.
    • Windows: os_crypt.encrypted_key is DPAPI-protected (not yet implemented in authgrabber).
  • The macOS key derivation (from os_crypt/async/browser/keychain_key_provider.mm):
    • password = the raw keychain secret string (base64 text, not decoded).
    • key = PBKDF2-HMAC-SHA1(password, salt = “saltysalt”, 1003 iters, 16 bytes)
    • algorithm = AES-128-CBC.
  • The v10 value format (from os_crypt/async/common/encryptor.cc):
    • Passwords: v10 + AES-128-CBC(key, IV = 16 spaces, ct) with PKCS#7.
    • Cookies: v10 + [16-byte IV][ciphertext]; the decrypted plaintext starts with a 16-byte salt that must be stripped, then the value.
    • Fallbacks: an empty-key decrypt is tried if the derived key fails (Chromium crbug 40055416), and AES-GCM (v10 + nonce(12) + ct + tag) is used by non-keychain providers.
  • Keychain service names per browser: Vivaldi Safe Storage, Chrome Safe Storage, Brave Browser Safe Storage, Microsoft Edge Safe Storage, Chromium Safe Storage.
  • The macOS Keychain may prompt to allow a new binary to read the browser’s “Safe Storage” item.
  • The security CLI can return empty transiently until access is granted.
  • security find-generic-password -s "<name> Safe Storage" -w.
  • A profile whose keychain secret does not match its stored cookies (e.g. the item holds 16 bytes while the values need the matching AES-128-CBC key) produces values that fail to decrypt - AES-GCM rejects wrong keys, and CBC yields garbage with invalid PKCS#7.
  • On such machines the value comes back empty rather than wrong.
  • Logins saved via the password-manager UI write plaintext usernames and an encrypted password.
  • A login whose encrypted blob is shorter than one AES block suggests a partial write (re-check after the browser fully exits).
  • Firefox logins.json is JSON; Chromium uses Login Data SQLite.
  • Both are read by the same logins command with identical redacted output.
  • User-agent: Firefox derives from compatibility.ini (LastVersion) plus the host OS.
  • Chromium is reconstructed from the browser’s Last Version file.
  • The macOS token is hardcoded to Intel Mac OS X 10_15_7 because Chromium freezes that string in its UA regardless of the real macOS version or chip (a long-standing web-compat decision), so a frozen token matches what the browser actually sends.
  • Chromium does not persist its UA in the profile, so exact reconstruction is not possible from disk.