dsh-plugin-qqchat(v1.0.0)
只读的 QQ 本地聊天记录获取插件,挂在 DSH GUI 里用。
红线:不发消息、不注入 QQ 进程、不 hook、不改 QQ 安装包、不碰网络协议、不调用云端模型。 所有读取都在拷贝出来的快照上进行,原库零写入。
当前进度
| 阶段 | 内容 | 状态 |
|---|---|---|
| P1 | 骨架 + qq_doctor + GUI 可见 |
✅ |
| P0 | 剥 1024 头 + 非标准 SQLCipher 解密(已跑通真机) | ✅ |
| P5 | 只读内存扫描取密钥 + 自动解密(qq_find_key / qq_decrypt) |
✅ |
| P3 | 40800 protobuf 解析 + 图片/视频/表情/语音/文件定位(qq_media) |
✅ |
| P4 | 归一化库 + 会话隔离 + 增量摘录(qq_sync / qq_conversations / qq_messages) |
⏳ |
| P5b | 导出 jsonl/csv/md + 脱敏(qq_export) |
⏳ |
| P6 | mock 后端(离线自测/CI) | ⏳ |
| P7 | 打包 + 端到端验收 | ⏳ |
真机战绩(QQ NT 9.9.35.52892,账号 3960766092):
- 取密钥:7 秒扫完 QQ 进程内存,18/18 个非空库全部配对(另有 3 个空库本就没有数据);
- 解密:
nt_msg.db8290 页 / 3 秒,33 张表,群消息 33,233 条 + 私聊 165 条; - 媒体:扫描 33,790 条消息,解析出图片/表情/视频/语音,308 个媒体元素定位到本机真实文件。
规划与调研为内部文档,未随仓库发布;本仓库只保留可复现的实现与实测结论。
⚠️ 调研报告里写的"16 字节 ASCII 口令 +
kdf_iter=4000(走 PBKDF2 派生 enc_key)" 在本机不成立。实测是每库一把 32 字节 raw key,enc_key 直接用它,不过 PBKDF2。 见下节。
已实测的 QQ NT 容器结构
在本机 QQ NT 9.9.35.52892、账号 3960766092、21 个库上逐字节确认:
offset 0-15 : "SQLite header 3\0" 假魔数(真魔数是 "SQLite format 3")→ sqlite3 报
"file is not a database"
offset 32-39 : "QQ_NT DB" 容器魔数
offset 44 : 12 80 01 protobuf: field2, LEN=128
offset 47..174: 128 个 ASCII hex 字符 64 字节全局指纹(21 个库完全相同,**不是密钥**)
: 1a 07 "1.1.0.1" 格式版本
: 22 09 "HMAC_SHA1" MAC 算法(页 MAC 用 SHA1,与 reserve 48 相符)
: 28 <varint> 创建时间戳
[0, 1024) : 明文头页(其余为零)
[1024, EOF) : SQLCipher 加密区,页大小 4096;载荷前 16 字节 = **每库独立的 salt**
判据 (size − 1024) % 4096 == 0 在 21/21 个库上全部成立。
实测确认的页布局(用 SQLite 头部自校验反推,见 qqdec_hdr.ps1 的判据):
page 1 : [salt 16][ciphertext 4032][IV 16][HMAC 20][pad 12] 共 4096
page n : [ciphertext 4048] [IV 16][HMAC 20][pad 12] 共 4096
→ crypto reserve = 48,IV 位于页内偏移 (4096 - 48)
解密 : AES-256-CBC,key = 32 字节 raw key,IV 取页内那一处
page 1 还原 = "SQLite format 3\0" + 明文(4032) ← salt 顶掉了真正的魔数
⚠️ reserve 不是全局常量:本机 18 个库里 17 个是 48(HMAC-SHA1 布局),
guild1.db是 80(16 IV + 64 HMAC-SHA512)。所以解密器逐个库用头部自校验自动探测 (先试 48 再试 80,哪个能解出合法 SQLite 头就用哪个),不套全局默认值。 实测结果:18/18 个库全部解密成功且 sqlite3 可读。
密钥:每库一把,形态是内存里的 x'<64hex key><32hex salt>',
后半段 salt 与文件载荷前 16 字节逐字节相同(这就是配对依据,不是猜)。
enc_key = 那 32 字节本身,不走 PBKDF2。
已知未解:页面 HMAC 的
mac_key派生方式还没定(当前 0/8290 通过)。 但解密正确性不依赖它——判据是 SQLite 自己把db size in pages等头字段对上、 且 sqlite3 能完整读出 33 张表与 33k+ 行。HMAC 只当额外的完整性校验继续啃。
消息体(40800)与媒体
45001 元素 id 45002 元素类型 45101 文本
45402 文件名 45405 大小 45406 MD5(16B) 45411/45412 宽高 45418 原图
45815 显示文本(如 '[图片]'/'[动画表情]') 45804 下载路径
45002 |
语义 | 实测条数(群消息抽样) |
|---|---|---|
| 1 | 文本 | 17,940 |
| 2 | 图片 / 表情(靠尺寸、扩展名、45815 区分:≤200×200 或有"表情"字样即表情) |
5,426 |
| 3 / 5 | 视频 | 186 |
| 4 | 语音 | 18 |
| 6 | 系统表情(476xx) |
274 |
| 7 | @ 提及 | 3,849 |
| 8 | 引用(47705 被引用人、47713 原文) |
4,816 |
| 10 | 小程序卡片(47901 JSON) |
180 |
| 11 | 商城表情(80900 名字如 [举杯]、80903 MD5) |
263 |
媒体在磁盘上的落点(Windows 按月目录 + 小写 MD5 文件名):
Pic\<月>\Ori\<md5>.<ext> Pic\<月>\Thumb\<md5>_0.png / _720.png
Video\<月>\Thumb\<md5>_0.png 视频原片通常没落盘 → 返回封面并标注是缩略图
Ptt\<月>\Ori\<md5>.amr File\Thumb\<md5>_750.jpg
Emoji\emoji-recv\<月>\Ori\<md5>.jpg Emoji\marketface\<包id>\<md5>[_aio.png]
绝不套用安卓那套
Cache_+ CRC64 命名;按 MD5 建索引再查是最稳的。
工具清单
| 工具 | 作用 |
|---|---|
qq_doctor |
环境体检:QQ 版本/进程、账号目录、21 个库的形态与页数、容器格式、密钥状态、媒体目录、下一步 |
qq_find_key |
先看有没有,缺了才读内存:从运行中的 QQ 进程只读取每库 raw key(不注入/不 hook),按 salt 配对,默认不回显明文 |
qq_decrypt |
拷贝 → 剥头 → 解密 → SQLite 头部自校验 → sqlite3 实开验证;校验不过拒绝采信 |
qq_media |
解析消息里的图片/视频/表情/语音/文件,并定位到本机真实文件(优先原图,其次缩略图;没有就如实报 false) |
安装
首次安装:先在本目录运行
install.ps1(Windows)或node install.mjs(macOS/Linux)。 脚本按当前目录自动生成cordis.patch.yml(已.gitignore),不写死任何盘符。
缺少环境怎么办(默认自动安装)
安装时 install.ps1 默认静默安装缺失环境;只想检查时加 -CheckOnly:
powershell -ExecutionPolicy Bypass -File .\install.ps1
# 只检查、不安装:
powershell -ExecutionPolicy Bypass -File .\install.ps1 -CheckOnly
插件加载时也会在后台运行 bootstrap.ps1 -AutoInstall -Quiet(6 小时内不重复),不阻塞 DSH 启动;首次装完可能需要重试工具。
- Python:优先用 DSH 自带运行时;没有则
winget install Python.Python.3.12 --scope user。 - QQ NT:
winget install --id Tencent.QQ.NT -e。 - 安装完 QQ 后仍需手动登录;插件只读已登录账号的本地库,不代登录。
- 关闭自动安装:patch 的
config里设autoInstall: false,或设置环境变量DSH_BOOTSTRAP=0。
任选其一:
- GUI:「设置 → 插件 → 安装 → 本地路径」填本目录的绝对路径(例如
%USERPROFILE%\.dsh\plugins\qqchat); - CLI:
plugin_manager install_bundle file:<本目录的绝对路径>。
装完可在 GUI 的「插件」页整体启用/停用。
改代码不会自动生效(ESM 按 URL 缓存):重启 DSH,或换一个新的 entry id; 只有改了
cordis.patch.yml(挂载参数)才需要重新install_bundle。
零依赖
- JS 半边:只用 Node 内置模块(按绝对路径挂载的插件没有可达
node_modules); - 引擎:只用 Python 标准库;
- 密码学:走 PowerShell 5.1 的 .NET(
Rfc2898DeriveBytes+HMACSHA1+ AES,已实测可用) —— 本机 Python 没有任何 crypto 库,所以不引入sqlcipher3/Python 3.14 依赖。
目录
qqchat/
├─ package.json / cordis.patch.yml.tpl / config.example.json / README.md
├─ lib/index.js 宿主半边:qq_* 工具(零依赖桥)
├─ engine/qqchat/ 引擎(仅标准库)
│ ├─ cli.py 命令行契约:stdout 只有一个 JSON 对象
│ ├─ paths.py QQ 安装/版本/账号/媒体目录发现(不硬编码 Documents)
│ ├─ qqnt.py 容器头解析 + 页算术 + 形态识别
│ ├─ doctor.py 环境体检
│ └─ config.py 配置加载
└─ data/ 摘录产物(自动创建)
可移植性 / 换机器安装
这个仓库不包含任何本机绝对路径、QQ 号或解密数据:cordis.patch.yml 由安装脚本按当前目录生成(已 .gitignore),config.json、data/、*.log 都不入库。
换一台机器:
git clone本仓库到任意目录;- 在仓库目录运行
powershell -ExecutionPolicy Bypass -File .\install.ps1(Windows)或node install.mjs(macOS/Linux)——生成cordis.patch.yml,并准备一份config.json; - 在 DSH 里执行
plugin_manager install_bundle file:<本目录的绝对路径>; - 运行
qq_doctor做环境体检,按提示填config.json。
运行环境
- Windows 10/11(x64):QQ NT 的本地库、媒体目录与 PowerShell 解密都按 Windows 实现。
- PowerShell 5.1+(Windows 自带):解密走 .NET 的
Rfc2898DeriveBytes+HMACSHA1+ AES。 - Python 3.11+:优先使用 DSH 自带运行时;引擎只用标准库。
- macOS / Linux 未适配:
engine/qqchat/paths.py与qqnt.py需要自行移植。 - 只读:不发消息、不注入 QQ 进程、不调用云端模型;数据库口令只在运行时从内存读取,用完即弃。
No comments yet. Be the first to write one.