微信聊天记录解密导出:SQLCipher密钥派生与Python实现全解析
简介chatlog 是一款用于导出微信聊天记录的本地化工具源码GitHub 原仓库已下架这份资源相当于完整源码备份。面向需要离线解析微信数据库、进行二次开发或研究本地数据提取技术的开发者可在 Windows、macOS 或 Linux 环境自行编译使用。资源共 151 个文件压缩包仅 243KB核心代码以 Go 编写131 个 .go 文件涵盖文件复制、时间处理、数据源连接、消息解析等模块另配套 .proto 协议定义、Markdown 说明文档、YAML 配置、Dockerfile 与 shell 脚本便于快速构建与部署。目前已有 122 人学习下载适合中等水平以上开发者参考。源码内部包含数据库连接与字段映射、消息类型识别、附件路径提取等实现目录结构清晰可学习 SQLite 结构分析及 Go 工程组织方式对于需要存档微信聊天记录或构建同类工具的研究者这份源码可提供可直接运行的参考起点避免从零逆向。1. chatlog 下架之后微信聊天记录导出工具到底在解什么难题用 chatlog 导出微信聊天记录曾经是 GitHub 上能找到的最直接方案。工具本身不复杂读手机里的加密数据库解出明文再导出成网页或文本。问题是现在 GitHub 上已经搜不到它的源码找过来的朋友往往卡在同一步——仓库链接失效教程里的命令又没看懂。下面把 chatlog 类工具背后那条链路完整拆开记录存在哪、密钥怎么来、怎么用命令解、怎么写脚本导。适合自己持有手机、想给聊天记录做离线归档的人也适合想搞懂微信本地数据结构的同学。整条链路并不依赖某个特定工具的源码只要原理清楚自己重建一个完全可行。2. 先摸清微信本地数据库EnMicroMsg.db、SQLCipher 与密钥派生公式2.1 微信把聊天记录存在手机哪里Android 微信的数据全部落在应用私有目录 /data/data/com.tencent.mm/ 下真正的聊天数据库是 MicroMsg 目录里某个 32 位 hash 子目录下的 EnMicroMsg.db。这个 32 位 hash 目录对应「设备加账号」的组合换手机号或换设备登录目录名会变。里面除了 EnMicroMsg.db还有 WxFileIndex.db、WxVideoIndex.db 等辅助库聊天记录主体就是 EnMicroMsg.db。数据库文件是 SQLCipher 加密过的 SQLite直接打开只会看到乱码头部也没有普通 SQLite 的 SQLite format 3 标志。这里要强调一个常见误区只拷贝 EnMicroMsg.db 一个文件是不够的。老版本微信把账号内部编号 uin 存在 /data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml 里后续密钥派生要用它。备份时要把整个 MicroMsg 目录连同 shared_prefs 一起拷走否则拿到数据库也推不出密钥。这也是很多导出手法第一次尝试就失败的原因——数据库拿到了配置和账号信息没拿到。微信选择整库加密而不是只加密敏感字段对导出工具意味着两件事。第一必须先把整库解开才能用任何 SQL 工具读取第二密钥不对时 SQLCipher 不会给密码错误这种友好提示而是直接报 file is not a database。这个「明明有库却打不开」的现象是后面避坑章节的重点。2.2 表结构message 表、rcontact 表与常用字段解开之后露出的是普通 SQLite 表结构。最核心的是 message 表字段包括 msgId、msgSvrId、type、status、isSend、createTime、talker、content、imgPath 等。type 表示消息类型isSend 表示是不是自己发的talker 是会话对象的 wxidcontent 是消息正文。文本消息 content 就是字面文字图片、语音这类消息 content 里存的是路径或描述信息真正的文件按 imgPath 指向外部目录。语音消息的时长信息存在 lvbuffer 字段里二进制格式导出时要单独解析。rcontact 表是通讯录username 对应 message.talkernickname 是昵称remark 是备注。导出时想显示「谁说了什么」必须把 message 和 rcontact 按 usernametalker 关联。群聊特殊一些message 表里群成员的 talker 是成员的 wxid但成员在本群的昵称存在 chatroom 表的 memberlist 字段里JSON 结构解析时要单独处理。我一般先查 sqlite_master 把表名和建表语句打印出来再决定读哪张不拿多年前的字段名硬套——微信不同版本加过不少字段硬编码列名会翻车。2.3 SQLCipher 版本差异为什么有的库要指定 cipher_compatibilitySQLCipher 从 2.x 走到 4.x密钥派生参数一直在变。最影响导出的是 KDF 迭代次数3.x 默认 64000 次4.x 默认 256000 次。微信内置的 SQLCipher 版本跟着应用版本走老版本微信导出的库用新版 sqlcipher 打开时不指定兼容模式密钥派生参数对不上表现就是库能打开但读不出数据。命令行工具对应两个开关PRAGMA cipher_compatibility3 告诉引擎按 3.x 的 KDF 参数解析PRAGMA cipher_migrate 把老库页面重写成新格式。实际操作中我会固定用一套顺序先 PRAGMA key再 PRAGMA cipher_compatibility最后才考虑 cipher_migrate。顺序不能反migrate 依赖正确的 key 已经生效。另外不同版本 message 表的结构差异不大但 sqlite_master 里建表语句长得不一样导出脚本不要硬编码列名先 SELECT * 看一遍实际列更稳妥。2.4 密钥派生IMEI、UIN 与 md5(imeiuin) 前七位Android 微信数据库口令公开资料里最常用的一条规则是md5(IMEI UIN) 的十六进制结果取前 7 位。IMEI 是设备串号UIN 是账号在微信服务器上的内部编号。UIN 存在 /data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml 里形如int nameuin value12345/。注意不一定是 uin 这个字段名较新版本可能要去 auth_info_key_prefs.xml 里找。之前的工具源码消失后社区里流传的多数扒库脚本仍沿用这条规则新版本微信如果推不出来多半是 uin 拿错了账号或 IMEI 输成了双卡的另一张。iOS 端是另一套逻辑数据在 iTunes 备份的 App 容器里密钥派生会用到设备 UDID 和更多参数与 Android 不同。本文后面的脚本默认按 Android 走iOS 单独说一句先解备份拿到 Documents 下的库文件再按 iOS 规则处理步骤比 Android 多一跳。想先跑通整条链路的话建议用 Android 老版本微信的库起步成功率最高。3. 复现 chatlog 的第一步用 sqlcipher 命令行把加密库解密成明文库3.1 装一个能用的 sqlcipher版本选择直接影响成败命令行解密是整个导出链路里最值得先跑通的一步。安装方面macOS 用 brew install sqlcipherDebian/Ubuntu 用 apt install sqlcipher。装完先看版本sqlcipher --version。这里有个坑apt 源里的 sqlcipher 往往停留在 3.xbrew 的通常是 4.x。对微信老库来说 3.x 反而省事默认兼容参数就对用 4.x 就必须在会话里补 cipher_compatibility3。别急着自己编译最新版发行版自带的够用。我一般把 sqlcipher 当作黑匣子阶段工具只用来把加密库转成明文库后续解析全部交给标准 sqlite3。这样全链路里只有一步依赖编译产物出问题好隔离。踩坑的人多半是在「让 Python 直接读加密库」上面花了大半天最后发现是 sqlcipher 版本和库版本不匹配。3.2 打开加密库并设置密钥三条 PRAGMA 的正确会话拿到 EnMicroMsg.db 和 7 位密钥后先做一个最小验证sqlcipher /path/to/EnMicroMsg.db进入交互后依次执行PRAGMA key xxxxxxxxx; -- 7 位十六进制口令 PRAGMA cipher_compatibility 3; -- 按 SQLCipher 3.x 规则解析 PRAGMA cipher_migrate; -- 老库转新格式可选第一条是核心。key 设置的是口令字符串SQLCipher 内部用 PBKDF2 派生真正的 AES 密钥所以不需要把它补成 32 字节。第二条让解析器按 3.x 的 KDF 参数跑老库必须加。第三条只在提示需要迁移时执行如果接下来马上要 ATTACH 导出明文库migrate 可以跳过——导出过程读的是兼容模式下的数据写出来的是明文不需要改原库格式。三条按这个顺序来反了会出现「key 设置了但数据仍不可读」的诡异现象。3.3 导出明文库ATTACH 加 sqlcipher_export 一步到位验证能 SELECT 出内容后把它导出成明文库ATTACH DATABASE /path/to/plain.db AS plaintext KEY ; SELECT sqlcipher_export(plaintext); DETACH DATABASE plaintext;sqlcipher_export 会把当前会话里已解密的所有表、索引、触发器一起写入 plain.db。KEY 表示明文库不再加密。这一步是整条链路的后悔药plain.db 生成后原加密库只作为证据留存后续所有读取都在明文库上做速度也快得多。导出完直接用 sqlite3 验证.headers on .mode column SELECT count(*) FROM message; SELECT createTime, isSend, type, substr(content,1,40) FROM message LIMIT 5;count 是第一步校验和微信里聊天记录总数对一下数量级。substr 是为了避免 content 里埋了换行把终端刷屏。提示如果 ATTACH 之后 SELECT sqlcipher_export 返回空结果先检查是不是在设置 key 之前就执行了 ATTACH。导出会话里 PRAGMA 顺序同样严格。3.4 字段识别先把表结构打印出来再写脚本我不建议直接抄网上的 SELECT 语句先执行SELECT name, sql FROM sqlite_master WHERE typetable AND name IN (message,rcontact,chatroom);把建表语句打出来确认 type、isSend、talker、createTime 这些列都在再写查询。实际遇到过某版本把 content 挪到别的表或把 createTime 改名的情况硬编码列名会直接失败。这一步花五分钟能省后面调试脚本的半小时。确认结构没问题之后明文库就可以交给 Python 处理了。4. 用 Python 复刻 chatlog 核心逻辑密钥推导、消息解析与 HTML 导出4.1 先推导密钥IMEI 和 UIN 拼起来算 md5命令行方式跑通后把整个流程脚本化。第一步是把密钥派生写成函数import hashlib def derive_key(imei: str, uin: str) - str: 微信 Android 旧版数据库口令md5(imeiuin) 前 7 位十六进制 raw hashlib.md5(f{imei}{uin}.encode(utf-8)).hexdigest() return raw[:7] if __name__ __main__: # 调试时用真实值替换先打印确认再连库 print(derive_key(862912031234567, 12345678))逻辑不复杂但有两个容易错的地方。一是拼接顺序必须是 imei 在前 uin 在后反了推导结果完全不同二是返回的是字符串而不是字节数组它会被当作 SQLCipher 的口令去做 PBKDF2不是直接用做 AES key。调试时先用一组已知的 imei 和 uin 算出来核对别等到连接数据库才发现问题。4.2 读库两条路线直连加密库还是先生成明文库写脚本时面临一个选择直接用 pysqlcipher3 连加密库还是像第 3 章那样先用 CLI 生成 plain.db 再用标准 sqlite3 读。两条路线我都用过结论很明确优先走「CLI 生成明文库 标准 sqlite3」。pysqlcipher3 的安装是血泪经验它在 Python 3.11 以上的环境里基本编译失败依赖老版本 OpenSSL 头文件和 SQLCipher amalgamation 源码补丁常常跟不上。就算装好了Python 侧还要手动指定 cipher_compatibility 等 PRAGMA报错信息比 CLI 还难懂。明文库方案里 Python 只做数据解析不依赖任何加密库换台机器也能跑。如果你确实想直连代码长这样from pysqlcipher3 import dbapi2 as sqlite3 conn sqlite3.connect(EnMicroMsg.db) conn.execute(fPRAGMA key{key}) # key 只含 hex 字符 conn.execute(PRAGMA cipher_compatibility 3)注意 f-string 拼接 PRAGMA 口令时key 要保证只含 hex 字符这是工具脚本口令来源受控可以不考虑注入。但要明白这条路径的脆弱点Python 环境一旦变动编译问题会先于业务逻辑出现所以生产归档我还是推荐先转明文库。4.3 读取消息并做基础清洗明文库到手后解析逻辑与普通 SQLite 没有区别。我用 sqlite3.Row 让行对象支持按列名访问import sqlite3 def load_messages(db_path: str, talker: str | None None) - list: 读取 message 表可按会话对象过滤 conn sqlite3.connect(db_path) conn.row_factory sqlite3.Row cur conn.cursor() sql SELECT createTime, isSend, type, content FROM message params () if talker: sql WHERE talker ? params (talker,) sql ORDER BY createTime return cur.execute(sql, params).fetchall()WHERE 条件用 ? 占位符是必须养成的习惯talker 来自外部参数时拼字符串会让导出脚本引入注入风险。ORDER BY createTime 保证聊天顺序不是每个版本都会按时间排序返回。类型映射留到导出阶段做这里先保证数据裸读出来。4.4 导出成可读 HTML类型映射与时间戳清理消息类型是数字编号导出时要转成人话type含义1文本3图片34语音43视频47表情49链接/小程序10000系统提示时间戳的处理是另一处陷阱别直接格式化import html import time TYPE_NAMES {1: 文本, 3: 图片, 34: 语音, 43: 视频, 47: 表情, 49: 链接, 10000: 系统} def to_html(rows: list, out_path: str chatlog.html) - None: 把 message 行记录导出成浏览器可读的 HTML with open(out_path, w, encodingutf-8) as f: f.write(htmlheadmeta charsetutf-8/headbody) for r in rows: ts r[createTime] if ts 10**12: ts // 1000 # 毫秒时间戳转秒 t time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(ts)) who 我 if r[isSend] 1 else 对方 kind TYPE_NAMES.get(r[type], str(r[type])) text html.escape(str(r[content])).replace(\n, br) f.write(fpb{t} {who}/b [{kind}] {text}/p) f.write(/body/html)三处不能省ts 大于 10 的 12 次方时先除 1000否则时间会变成 1970 年html.escape 必须做聊天内容里可能出现script这类字面内容不转义生成的 HTML 打开时会被当成页面代码执行写入编码固定 utf-8缺少 charset 声明会乱码。这样导出的文件双击浏览器就能看算是一个 chatlog 式工具的最小可用版本。图片等附件要导出的话按 imgPath 字段在 /sdcard/tencent/MicroMsg/ 下找原文件message 表里不存二进制。5. 复现 chatlog 最容易翻车的 5 个点排查顺序与解决办法5.1 现象PRAGMA key 后报 file is not a database原因密钥不对或者这个库根本不是预期的微信库。SQLCipher 在口令错误时不会给「密码错误」而是把文件当作损坏或非数据库处理。最常见的是双卡手机 IMEI 拿错、uin 拿成另一个账号的值。解决双卡手机把两个 IMEI 都试一遍uin 重新从 shared_prefs 和 auth_info_key_prefs.xml 两个文件核对确认这个 db 是当前登录账号产生的不是换号前的遗留文件。再用一组已知输入跑 md5确认拼接顺序没写反。5.2 现象PRAGMA key 不报错但 SELECT 内容为空原因库按错误的兼容参数解析出来了表结构在但数据页读不出常见于用 4.x 的 sqlcipher 打开 3.x 老库。解决回到 3.2 的顺序先执行 PRAGMA cipher_compatibility 3; 再重查。还不行就试 PRAGMA kdf_iter 4000;极老版本微信用的迭代次数比默认小。改完参数后重新 ATTACH 导出 plain.db别在原文件上反复操作。5.3 现象导出的时间全是 1970-01-01原因createTime 在部分版本里是毫秒时间戳。time.localtime 接收的是秒喂进去十三位数直接溢出或回绕到 1970。解决统一在格式化前判断ts 大于 10 的 12 次方就除以 1000。这个判断对秒级和毫秒级都兼容是导出脚本里最稳的写法。别用 len(str(ts)) 判断位数负数和边界值容易出问题。5.4 现象群聊记录只有 wxid看不到昵称原因群聊里 message.talker 存的是群成员的 wxidrcontact 表没有这群人的条目群内昵称存在 chatroom.memberlist 里。解决把 chatroom 表读出来memberlist 是字符串形式的 JSON 数组解析后建立 wxid 到群昵称的映射再回填到导出结果。群聊里「我/对方」的判断仍可用 isSend 字段但展示名要用映射后的昵称否则整份群记录读起来像乱码。5.5 现象解密成功但导出的消息数量比微信里看到的少原因新版微信可能把部分消息拆到其他表或本地消息已被清理策略删除。message 表只保留本地现存的部分本地没有的任何导出工具都拿不回来。解决先 SELECT name FROM sqlite_master看有没有 msg_ 前缀或历史记录相关的分表有就合并。没有的话接受一个事实导出工具不是备份工具能导出的最大范围就是本机数据库里现存内容。想长期留存得在数据还在时定期导。6. 给导出结果加一道校验三条手段与一个归档习惯6.1 数量校验与时间连续性检查导出后先做两条快速校验。数量上SELECT count(*) FROM message 与导出的行数必须一致HTML 里换行符被替换过不能靠数p标签直接对比行数。时间连续性上把 createTime 排序后扫一遍相邻间隔出现跨月或跨年的跳跃说明中间可能有删除或漏了分表数据。这两条校验加起来不到一分钟能拦住大多数低级错误。6.2 给明文库加一个哈希存档解密出来的 plain.db 是整批归档的依据给它算 SHA-256 存成同目录文件sha256sum plain.db plain.db.sha256之后每次引用这份导出先 sha256sum -c 校验再操作。这个习惯防止「解密一次、之后又在半解密状态的文件上做分析」这类事故也让你能确认归档链路没有被中途替换。旁边再写一个 txt 记录导出时间、微信版本、imei 前六位和 uin下次重导时能快速判断环境是否变化。6.3 归档习惯明文库只在本地留一份明文库包含完整聊天内容比聊天记录 app 里现有的更全。我自己的习惯是换机或清理手机前先导一轮导出文件放本地磁盘专用目录不放进任何网盘同步路径分析完的临时明文库及时删掉只留 sha256 校验文件和 HTML 导出。毕竟能解开这份数据的人不多但文件一旦落到不该去的地方问题性质就完全变了。这套链路我重建过不止一次每次换手机或微信版本升级都要重新确认密钥派生和兼容参数。别迷信网上存着的旧脚本跑一遍 2.4 的派生公式、3.2 的 PRAGMA 顺序半小时就能验证环境是否还成立。希望帮到你。本文还有配套的精品资源点击获取