Offline Windows DPAPI: unwrapping the os_crypt key
2026-06-17 · 4 min
Chromium's cookie key is a
DPAPI blob: an AES-256 key encrypted under the Windows user's credentials.
The naive way to unwrap it is to call CryptUnprotectData while logged in
as that user. On an acquired image you do not have that luxury — but DPAPI
is fully reversible offline if you have the right inputs. Here is the whole
chain.
What DPAPI protects with
DPAPI never encrypts your data with your password directly. It uses a masterkey — a 64-byte secret stored, itself encrypted, in:
%APPDATA%\Microsoft\Protect\<SID>\<GUID>
One file per masterkey, named by GUID; the folder is named by the user's SID. A DPAPI blob records, in its header, the GUID of the masterkey that sealed it — so the blob tells you which masterkey file you need.
To decrypt a blob you therefore need three things on disk plus one secret:
- the blob (here,
Local State'sencrypted_key), - the masterkey file for the GUID the blob names,
- the user's SID (the
\Protect\folder name), - the user's NT hash or password.
Step 1 — decrypt the masterkey
The masterkey file is encrypted with a key derived from the user secret
and the SID. impacket's deriveKeysFromUserkey is the reference:
preKey = HMAC-SHA1( NT_hash, UTF16LE(sid + "\0") )
(From a password instead, the equivalent uses SHA1(password) and
MD4(password) — and MD4(password) is the NT hash, which is why the
hash alone is enough.) That preKey then feeds Microsoft's masterkey
derivation, which is not standard PBKDF2 — it XOR-accumulates
PRF(P, derived) rather than PRF(P, U_prev). Get that loop wrong and
everything downstream is garbage.
The derived bytes split into an AES-256 key and IV, AES-CBC-decrypt the masterkey body, and — crucially — the result carries an HMAC that you verify. That self-check is what makes offline DPAPI trustworthy: a wrong SID or wrong hash fails the HMAC, so a successful decrypt is provably correct. There is no "looks plausible" failure mode.
Modern Windows 10/11 masterkeys use AES-256 with an HMAC-SHA1 or HMAC-SHA512 PRF. Older ones used 3DES; that path is rare and not worth porting for a cookie key.
Step 2 — decrypt the blob
With the 64-byte masterkey in hand, the encrypted_key blob decrypts on
its own terms:
keyHash = SHA1(masterkey)
sessionKey = HMAC-SHA512(keyHash, blob.salt)
plaintext = AES-256-CBC-decrypt(blob.data, key = sessionKey[:32], iv = 0)
Strip PKCS#7 padding and you have the 32-byte AES key — the same key that
decrypts every v10 cookie value
in the profile. Chrome calls CryptProtectData with no entropy, so there
is no extra secret to supply.
Where the NT hash comes from
The chain needs the user's NT hash (or password). On a disk image you
recover it the same way you would for any credential work: from the SAM
hive, decrypted with the boot key from SYSTEM. That is exactly what a
secretsdump-style tool produces. So the practical pairing is:
SYSTEM+SAM→ user NT hash,Local State+ masterkey file + SID + that NT hash → Chrome AES key,- AES key +
Cookies→ plaintext.
For a domain account whose password you do not have, the user's plaintext password (if known) works directly; otherwise the masterkey may be recoverable via the domain backup key — out of scope here.
A note on app-bound (v20) keys
Everything above unwraps the classic v10 encrypted_key. The v20
app_bound_encrypted_key adds a system-DPAPI layer (recoverable from the
machine key in the SECURITY hive) plus an app-bound service step that is
not pure offline crypto. Treat v20 as partially-offline until tooling
catches up.
Doing it without a Windows box
None of this needs the original machine — it is HMAC, SHA, AES-CBC and a quirky KDF. That is precisely why it can run entirely in a browser tab, with no password or key ever leaving the page.