Electron 发布 cookie 库先查明文再写解密

平台侧核验(第四源)、播放量取数、番茄「审核不通过」作品审计------这些活都不在 MatrixMedia 应用里跑,而是外部脚本。它们共同的前提只有一个:拿到各平台的登录态。这个登录态不需要用户重新登录,也不需要手工导 cookie:Electron 应用的 session 分区早就把它完整落盘了,脚本读出来注入 headless 浏览器即可。

网上教程讲到这一步,几乎都直接跳进 DPAPI 解密。今天做了一次全量普查,结果推翻了一个默认假设------本机 12 个分区 187 条 cookie,一条都没加密。先查明文再写解密,省下的不只是一段 ctypes 代码,还有一条被静默空串带偏的排查弯路。

登录态落在哪里

text 复制代码
%APPDATA%\matrix-video\Partitions\<URL编码的分区名>\Network\Cookies

分区目录名 = 账号名 + 平台中文名,整体经 URL 编码。不可能片场抖音 是 7 个汉字,UTF-8 共 21 字节,quote 之后每个字节展开成 3 个百分号字符,目录名长度正好 63。8 字平台(博客园、百家号、视频号、小红书)长 72,9 字平台(哔哩哔哩、番茄视频、知乎专栏)长 81。目录名长度本身就是个校验器:拼出来的路径不在 63/72/81 这三档里,多半是账号名前缀写错了,而不是文件丢了。

本机实测 12 个分区:博客园、哔哩哔哩、头条、小红书、快手、抖音、掘金、番茄视频、百家号、知乎专栏、视频号,外加一个只有 5 条 cookie 的脏分区 x掘金(历史遗留,来历未查)。所以读侧的过滤要按 host_key 来,别按目录全收------不然掘金平台的 cookie 里会混进 5 条来路不明的。

Cookies 本身是 SQLite 库,表名就叫 cookies,关心的列是 host_key、name、value、encrypted_value、path、is_secure、is_httponly、samesite。

普查:187 条,0 条加密

探针只数字段长度,不碰任何值:

python 复制代码
import os, sqlite3, shutil, tempfile, urllib.parse
root = os.path.join(os.environ['APPDATA'], 'matrix-video', 'Partitions')
tmp = os.path.join(tempfile.gettempdir(), 'probe.db')
for d in sorted(os.listdir(root)):
    db = os.path.join(root, d, 'Network', 'Cookies')
    if not os.path.exists(db):
        continue
    shutil.copy2(db, tmp)
    con = sqlite3.connect(tmp)
    n = con.execute('select count(*) from cookies').fetchone()[0]
    v = con.execute("select count(*) from cookies where length(value)>0").fetchone()[0]
    e = con.execute("select count(*) from cookies where length(encrypted_value)>0").fetchone()[0]
    print(urllib.parse.unquote(d), n, 'plain', v, 'enc', e)
    con.close()
os.remove(tmp)

结果整齐得反常:12 个分区 187 条 cookie,plain 全部等于行数,enc 全部为 0。抖音最多(50 条),视频号最少(2 条,只有 sessionid/wxuin),百家号 32 条(BDUSS/STOKEN/PTO 都在其中)。

为什么 Chromium 会存明文?Chromium 落盘 cookie 时,加密走的是一个可插拔的委托(cookie crypto delegate):委托存在,才把值加密进 encrypted_value;委托不存在,走明文降级------加密是「有委托才加密」,不是「必然加密」。Chrome 浏览器里这个委托永远在,所以大家默认库是加密的;这台机器上的 Electron 构建(matrix-video)显然没把委托接上。具体是哪个开关、哪个版本行为,未深究,也不影响读侧结论。这也提醒一句:换一台机器或换一个 Electron 版本,普查结果可能就不是 0,读代码时两种路径都得留着。

读取顺序:明文优先,DPAPI 兜底

项目里 article_publish/cookies.py 的实现顺序是对的,值得照抄:

python 复制代码
val = row["value"]                       # 明文列优先
if (not val) and row["encrypted_value"]: # 空了才走 DPAPI
    d = _dpapi_unprotect(row["encrypted_value"])
    val = d.decode("utf-8", "replace") if d else ""

DPAPI 兜底路径有一个此前实测踩过的坑:encrypted_value 的 blob 可能带 v10/v11 前缀,解密时两种候选都要试:

python 复制代码
cands = [blob]
if blob[:3] in (b"v10", b"v11"):
    cands.append(blob[3:])      # 去掉前缀再试一次
for cand in cands:
    try:
        return _dpapi_unprotect(cand).decode("utf-8", "replace")
    except Exception:
        continue
return ""

只试一种写法会静默返回空字符串 ,下游拿到空值等于「没有这个 cookie」,表现出来是「登录态失效」------方向完全指错,会让人去重登一个本来好好的账号(10 月 27 日那篇写过,此处只作提醒)。反过来,今天这组数据说明另一侧也有坑:如果默认库必然加密、只写 DPAPI 路径,在这台机器上一条都解不出来 ------明明明文就躺在 value 列里。两个方向的静默失败都指向同一个误诊。

另外两个动作是必须的,不是优化:

  • 复制到临时库再读 。直接开原库会撞上正在运行的 Electron 进程的 SQLite 锁(WAL 模式),表现为读不到新写入的 cookie。shutil.copy2 到临时目录再连,几毫秒的事。
  • 读完立刻删临时库 。里面是可解密的凭据,哪怕是明文,也不该躺在 %TEMP% 里过夜。

注入侧的讲究在之前的文章里拆过,这里只列三条硬结论:

  1. headless 用系统 Chrome,别用 app 二进制 。复用 MatrixMedia 自己的可执行文件会自启 puppeteer-in-electron 抢调试端口,冲突退出。--headless=new --remote-debugging-port=<独立端口> --user-data-dir=<临时目录>,用完不杀全局 chrome。
  2. Network.setCookie 的 domain 保留前导点 。.yueduwuxian.com 写成 yueduwuxian.com 会变成 host-only cookie,不作用于 pugc. 子域,直接被踢回登录页。
  3. 页内 fetch 带 credentials:'include' ,让浏览器自己决定带哪些 cookie------手动拼 Cookie: 头,domain/path/SameSite 任何一项不对就是 401,还查不出原因。

这套通道什么时候会失灵

  • 视频号。只有 2 条 cookie 时 headless 页面会「加载失败」,实测视频号平台侧核验只能借应用自己的浏览器窗口(连它的调试端口读页面)。不是脚本 bug,是 sph 登录态本来就小,缺一条辅助 cookie 页面就起不来。
  • 登录态过期。明文读出来 ≠ 有效。读侧只负责把登录态交给浏览器,有效性由页面行为判定:跳登录页就是失效,别在解密环节找原因。
  • 脏分区 。x掘金 这种历史遗留目录,按 host_key 过滤天然免疫,但按目录名批量读就会中毒。
  • 写操作不接自动链路。这条通道适合核验、取数、审计这类只读活;删除作品这种写操作另有规矩(默认 dry-run、显式确认、不进自动化),今天不展开。

小结

一条被大多数人跳过的普查,结论就三句:Electron 分区 cookie 库不必然加密,先 count 一遍再决定要不要写解密;读取顺序永远是明文优先、DPAPI 兜底、v10/v11 双候选;临时库复制防锁、用完即删。187 条 cookie 的普查探针只读不写、不打印任何值,随时可以重跑复核。

相关推荐
郭小李4 小时前
springmvc 拦截器
java·开发语言·前端
web打印社区4 小时前
Chrome-kiosk-printing能上生产吗
开发语言·前端·javascript·chrome·websocket
白帽攻防录4 小时前
某pc客户端app从xss到rce实战案例
前端·网络安全·黑客·xss·rce·src漏洞·app漏洞
志尊宝4 小时前
AI原生前端开发(041):AI辅助的状态管理(Zustand/Pinia)
前端·javascript·ai-native
修炼的dance5 小时前
pngquant 两档质量后接 oxipng,每步省多少花几秒
前端
费曼学习法5 小时前
无人值守AI内容矩阵实战(10):Windows 无 systemd,进程存活率 99% 怎么做
前端
小兔子5 小时前
Integer 的 == 在 100 时是 true,在 1000 时是 false
前端
尘世中一位迷途小书童5 小时前
和 AI 打交道会撞见的一整串词:从大模型到 Agent、RAG、MCP、Skill,一次串清
前端·人工智能
lusklusklusk5 小时前
Python 全局变量与局部变量区别——global 关键字、__main__ 块与作用域示例
开发语言·前端·python