How Chromium encrypts cookies on Windows
2026-06-17 · 3 min
The Cookies database tells you which cookies existed and when. It does
not, by itself, tell you their values — on Windows, Chromium has stored
the encrypted_value column as ciphertext since v80. Recovering the
plaintext is a two-key problem: a per-profile AES key that lives in
Local State, wrapped by a Windows-user key you have to unwrap with
DPAPI. This post covers the first half — the formats. The
offline DPAPI chain covers
the second.
The key lives in Local State
Chromium generates one AES-256 key per profile and stores it, base64-encoded,
in the profile's parent Local State file (JSON):
%LOCALAPPDATA%\Google\Chrome\User Data\Local State
%LOCALAPPDATA%\Microsoft\Edge\User Data\Local State
{
"os_crypt": {
"encrypted_key": "RFBBUEkBAAAA0Iyd3wEV0RGM...",
"app_bound_encrypted_key": "QVBQQgEAAADQ..."
}
}
Base64-decode encrypted_key and the first five bytes are the ASCII tag
DPAPI. Strip it and what remains is a DPAPI blob — the AES key
encrypted under the user's Windows credentials. You cannot read the key
out of Local State directly; it has to be unwrapped first.
The cookie value format
Each encrypted_value is a self-describing blob. The modern layout is:
| Bytes | Meaning |
|---|---|
0..3 | version tag — ASCII v10 or v20 |
3..15 | 12-byte AES-GCM nonce |
15..n-16 | ciphertext |
n-16..n | 16-byte GCM authentication tag |
So decryption is AES-256-GCM with the os_crypt key, the embedded nonce,
and the trailing tag. The on-disk layout (ciphertext followed by tag) is
exactly what WebCrypto's decrypt expects, which is why the
in-browser decryptor
can do it with no extra reassembly.
Two version tags matter:
v10— the classic scheme. The key fromLocal Statedecrypts the value directly. Plaintext is the cookie value as-is.v20— app-bound encryption (Chrome 127+). The decrypted plaintext is prefixed with a 32-byte SHA-256 domain hash that you strip to get the value. v20 values are encrypted with the app-bound key (app_bound_encrypted_key), not the regularencrypted_key.
Values written before v80 have no version tag — they start with the DPAPI
magic 01 00 00 00 and were encrypted per-value with CryptProtectData.
Those need the same DPAPI unwrap, applied to each value rather than to a
shared key.
What you can decrypt offline
| Scheme | Key source | Offline from files? |
|---|---|---|
| v10 | encrypted_key (DPAPI-wrapped) | Yes — with the masterkey + user secret |
| Pre-v80 per-value | DPAPI per value | Yes — same inputs, per value |
| v20 (app-bound) | app_bound_encrypted_key | Partly — see below |
For v10, everything is recoverable from acquired files: the
Local State key, the user's DPAPI masterkey, and the user's NT hash or
password. That is the common case and the one the tool automates.
v20 adds a second layer. The app-bound key is wrapped with system
DPAPI and then run through an app-bound service that ties it to the
machine. The system-DPAPI layer is recoverable offline (the machine key
comes out of the SECURITY hive), but the app-bound step is not pure
file-based crypto on current builds. If you already hold the unwrapped v20
key, decrypting the values is straightforward GCM; deriving that key from
files alone is the part that does not fully reduce to offline math yet.
Other platforms, briefly
This is a Windows story. On macOS the os_crypt key lives in the login
Keychain (Chrome Safe Storage), unlocked with the user password. On
Linux it is either a hard-coded peanuts key or a secret in
GNOME Keyring / KWallet. Firefox does not encrypt cookie values at all,
and Safari binarycookies are plaintext — see
browser cookies in forensics.