桌面开发8 min2 次浏览

解开「每库一钥」:拾语直连微信聊天数据库的实现

拾语的核心能力建立在一个前提上:能直接读取 本人设备上、本人账号 的微信聊天数据库。这件事说起来一句话,做起来要连闯四关——数据库是 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. 打开某个库前,先读它第 1 页前 16 字节拿到 salt;
  2. 拿 salt 去密钥环里认领对应的钥匙;
  3. 没有就报「密钥不完整(2/4 个库)」,提示补齐。

密钥环持久化在 wechat-keys.json,整个环作为一段密文落盘(有系统密钥链时走 Electron safeStorage,即 Windows 上的 DPAPI),没有密钥链的环境才退回明文。这个设计还有一个兼容细节:早期版本只存单把 key,读取时自动包成 KeyRing,后续成功解锁其它库时逐步补全成完整密钥环——老用户的配置文件不需要迁移脚本。

第三关:密钥在内存里

密钥文件在磁盘上并不存在,它只活在微信进程的内存里。微信 4.x 把每个库的密钥放在 Config.Cipher 数据块中,格式是一段 ASCII 文本:x'<64 位密钥><32 位 salt 十六进制>',而整个数据块按 32 字节的掩码循环异或做混淆。

这就把问题变成了一个内存特征扫描,思路分四步:

  1. 先拿 salt。要读的每个库,第 1 页前 16 字节的 salt 是已知的(文件头上就有,不需要解密),把它转成十六进制文本。
  2. 构造特征串。salt 的十六进制文本套上那个 32 字节掩码——掩码是循环异或的,所以同一个明文特征在内存里有 32 种相位偏移,全部生成。
  3. 扫描。用 PowerShell + C# 的小扫描器(memscan.cs)只读微信主进程 Weixin.exe 的私有可写内存——Config.Cipher 对象在堆上,不需要碰映射文件或共享段。匹配特征串,命中位置往前 64 个字符就是同一数据块里的密钥。
  4. 回验。每个命中都拿真实数据库第 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? },图片、语音、文件、引用、位置、系统消息都转成可读文本。

检索:为什么不用向量库

到了问答这一层,最常见的方案是向量化 + 相似度检索。拾语选了另一条路:纯内存关键词检索,不引入向量库。理由有三:

  1. 材料是单会话的消息,不是十万级文档库,规模上限是「几千条消息」,毫秒级就能算完;
  2. 聊天记录里向量检索的优势场景(同义改写)远不如关键词命中可靠——用户问的就是聊天里出现过的词;
  3. 桌面应用的依赖和内存预算有限,为一个单会话场景引入嵌入模型不划算。

具体做法(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 缓存失效。每一关的共同点是:不假设环境永远不变,把验证做在每个不可信的边界上。这个原则放在哪里都成立。

最后重申适用边界:这套实现只面向「读取本人设备上本人账号的数据」这一场景,前提是本机微信已通过自身的身份验证;它不构成对任何账号体系的攻击手段,也不应被用于访问他人数据。技术描述出于互操作与自有数据访问的目的,具体使用请遵守相关法律法规。

相 关 文 章

3 篇
全部文章
桌面开发

拾语:让微信聊天记录变成可以提问的记忆

想象一个很具体的场景:你记得三年前和某个朋友聊过一件很重要的事,只记得大概聊过「预算」和「周五」,但翻了半小时聊天记录还是没找到。微信自带的搜索能按关键词定位到某几条消息,可它回答不了「我们当时到底商量的什么」——它只会把消息一条条列给你,阅读和归纳还是你自己的事。 「拾语」(Shiyu,版本 1.…

6 min

Android 开发

把点划钉在纳秒上:FlashMorse 的时序引擎与 Miuix 界面工程

上一篇《闪光摩斯:让手电筒替你说话》讲了这个 App 想解决什么问题。这一篇拆工程实现:一条从文本到光的链路,以及为了让「点划比例」在任何设备上都站得住,踩过的那些坑。 所有结论都来自源码与真机实测,不含推测。源码以 MIT 许可开源在 Fq77/FlashMorse,下面引用的每一段代码都可以在仓…

12 min

移动开发

从 View 到 Miuix Compose:来咔!的界面重构与离线色彩管线

接上一篇《来咔!把一整间暗房装进一台离线手机》,这篇拆「来咔!」的工程骨头:一条从导入到出片的离线色彩管线,以及把它从 View/XML 搬到 Miuix Compose 的过程。所有结论都来自对源码的实读,不臆测。 项目来源与署名 :本文所涉「来咔!(leicaMoments)」是基于开源项目 L…

7 min

© 2026 风起 版权所有|永远相信美好的事情即将发生!

隐私说明豫ICP备2026055814号-1豫公网安备41018302000627号