平台侧核验(第四源)、播放量取数、番茄「审核不通过」作品审计------这些活都不在 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%里过夜。
拿到 cookie 之后
注入侧的讲究在之前的文章里拆过,这里只列三条硬结论:
- headless 用系统 Chrome,别用 app 二进制 。复用 MatrixMedia 自己的可执行文件会自启 puppeteer-in-electron 抢调试端口,冲突退出。
--headless=new --remote-debugging-port=<独立端口> --user-data-dir=<临时目录>,用完不杀全局 chrome。 Network.setCookie的 domain 保留前导点 。.yueduwuxian.com写成yueduwuxian.com会变成 host-only cookie,不作用于pugc.子域,直接被踢回登录页。- 页内
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 的普查探针只读不写、不打印任何值,随时可以重跑复核。