Reverse-Engineering Chrome's Cookie Encryption (To Authenticate AI Agents)
Source: Dev.to
The Problem – Login Screens
If you’ve built AI agents that interact with websites, you’ve hit this wall: login screens.
Your agent needs to:
- Check LinkedIn notifications
- Scrape a dashboard
- Post to a platform
But the site demands authentication. The first thing most developers do is open Chrome DevTools, copy the Cookie header, and paste it into the script.
It works… for about 24 hours. Then the session expires, the automation breaks at 3 am, and you’re woken up by angry alerts.
I got tired of this cycle. Chrome already has my authenticated sessions stored locally. I’m logged into LinkedIn right now. What if my agent could just use that?
Turns out it can – but Chrome doesn’t make it easy.
Where Chrome Stores Cookies
Chrome stores cookies in a SQLite database. The location depends on your OS:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/Google/Chrome/Default/Cookies |
| Linux | ~/.config/google-chrome/Default/Cookies |
| Windows | %LOCALAPPDATA%\Google\Chrome\User Data\Default\Network\Cookies |
You can open the file with any SQLite client, e.g.:
sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/Cookies
Schema (relevant columns)
CREATE TABLE cookies(
creation_utc INTEGER NOT NULL,
host_key TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL,
encrypted_value BLOB NOT NULL,
path TEXT NOT NULL,
expires_utc INTEGER NOT NULL,
is_secure INTEGER NOT NULL,
is_httponly INTEGER NOT NULL
/* … other columns omitted … */
);
Example query for LinkedIn
SELECT name, value, encrypted_value
FROM cookies
WHERE host_key LIKE '%linkedin%';
Typical result:
name: li_at
value: (empty)
encrypted_value: v10[blob of binary garbage]
The value column is empty; everything interesting lives in encrypted_value. That blob is encrypted.
How Chrome Encrypts Cookies
Chrome started encrypting cookies around 2014 to prevent malware from trivially stealing sessions. The encryption method varies by platform.
| Platform | Where the master key is stored | Encryption algorithm |
|---|---|---|
| macOS | macOS Keychain entry “Chrome Safe Storage” | AES‑128‑CBC (key derived via PBKDF2) |
| Linux | GNOME Keyring / KWallet via libsecret (fallback: literal peanuts) | AES‑128‑CBC |
| Windows | Data Protection API (DPAPI) – tied to the logged‑in Windows user | DPAPI (no extractable key) |
All three prepend the literal string v10 to the encrypted payload to indicate the version.
Decrypting Cookies with Python (macOS example)
Below is a minimal, self‑contained script that:
- Retrieves Chrome’s master key from the macOS Keychain.
- Derives the AES‑128 key using PBKDF2.
- Decrypts the
encrypted_valueblobs. - Returns a dictionary of LinkedIn cookies.
Note: The same overall flow works on Linux (replace the key‑retrieval step) and on Windows (use
win32crypt.CryptUnprotectData).
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shutil
import sqlite3
import subprocess
import tempfile
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# ----------------------------------------------------------------------
# 1️⃣ Get Chrome's master key from the macOS Keychain
# ----------------------------------------------------------------------
def get_chrome_key_mac() -> str:
"""
Returns the password stored under the Keychain entry
“Chrome Safe Storage”. This is the raw secret Chrome uses
as the PBKDF2 password.
"""
cmd = [
"security",
"find-generic-password",
"-s", "Chrome Safe Storage",
"-w"
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return result.stdout.strip()
# ----------------------------------------------------------------------
# 2️⃣ Derive the AES‑128 key from the master secret
# ----------------------------------------------------------------------
def derive_aes_key(chrome_key: str) -> bytes:
"""
Chrome uses PBKDF2‑HMAC‑SHA1 with:
• salt = b"saltysalt"
• iterations = 1003
• key length = 16 bytes (AES‑128)
"""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA1(),
length=16,
salt=b"saltysalt",
iterations=1003,
backend=default_backend(),
)
return kdf.derive(chrome_key.encode()) # chrome_key is a UTF‑8 string
# ----------------------------------------------------------------------
# 3️⃣ Decrypt a single cookie value
# ----------------------------------------------------------------------
def decrypt_cookie(encrypted_value: bytes, aes_key: bytes) -> str:
"""
Chrome prefixes encrypted blobs with b"v10". The actual ciphertext
is AES‑128‑CBC with a static IV of 16 spaces (0x20).
"""
if not encrypted_value.startswith(b"v10"):
# Not encrypted – return as‑is (unlikely for Chrome)
return encrypted_value.decode(errors="ignore")
# Strip the version prefix
ciphertext = encrypted_value[3:]
# Static IV = 16 spaces
iv = b" " * 16
cipher = Cipher(
algorithms.AES(aes_key),
modes.CBC(iv),
backend=default_backend(),
)
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
# Remove PKCS#7 padding
padding_len = padded[-1]
if padding_len > 16:
# Something went wrong; return raw bytes
return padded.decode(errors="ignore")
return padded[:-padding_len].decode(errors="ignore")
# ----------------------------------------------------------------------
# 4️⃣ Pull LinkedIn cookies from Chrome's SQLite DB
# ----------------------------------------------------------------------
def get_linkedin_cookies() -> dict[str, str]:
"""
Returns a mapping {cookie_name: cookie_value} for all cookies whose
host contains “linkedin”. The function works on macOS; adjust the
`cookie_path` for Linux/Windows.
"""
# Chrome locks the DB while it runs, so copy it to a temp file first
cookie_path = os.path.expanduser(
"~/Library/Application Support/Google/Chrome/Default/Cookies"
)
with tempfile.NamedTemporaryFile(delete=False) as tmp:
shutil.copy2(cookie_path, tmp.name)
# Open the copied DB
conn = sqlite3.connect(tmp.name)
cur = conn.cursor()
cur.execute(
"""
SELECT name, encrypted_value
FROM cookies
WHERE host_key LIKE '%linkedin%'
"""
)
rows = cur.fetchall()
conn.close()
os.unlink(tmp.name) # clean up the temp file
# Prepare decryption materials
chrome_key = get_chrome_key_mac()
aes_key = derive_aes_key(chrome_key)
# Decrypt each cookie
cookies: dict[str, str] = {}
for name, enc_val in rows:
# `enc_val` comes out of SQLite as a `bytes` object
cookies[name] = decrypt_cookie(enc_val, aes_key)
return cookies
# ----------------------------------------------------------------------
# Example usage
# ----------------------------------------------------------------------
if __name__ == "__main__":
linkedin_cookies = get_linkedin_cookies()
for name, value in linkedin_cookies.items():
print(f"{name} = {value}")
What the script does
- Copies Chrome’s
CookiesSQLite file to a temporary location (Chrome keeps the original locked). - Queries for any rows whose
host_keycontains “linkedin”. - Retrieves the master key from the macOS Keychain (
Chrome Safe Storage). - Derives the AES‑128 key via PBKDF2‑HMAC‑SHA1.
- Decrypts each
encrypted_value(skipping thev10prefix, using a static IV of 16 spaces, and stripping PKCS#7 padding). - Returns a plain‑text dictionary that can be fed directly into
requestsor any HTTP client.
TL;DR
- Chrome stores cookies in a SQLite DB (
Cookies). - On macOS the cookie values are encrypted with AES‑128‑CBC; the key is derived from a password stored in the Keychain (
Chrome Safe Storage). - By extracting that password, deriving the AES key, and decrypting the
encrypted_valueblobs, you can programmatically reuse the same session cookies your browser already has—no manual copy‑paste, no 24‑hour expiry.
Now your AI agent can stay logged in as long as you stay logged in to Chrome. 🎉
cookies[name] = decrypt_cookie(encrypted_value, aes_key)
conn.close()
os.unlink(tmp.name)
return cookies
Usage
cookies = get_linkedin_cookies()
print(cookies)
# {'li_at': 'AQEDAT...', 'JSESSIONID': 'ajax:123...', ...}
It works. You now have decrypted session cookies that you can use in requests:
import requests
response = requests.get(
'https://www.linkedin.com/feed/',
cookies=cookies
)
# You're authenticated
Why a Script Isn’t Enough
So we can decrypt cookies. Problem solved? Not quite. This script has several issues:
- Security: The decrypted cookies are now in plaintext in your script’s memory, your logs, and potentially your git history. Session tokens are as sensitive as passwords.
- No scoping: Any script can access any cookie. Your “LinkedIn agent” could also read your bank cookies—a security nightmare.
- No audit trail: When something goes wrong (and it will), you have no idea which agent accessed what, when.
- Session management: Cookies expire. Sites rotate tokens. You need to track freshness and know when to re‑authenticate.
- Multi‑agent chaos: When you have 5 agents accessing 10 sites, cookie management becomes its own project.
From Script to Tool
This is why I built AgentAuth.
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Chrome Extension│────▶│ Encrypted Vault │────▶│ Your Agent │
│ (export once) │ │ (AES‑256) │ │ (scoped access)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
- Chrome Extension: Click “Export” while logged into a site. The extension captures the cookies and sends them to the local vault—no more DevTools copy‑paste.
- Encrypted Vault: Cookies are stored in a local SQLite database, encrypted with AES‑256 using a password you control. Not scattered across scripts.
- Scoped Agents: Create named agents with specific domain access. Your
linkedin-agentcan only accesslinkedin.comcookies; it can’t touch your bank. - Audit Logging: Every access is logged with timestamp, agent name, and domain.
Example Code
from agent_auth import Vault
import requests
vault = Vault()
vault.unlock("password")
cookies = vault.get_session("linkedin.com")
# Use with requests
response = requests.get('https://linkedin.com/feed', cookies=cookies)
# Or with Playwright
# context.add_cookies(cookies)
One line to get authenticated cookies. No decryption code. No hard‑coded tokens. No security nightmares.
The Bigger Picture
While building this I realized that session management for AI agents is unsolved infrastructure.
- We have OAuth for user‑facing apps.
- We have API keys for server‑to‑server communication.
- But AI agents? They’re still stuck with the same hacks we used in 2010 for web scraping.
The industry is building increasingly sophisticated agents—agents that can browse, fill forms, make purchases. Yet we’re still copy‑pasting cookies from DevTools.
AgentAuth is my attempt to fix this. It’s open source, works today on any site (no OAuth adoption required), and integrates with LangChain, Playwright, and n8n.
Links
- GitHub: https://github.com/jacobgadek/agent-auth
- PyPI:
pip install agentauth-py - n8n node:
npm install n8n-nodes-agentauth
If you’re building agents that need authentication, give it a try. And if you want to contribute—especially Windows DPAPI support or a Firefox extension—PRs are welcome.
If you found this useful, consider starring the repo. It helps others discover the project.