Browser internals
How the supported browsers store authenticated state.
authgrabber - browser internals
Section titled “authgrabber - browser internals”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).
Common patterns
Section titled “Common patterns”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.
Firefox
Section titled “Firefox”- Data root per OS: macOS
~/Library/Application Support/Firefox, Linux~/.mozilla/firefox, Windows%APPDATA%\Mozilla\Firefox\Profiles. profiles.inilists profiles; the active one is named by an[Install*]section, elseDefault=1.- Stores:
cookies.sqlitetablemoz_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
libnss3and callNSS_Init+PK11SDR_Decryptrather 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.iniLastVersionplus the host OS, or readgeneral.useragent.overridefromprefs.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 Datawhere<Name>isVivaldi,Google/Chrome,Microsoft Edge,BraveSoftware/Brave-Browser, orChromium.
- macOS
- No
profiles.ini; profiles are subdirsDefault,Profile 1, … and the last-used profile is inLocal State(profile.last_used). - We default to the profile named on the CLI, else
Default. - Stores (in the profile dir):
Cookies(SQLite, tablecookies) - session cookies, encrypted values.Login Data(SQLite, tablelogins) - 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).
Chromium localStorage is LevelDB, not SQL
Section titled “Chromium localStorage is LevelDB, not SQL”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\x00olkvalue: "1786535475143.478"
key: _https://example.com\x00access_tokenvalue: "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
key: META:https://example.comvalue: <metadata / account markers>
key: METAACCESS:https://example.comvalue: <last access markers>Notes:
- The
_prefix +\x00separator 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 Stateunderos_crypt.encrypted_key(base64, plaintext after thev10prefix). - With a keyring backend, it is in the keyring.
- Windows:
os_crypt.encrypted_keyis DPAPI-protected (not yet implemented in authgrabber).
- macOS: the key is derived from the Keychain item
- 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
v10value format (fromos_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.
- Passwords:
- Keychain service names per browser:
Vivaldi Safe Storage,Chrome Safe Storage,Brave Browser Safe Storage,Microsoft Edge Safe Storage,Chromium Safe Storage.
Gotchas found while testing
Section titled “Gotchas found while testing”- The macOS Keychain may prompt to allow a new binary to read the browser’s “Safe Storage” item.
- The
securityCLI 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 DataSQLite. - Both are read by the same
loginscommand with identical redacted output. - User-agent: Firefox derives from
compatibility.ini(LastVersion) plus the host OS. - Chromium is reconstructed from the browser’s
Last Versionfile. - The macOS token is hardcoded to
Intel Mac OS X 10_15_7because 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.