解开「每库一钥」:拾语直连微信聊天数据库的实现
拾语的核心能力建立在一个前提上:能直接读取 本人设备上、本人账号 的微信聊天数据库。这件事说起来一句话,做起来要连闯四关——数据库是 SQLCipher 加密的;微信 4.x 把密钥拆成了「每个库一把」;密钥不在任何文件里,而在微信进程的内存里;微信还在持续往库里写数据。这篇文章把这四关逐一拆开讲。…

目录(展开)
拾语的核心能力建立在一个前提上:能直接读取本人设备上、本人账号的微信聊天数据库。这件事说起来一句话,做起来要连闯四关——数据库是 SQLCipher 加密的;微信 4.x 把密钥拆成了「每个库一把」;密钥不在任何文件里,而在微信进程的内存里;微信还在持续往库里写数据。这篇文章把这四关逐一拆开讲。
先说清边界:以下内容是对自有工具实现思路的介绍,服务于「访问自己的数据」这一目的——工具的运行前提是本机微信已经登录、数据库就在用户自己的机器上。它不提供绕过任何身份认证的手段,也不应被用于访问他人数据;涉及聊天记录中他人个人信息时,请遵守《个人信息保护法》关于个人合理使用的规定。文中不附可直接复用的完整代码。
第一关:SQLCipher 页结构
微信 3.x / 4.x 的聊天库都是 SQLCipher 加密的 SQLite,文件和明文 SQLite 只有两个字面区别:
- 文件头 16 字节不是
SQLite format 3\0,而是 salt(KDF 的盐); - 每一页的布局是
[密文][IV 16B][HMAC],页尾按参数档位保留一段校验区。
第 1 页比较特殊:前 16 字节被 salt 占用,密文从偏移 16 开始。解密一页就是标准流程——密钥 + salt 经 KDF 派生出加密钥和 HMAC 钥,AES-CBC 解密,页尾验 HMAC。
真正的问题在 KDF 参数。SQLCipher 各版本的默认档位不同:
| 档位 | KDF | 迭代 | HMAC | 页尾保留区 |
|---|---|---|---|---|
| SQLCipher 4 | SHA512 | 256000 | HMAC-SHA512 | 80 |
| SQLCipher 3 | SHA1 | 64000 | HMAC-SHA1 | 48 |
| 变体若干 | — | — | HMAC-SHA256 / 无 HMAC | 32 / 64 / 16 |
拾语的做法不是「猜版本」,而是试解 + 结构校验:用候选档位解第 1 页,解出来之后检查 SQLite 头部的 page-size / reserved 字段是否自洽(page size 等于实际页大小、保留区大小与档位一致),命中才算确认,不中就换下一档。HMAC 校验过滤掉绝大多数错误档位,结构校验兜住剩下的歧义。这套逻辑在 sqlcipher.js 里约 300 行,覆盖了 7 个参数档位。
第二关:每库一钥
微信 3.x 时代一个账号一把密钥,很多老工具按这个假设写。4.x 改了设计:每个 .db 各有一把独立密钥——message_0.db、contact.db、session.db 互不通用,不存在可复用的主口令。这不是版本 bug,是设计如此。
所以拾语要维护的不是「一把钥匙」,而是一张 salt → key 的密钥环:
- 打开某个库前,先读它第 1 页前 16 字节拿到 salt;
- 拿 salt 去密钥环里认领对应的钥匙;
- 没有就报「密钥不完整(2/4 个库)」,提示补齐。
密钥环持久化在 wechat-keys.json,整个环作为一段密文落盘(有系统密钥链时走 Electron safeStorage,即 Windows 上的 DPAPI),没有密钥链的环境才退回明文。这个设计还有一个兼容细节:早期版本只存单把 key,读取时自动包成 KeyRing,后续成功解锁其它库时逐步补全成完整密钥环——老用户的配置文件不需要迁移脚本。
第三关:密钥在内存里
密钥文件在磁盘上并不存在,它只活在微信进程的内存里。微信 4.x 把每个库的密钥放在 Config.Cipher 数据块中,格式是一段 ASCII 文本:x'<64 位密钥><32 位 salt 十六进制>',而整个数据块按 32 字节的掩码循环异或做混淆。
这就把问题变成了一个内存特征扫描,思路分四步:
- 先拿 salt。要读的每个库,第 1 页前 16 字节的 salt 是已知的(文件头上就有,不需要解密),把它转成十六进制文本。
- 构造特征串。salt 的十六进制文本套上那个 32 字节掩码——掩码是循环异或的,所以同一个明文特征在内存里有 32 种相位偏移,全部生成。
- 扫描。用 PowerShell + C# 的小扫描器(
memscan.cs)只读微信主进程Weixin.exe的私有可写内存——Config.Cipher对象在堆上,不需要碰映射文件或共享段。匹配特征串,命中位置往前 64 个字符就是同一数据块里的密钥。 - 回验。每个命中都拿真实数据库第 1 页再解一次:HMAC 对得上才算数,误报直接丢弃。
实测 4 个库、约 245 MB 私有堆,1~2 秒出全量密钥。扫描只读不写,全部 salt 命中就提前收工。结果写入密钥环,下次启动不用再扫。
两个工程细节值得一提:
- 打包路径问题:扫描器是 PowerShell 脚本,而 electron-builder 打包后代码在
app.asar里,PowerShell 读不了 asar 内部路径。这几个文件放进asarUnpack,运行时做一次.asar→.asar.unpacked的路径映射。 - 兜底路径:特征串没命中(微信改版、掩码变化)时,退回「对单个库暴力试解第 1 页」——用已知 salt 直接穷举验证,慢,但只依赖加密格式本身,不依赖内存布局。
第四关:微信还在写
解密不是一次性的。微信随时在收消息,刚收到的内容常常还在 -wal 文件里没合并进主库。拾语的处理:
- WAL 叠加:解密主库页之后,把 WAL 里已提交的页按帧序叠加进去,保证刚收到的消息不丢;
- 缓存失效:解密结果写到
%APPDATA%\shiyu\wechat-cache\*.db,文件名带上原库的 size + mtime——微信一写入,mtime 变了,缓存自动作废重解; - 退出即清:应用退出时先释放连接,再删除缓存目录并校验无残留。明文库不该在磁盘上过夜。
读到数据之后:结构自适应
解开了库,还有一层不确定:微信大版本可能改表名、改列名。拾语的策略是结构自适应而不是写死——schema.js 在启动时探测每个库的表名列名再映射。比较麻烦的是 4.x 的消息表按哈希命名(MSG_ + hash),表结构未知就跳过,靠设置里的「结构诊断」打印出实际表名列名来补适配。读出来的消息统一归一化成 { chatId, who, senderId, ts, time, content, type, quote? },图片、语音、文件、引用、位置、系统消息都转成可读文本。
检索:为什么不用向量库
到了问答这一层,最常见的方案是向量化 + 相似度检索。拾语选了另一条路:纯内存关键词检索,不引入向量库。理由有三:
- 材料是单会话的消息,不是十万级文档库,规模上限是「几千条消息」,毫秒级就能算完;
- 聊天记录里向量检索的优势场景(同义改写)远不如关键词命中可靠——用户问的就是聊天里出现过的词;
- 桌面应用的依赖和内存预算有限,为一个单会话场景引入嵌入模型不划算。
具体做法(retrieval.js,234 行):
- 分词:拉丁词按词切,中文按 bigram 切(「交付时间」→「交付」「付时」「时间」),过一遍人工维护的中英停用词表。bigram 对中文检索是性价比极高的选择,不需要分词器。
- 切段:按时间间隔(默认 30 分钟无消息即断)、标点收尾和 1200 字预算把消息流切成段——段的边界尽量贴着对话的自然边界。
- 打分:用问题里的关键词对每段命中计分,取命中段加相邻段做上下文(答案常在命中段的隔壁)。
- 清理:进入模型上下文前剥掉「引用 …」这类微信附加文本和小程序链接,避免污染关键词与上下文。
回答下方会显示「检索 x/y 段 · n 字」,检索了哪些段、一共多少字,如实呈现。
工程分层:orchestration 不认识 Electron
最后一个架构决定:整个业务逻辑(数据源路由、解密、检索、LLM 调用、线程管理)放在 orchestration/ 目录,不 import 任何 Electron API。Electron 主进程只是它的一个宿主,CLI 和测试是另外两个宿主。带来的直接收益:
- 7 个测试文件全部用
node --test直接跑,不需要起 Electron; scripts/wechat-status.js能以纯 Node 进程做数据库体检(--scan --report --chat <群wxid>);- 层间词汇表收敛在一份 README 里:对外统一
chatId,数据库里的talker/StrTalker/username都是实现细节。
orchestration 层约 4700 行,其中最大的单文件是 store.js(1104 行)——会话、消息、增量刷新、缓存调度都在这里,它是有资格继续拆的下一个目标。
小结
回头看这四关:SQLCipher 参数靠「试解 + 结构校验」而不是猜;每库一钥靠 salt → key 密钥环而不是全局钥匙;内存取钥靠「salt 特征串 + 32 相位 + 回验」而不是信任内存布局;微信持续写入靠 WAL 叠加和 mtime 缓存失效。每一关的共同点是:不假设环境永远不变,把验证做在每个不可信的边界上。这个原则放在哪里都成立。
最后重申适用边界:这套实现只面向「读取本人设备上本人账号的数据」这一场景,前提是本机微信已通过自身的身份验证;它不构成对任何账号体系的攻击手段,也不应被用于访问他人数据。技术描述出于互操作与自有数据访问的目的,具体使用请遵守相关法律法规。

