逆向工程 Chrome 的 Cookie 加密(以验证 AI 代理)
Source: Dev.to
(请提供需要翻译的正文内容,我将为您翻译成简体中文。)
问题 – 登录页面
如果你已经构建了与网站交互的 AI 代理,你一定遇到过这道墙:登录页面。
你的代理需要:
- 检查 LinkedIn 通知
- 抓取仪表盘数据
- 向平台发布内容
但网站要求进行身份验证。大多数开发者的第一步是打开 Chrome DevTools,复制 Cookie 头部,然后粘贴到脚本中。
它可以工作……大约 24 小时。随后会话过期,自动化在凌晨 3 点崩溃,你会被愤怒的警报叫醒。
我厌倦了这种循环。Chrome 已经在本地存储了我的已认证会话。我现在已经登录了 LinkedIn。如果我的代理能够直接使用它呢?
事实证明是可以的——但 Chrome 并没有让这件事变得容易。
Chrome 存储 Cookie 的位置
Chrome 将 Cookie 保存在 SQLite 数据库中。具体位置取决于操作系统:
| 操作系统 | 路径 |
|---|---|
| macOS | ~/Library/Application Support/Google/Chrome/Default/Cookies |
| Linux | ~/.config/google-chrome/Default/Cookies |
| Windows | %LOCALAPPDATA%\Google\Chrome\User Data\Default\Network\Cookies |
你可以使用任意 SQLite 客户端打开该文件,例如:
sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/Cookies
Schema(相关列)
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 … */
);
LinkedIn 示例查询
SELECT name, value, encrypted_value
FROM cookies
WHERE host_key LIKE '%linkedin%';
典型结果:
name: li_at
value: (empty)
encrypted_value: v10[blob of binary garbage]
value 列为空;所有有价值的信息都存放在 encrypted_value 中。该 BLOB 是 加密 的。
Chrome 如何加密 Cookie
Chrome 大约在 2014 年开始加密 Cookie,以防止恶意软件轻易窃取会话。加密方式因平台而异。
| 平台 | 主密钥存储位置 | 加密算法 |
|---|---|---|
| macOS | macOS Keychain 条目 “Chrome Safe Storage” | AES‑128‑CBC(密钥通过 PBKDF2 派生) |
| Linux | GNOME Keyring / KWallet 通过 libsecret(回退:文字 peanuts) | AES‑128‑CBC |
| Windows | 数据保护 API(DPAPI)——绑定到已登录的 Windows 用户 | DPAPI(无可提取的密钥) |
这三种平台都会在加密负载前加上文字字符串 v10 以指示版本。
使用 Python 解密 Cookie(macOS 示例)
下面是一个最小的、独立的脚本,它:
- 从 macOS 钥匙串中获取 Chrome 的主密钥。
- 使用 PBKDF2 派生 AES‑128 密钥。
- 解密
encrypted_value数据块。 - 返回 LinkedIn cookie 的字典。
注意: 相同的整体流程在 Linux(替换获取密钥的步骤)和 Windows(使用
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}")
脚本的工作原理
- 复制 Chrome 的
CookiesSQLite 文件到临时位置(Chrome 会锁定原始文件)。 - 查询
host_key包含 “linkedin” 的所有行。 - 从 macOS 钥匙串中获取 主密钥(
Chrome Safe Storage)。 - 通过 PBKDF2‑HMAC‑SHA1 推导 AES‑128 密钥。
- 解密 每个
encrypted_value(跳过v10前缀,使用 16 个空格的静态 IV,并去除 PKCS#7 填充)。 - 返回 一个明文字典,可直接用于
requests或任何 HTTP 客户端。
TL;DR
- Chrome 将 cookie 存储在 SQLite 数据库 (
Cookies) 中。 - 在 macOS 上,cookie 值使用 AES‑128‑CBC 加密;密钥来源于存储在钥匙串中的密码 (
Chrome Safe Storage)。 - 通过提取该密码、派生 AES 密钥并解密
encrypted_valueblob,你可以以编程方式复用浏览器已有的会话 cookie——无需手动复制粘贴,也不会在 24 小时后失效。
现在,只要你保持 Chrome 登录状态,你的 AI 代理就可以保持登录。 🎉
cookies[name] = decrypt_cookie(encrypted_value, aes_key)
conn.close()
os.unlink(tmp.name)
return cookies
用法
cookies = get_linkedin_cookies()
print(cookies)
# {'li_at': 'AQEDAT...', 'JSESSIONID': 'ajax:123...', ...}
它可以工作。现在你已经拥有解密后的会话 Cookie,可以在 requests 中使用:
import requests
response = requests.get(
'https://www.linkedin.com/feed/',
cookies=cookies
)
# You're authenticated
为什么仅靠脚本不足够
所以我们可以解密 cookie。问题解决了吗?并不完全。 这个脚本存在几个问题:
- 安全性: 解密后的 cookie 现在以明文形式存在于脚本的内存、日志,甚至可能出现在你的 Git 历史中。会话令牌的敏感程度与密码相当。
- 无作用域限制: 任意脚本都可以访问任何 cookie。你的“LinkedIn 代理”也可能读取你的银行 cookie——这是一场安全噩梦。
- 缺乏审计轨迹: 当出现问题(而且一定会出现)时,你根本不知道是哪位代理在何时访问了什么。
- 会话管理: Cookie 会过期,站点会轮换令牌。你需要跟踪其新鲜度并知道何时重新认证。
- 多代理混乱: 当你有 5 个代理访问 10 个站点时,Cookie 管理本身就成了一个独立项目。
从脚本到工具
这就是我构建 AgentAuth 的原因。
架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Chrome Extension│────▶│ Encrypted Vault │────▶│ Your Agent │
│ (export once) │ │ (AES‑256) │ │ (scoped access)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
- Chrome 扩展: 在登录网站后点击“导出”。扩展会捕获 Cookie 并发送到本地保险库——不再需要使用 DevTools 手动复制粘贴。
- 加密保险库: Cookie 存储在本地 SQLite 数据库中,使用您自行控制的密码进行 AES‑256 加密。不再分散在脚本中。
- 受限代理: 创建具有特定域名访问权限的命名代理。您的
linkedin-agent只能访问linkedin.com的 Cookie,无法触及银行等其他站点。 - 审计日志: 每次访问都会记录时间戳、代理名称和域名。
示例代码
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)
一行代码即可获取已认证的 Cookie。无需解密代码。没有硬编码的令牌。也没有安全噩梦。
更大的全局
在构建过程中,我意识到 AI 代理的会话管理仍是未解决的基础设施。
- 我们有面向用户的应用的 OAuth。
- 我们有服务器之间通信的 API 密钥。
- 但是 AI 代理呢?它们仍然停留在 2010 年用于网页抓取的那些 hack 上。
业界正在构建日益复杂的代理——能够浏览网页、填写表单、完成购买的代理。然而我们仍在从 DevTools 中复制粘贴 cookie。
AgentAuth 是我尝试解决此问题的方案。它是开源的,今天即可在任何站点上使用(无需 OAuth 采纳),并且可以与 LangChain、Playwright 和 n8n 集成。
链接
- GitHub: https://github.com/jacobgadek/agent-auth
- PyPI:
pip install agentauth-py - n8n node:
npm install n8n-nodes-agentauth
如果你正在构建需要身份验证的代理,试试看吧。如果你想贡献——尤其是 Windows DPAPI 支持或 Firefox 扩展——欢迎提交 PR。
如果你觉得这篇文章有帮助,请给仓库加星。它能帮助更多人发现这个项目。