逆向工程 Chrome 的 Cookie 加密(以验证 AI 代理)

发布: (2026年1月12日 GMT+8 02:52)
11 min read
原文: Dev.to

Source: Dev.to

(请提供需要翻译的正文内容,我将为您翻译成简体中文。)

问题 – 登录页面

如果你已经构建了与网站交互的 AI 代理,你一定遇到过这道墙:登录页面

你的代理需要:

  • 检查 LinkedIn 通知
  • 抓取仪表盘数据
  • 向平台发布内容

但网站要求进行身份验证。大多数开发者的第一步是打开 Chrome DevTools,复制 Cookie 头部,然后粘贴到脚本中。

它可以工作……大约 24 小时。随后会话过期,自动化在凌晨 3 点崩溃,你会被愤怒的警报叫醒。

我厌倦了这种循环。Chrome 已经在本地存储了我的已认证会话。我现在已经登录了 LinkedIn。如果我的代理能够直接使用它呢?

事实证明是可以的——但 Chrome 并没有让这件事变得容易。

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 大约在 2014 年开始加密 Cookie,以防止恶意软件轻易窃取会话。加密方式因平台而异。

平台主密钥存储位置加密算法
macOSmacOS Keychain 条目 “Chrome Safe Storage”AES‑128‑CBC(密钥通过 PBKDF2 派生)
LinuxGNOME Keyring / KWallet 通过 libsecret(回退:文字 peanutsAES‑128‑CBC
Windows数据保护 API(DPAPI)——绑定到已登录的 Windows 用户DPAPI(无可提取的密钥)

这三种平台都会在加密负载前加上文字字符串 v10 以指示版本。

使用 Python 解密 Cookie(macOS 示例)

下面是一个最小的、独立的脚本,它:

  1. 从 macOS 钥匙串中获取 Chrome 的主密钥。
  2. 使用 PBKDF2 派生 AES‑128 密钥。
  3. 解密 encrypted_value 数据块。
  4. 返回 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}")

脚本的工作原理

  1. 复制 Chrome 的 Cookies SQLite 文件到临时位置(Chrome 会锁定原始文件)。
  2. 查询 host_key 包含 “linkedin” 的所有行。
  3. 从 macOS 钥匙串中获取 主密钥(Chrome Safe Storage)。
  4. 通过 PBKDF2‑HMAC‑SHA1 推导 AES‑128 密钥。
  5. 解密 每个 encrypted_value(跳过 v10 前缀,使用 16 个空格的静态 IV,并去除 PKCS#7 填充)。
  6. 返回 一个明文字典,可直接用于 requests 或任何 HTTP 客户端。

TL;DR

  • Chrome 将 cookie 存储在 SQLite 数据库 (Cookies) 中。
  • 在 macOS 上,cookie 值使用 AES‑128‑CBC 加密;密钥来源于存储在钥匙串中的密码 (Chrome Safe Storage)。
  • 通过提取该密码、派生 AES 密钥并解密 encrypted_value blob,你可以以编程方式复用浏览器已有的会话 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 集成。

链接

如果你正在构建需要身份验证的代理,试试看吧。如果你想贡献——尤其是 Windows DPAPI 支持或 Firefox 扩展——欢迎提交 PR。

如果你觉得这篇文章有帮助,请给仓库加星。它能帮助更多人发现这个项目。

Back to Blog

相关文章

阅读更多 »