Go语言实现PC微信聊天记录解密导出:SQLCipher逐页解密与可读文本转换 简介这是一份面向PC端微信用户的聊天记录导出工具源码基于wails框架与React前端构建核心解决微信聊天记录难以长期留存、官方停止支持后无法回看的问题。工具界面尽量贴近微信原生风格降低上手成本理论上兼容所有Windows 32/64位微信版本并支持多开账号的选择与数据切换。功能覆盖图片、视频、语音、文件、链接、名片、定位、通话、转账、小程序、视频号、QQ音乐及第三方分享等十余种消息类型同时提供按类型、日期、群成员检索增量式导出、书签、会话分享与自动定位浏览位置等实用能力。资源包共156个文件以129个png界面素材、8个go源码文件为主另含md说明、json配置、plist与nsi打包脚本等压缩后约9.82MB结构完整便于二次开发。目前已有771人学习下载适合具备Go与前端基础的开发者研究聊天记录解析、数据库解密与桌面端打包思路。1. 一键导出PC微信聊天记录工具从加密数据库到可读文本的完整路径PC 微信的聊天记录躺在Msg目录下的.db文件里但直接双击打开只会看到一堆乱码——SQLCipher 加密过的页头前 16 字节是盐值后面全是密文。想把这些记录导出来做归档、检索或者迁移绕不开三件事定位数据库文件、拿到解密密钥、把解密后的 SQLite 数据转成可读格式。这个标题讲的就是用 Go 写一个工具把这三步串成一条命令跑完。适合两类人一是想给自己几年聊天记录做个本地备份的普通用户二是在 Go 里做桌面数据提取、需要参考 SQLCipher 解密流程的开发者。下面按我实际做过的顺序拆开讲包括密钥怎么拿、页解密为什么容易翻车、Go 里怎么组织代码。2. 先搞清楚微信在 PC 上把数据存成了什么2.1 数据库文件分布与版本差异PC 微信的数据目录默认在文档\WeChat Files\下每个登录过的账号一个以 wxid 开头的文件夹。进去之后核心目录是Msg里面按消息类型分了好几个.dbMSG0.db到MSG4.db存聊天消息MediaMSG.db存语音和图片的索引MicroMsg.db存联系人。文件大小从几十 MB 到几个 GB 不等MSG 系列会随着聊天量增长自动分片。不同微信版本目录结构有差异。3.9.x 之前Msg直接在账号目录下3.9.x 之后部分版本把多媒体文件挪到了FileStorage但数据库位置基本没变。判断版本最直接的办法是看WeChat Files下有没有All Users目录有的话说明是新版多账号结构。我一般先让工具扫描账号目录列出所有.db文件及其大小再决定处理哪些。2.2 SQLCipher 加密的页结构微信用的是 SQLCipher 的变体标准 SQLCipher 数据库第一页前 16 字节是随机盐微信在此基础上做了调整。一个未加密的 SQLite 数据库第一页开头是SQLite format 3\0加密后这 16 字节被替换成盐值后续每个页用 AES-256-CBC 加密页大小通常是 4096 字节。解密时需要密钥经过 PBKDF2-HMAC-SHA1 派生默认 64000 轮微信可能不同得到 32 字节的加密密钥再用它解密每一页。关键点在于SQLCipher 只加密页内容不改变页的偏移和大小。所以解密后的数据库和原始 SQLite 结构完全一致可以直接用标准 SQLite 驱动打开。这也是为什么工具的核心逻辑是「逐页解密再拼回一个完整文件」而不是去解析 SQL 语法。2.3 为什么不能直接改文件头绕过网上有些说法是「把前 16 字节改成 SQLite format 3 就能打开」这在早期未加密或弱加密版本可能碰巧能用但对现在微信的 SQLCipher 完全无效。原因有两个一是密钥派生需要正确的盐值改文件头等于把盐丢了二是即使第一页碰巧能读后续页仍是密文SQLite 读到第二页就会报database disk image is malformed。血泪经验是不要试图用十六进制编辑器手动改老老实实走密钥派生加逐页解密。3. 用 Go 实现解密与导出的核心步骤3.1 定位数据库与读取密钥密钥不在数据库文件里而是存在微信进程的内存中。常见做法有两种一是通过读取微信进程内存搜索密钥特征二是从WeChat Files下的config或注册表里找。前者需要管理员权限后者在新版微信里已经不可靠。我一般用进程内存扫描的方式在 Go 里通过golang.org/x/sys/windows调用ReadProcessMemory。// 在 Windows 上打开微信进程并读取内存 // 需要管理员权限运行 package main import ( fmt syscall unsafe golang.org/x/sys/windows ) func findWeChatPID() (uint32, error) { snapshot, err : windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) if err ! nil { return 0, err } defer windows.CloseHandle(snapshot) var entry windows.ProcessEntry32 entry.Size uint32(unsafe.Sizeof(entry)) for { err windows.Process32Next(snapshot, entry) if err ! nil { break } name : syscall.UTF16ToString(entry.ExeFile[:]) if name WeChat.exe { return entry.ProcessID, nil } } return 0, fmt.Errorf(WeChat.exe not found) }这段代码做的是遍历系统进程快照找到WeChat.exe的 PID。TH32CS_SNAPPROCESS表示抓取进程列表Process32Next逐个读取。拿到 PID 后用windows.OpenProcess以PROCESS_VM_READ权限打开再配合ReadProcessMemory扫描内存区域。参数上注意entry.Size必须先赋值否则Process32Next会返回错误。实际扫描时不要全内存遍历先枚举模块基址在微信自己的堆区域里搜 32 字节的高熵数据能大幅减少误报。3.2 密钥派生与逐页解密拿到 32 字节原始密钥后还需要用数据库第一页前 16 字节的盐做 PBKDF2 派生。Go 标准库没有 PBKDF2用golang.org/x/crypto/pbkdf2。派生参数HMAC-SHA1、迭代 64000 次、输出 32 字节。然后对每一页做 AES-256-CBC 解密注意第一页解密后要把前 16 字节替换回SQLite format 3\0。// 逐页解密 SQLCipher 数据库 // pageSize 通常为 4096salt 为文件前 16 字节 func decryptDB(srcPath, dstPath string, rawKey []byte) error { data, err : os.ReadFile(srcPath) if err ! nil { return err } salt : data[:16] // PBKDF2 派生 key : pbkdf2.Key(rawKey, salt, 64000, 32, sha1.New) pageSize : 4096 out : make([]byte, len(data)) for i : 0; i len(data); i pageSize { end : i pageSize if end len(data) { end len(data) } page : data[i:end] // 每页前 16 字节是 IV微信变体可能不同 iv : page[:16] cipherText : page[16:] block, _ : aes.NewCipher(key) mode : cipher.NewCBCDecrypter(block, iv) plain : make([]byte, len(cipherText)) mode.CryptBlocks(plain, cipherText) // 第一页恢复 SQLite 文件头 if i 0 { copy(plain, []byte(SQLite format 3\x00)) } copy(out[i:], plain) } return os.WriteFile(dstPath, out, 0644) }逻辑说明先读整个文件取前 16 字节作为盐。PBKDF2 用盐和原始密钥派生出真正的 AES 密钥。然后按 4096 字节一页循环每页前 16 字节当 IV剩余部分 CBC 解密。第一页解密后把开头 16 字节覆盖成 SQLite 标准头这样 SQLite 驱动才能识别。参数上pageSize如果遇到非 4096 的库需要从第一页解密后的头里读但微信基本都是 4096。迭代次数 64000 是 SQLCipher 默认微信如果改过需要从内存里同时抓取迭代次数否则派生出的密钥不对解密出来还是乱码。3.3 导出为可读格式解密后的.db就是标准 SQLite用database/sql加mattn/go-sqlite3驱动打开。MSG 表结构里关键字段StrTalker是对方 wxidStrContent是消息内容CreateTime是 Unix 时间戳Type是消息类型1 文本、3 图片、34 语音等。导出时按StrTalker分组按时间排序输出成 JSON 或 CSV。// 查询并导出消息为 JSON func exportMessages(dbPath, outPath string) error { db, err : sql.Open(sqlite3, dbPath) if err ! nil { return err } defer db.Close() rows, err : db.Query(SELECT StrTalker, StrContent, CreateTime, Type FROM MSG ORDER BY CreateTime) if err ! nil { return err } defer rows.Close() var msgs []map[string]interface{} for rows.Next() { var talker, content string var createTime int64 var msgType int rows.Scan(talker, content, createTime, msgType) msgs append(msgs, map[string]interface{}{ talker: talker, content: content, time: createTime, type: msgType, }) } data, _ : json.MarshalIndent(msgs, , ) return os.WriteFile(outPath, data, 0644) }这段代码把 MSG 表全量读出每行映射成一个 map最后序列化成 JSON。StrContent对非文本消息可能是 XML 或二进制导出时可以根据Type做过滤或转义。参数上ORDER BY CreateTime保证时间顺序如果数据量大可以加分页避免内存爆掉。实际用的时候我一般再加一个按StrTalker过滤的参数只导某个联系人的记录速度会快很多。4. 避坑与常见问题排查4.1 解密后 SQLite 报 malformed现象解密脚本跑完没报错但用 SQLite 打开提示database disk image is malformed。原因通常是密钥派生参数不对或者页大小不是 4096。解决先用十六进制查看解密后文件的前 16 字节确认是不是SQLite format 3\0。如果不是说明第一页解密失败回头检查 PBKDF2 迭代次数和盐值。如果头对了但后续页报错检查页大小从第一页偏移 16 字节处读两个字节得到页大小大端序再按实际页大小重新解密。4.2 内存扫描找不到密钥现象遍历微信进程内存后没有匹配到 32 字节密钥。原因可能是微信版本更新后密钥存储方式变了或者扫描范围不对。解决不要全内存盲扫先枚举微信进程的私有堆区域在这些区域里搜高熵数据。另外注意微信可能同时有多个进程WeChat.exe和WeChatAppEx.exe都要看。如果还是找不到可以尝试在微信刚登录、还没打开聊天窗口时扫描这时候密钥在内存中的特征更明显。4.3 导出中文乱码现象导出的 JSON 里中文显示为\uXXXX或者乱码。原因StrContent本身是 UTF-8但 JSON 序列化时默认转义非 ASCII。解决用json.Encoder并设置SetEscapeHTML(false)或者直接输出 CSV 用 UTF-8 BOM。如果是乱码而不是转义检查数据库连接有没有指定编码SQLite 默认 UTF-8一般不需要额外设置。4.4 大文件导出内存溢出现象几个 GB 的 MSG 库一次性读进内存导致程序崩溃。原因db.Query返回所有行msgs切片无限增长。解决改用流式写入每读一行就写一行到文件不要攒在内存里。或者按CreateTime分页每次查一个时间段。我一般用rows.Next()循环里直接写 JSON Lines每行一个 JSON 对象既省内存又方便后续处理。4.5 杀毒软件误报现象编译出的 Go 二进制被 Windows Defender 或某管家直接删掉。原因程序调用了ReadProcessMemory行为特征像恶意软件。解决加白名单或者用代码签名。如果只是自己用编译时加-ldflags -s -w减小体积有时能降低误报率。更稳妥的办法是把内存读取部分拆成单独的小工具主程序只做解密和导出减少敏感 API 调用。5. 进阶把导出结果做成可检索的本地归档导出成 JSON 只是第一步真正有用的是能搜。我一般会在导出后追加一步把 JSON 灌进一个本地 SQLite 或者 Bleve 索引支持按关键词、联系人、时间段检索。Bleve 是 Go 里的全文索引库几行代码就能建索引。// 用 Bleve 给导出的消息建全文索引 import github.com/blevesearch/bleve/v2 func buildIndex(msgs []Message, indexPath string) error { mapping : bleve.NewIndexMapping() index, err : bleve.New(indexPath, mapping) if err ! nil { return err } defer index.Close() for i, m : range msgs { docID : fmt.Sprintf(msg-%d, i) if err : index.Index(docID, m); err ! nil { return err } } return nil }建完索引后用bleve.NewQueryStringQuery(关键词)就能搜。参数上IndexMapping可以自定义分词器中文需要装gojieba之类的分词插件否则默认按字切分效果一般。验证方法很简单索引建好后搜一个你确定存在的词看能不能命中。如果搜不到检查字段有没有设成Index和Store。一个具体技巧导出时把StrTalker映射成联系人备注名。微信数据库里存的是 wxid直接看是一串乱码。可以在MicroMsg.db的Contact表里查UserName对应的Remark或NickName导出时替换掉。这样导出的 JSON 里直接是「张三」「李四」可读性提升一个档次。我踩过的坑是有些联系人没有备注Remark为空这时候要回退到NickName再为空就用 wxid 兜底。最后说个习惯每次微信更新后先拿一个小号或者旧账号的数据库跑一遍解密流程确认密钥派生参数没变再处理主账号。微信改加密参数不是第一次了留个后悔药比事后抓瞎强。希望帮到你。本文还有配套的精品资源点击获取