开启左侧

企业微信聊天记录导出全解析

[复制链接]
在线会员 KxS1X 发表于 前天 09:14 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题

从减稀 SQLite 到可读消息:稀钥派死、多库联查、消息复原
原文鉴于宁静钻研需要截至阐发,后颠末AI收拾整顿成文, 只会商大致手艺道理,没有会商所有细节,没有涉及所有不法用处。

1、为何企微数据库挨没有启?


把企业微疑的 .db文献拖退 DB Browser for SQLite,劈面即是一句报错:
file is not a database (or key is incorrect)
那没有是文献破坏,而是企微正在用 wxSQLite3—— 一个正在 SQLite 之上减了一层 AES-128-CBC 齐页减稀的定造版原。每个 4096 字节的数据库页皆被自力减稀,稀钥借戴着页码到场运算。

原文将复原那套减稀体制、示范怎样用 ATTACH DATABASE 跨库联查,并分离理论代码剖析消息导出管讲。

2、数据躲正在那里


企微 PC 版把统统当地数据塞退一个以 uid 定名的目次,uid 是一串 16 位数字(如 1688851234567890):
C:\Users\{您的用户名}\Documents\WXWork\
└── 1688851234567890\               ← 您的账号目次
    ├── Data\                       ← 主数据库目次
    │   ├── message.db              ← 谈天消息(最主要)
    │   ├── session.db              ← 会话列表
    │   ├── user.db                 ← 联系人根本疑息
    │   ├── user_extend.db          ← 联系人扩大疑息
    │   ├── avatar_store_v3.db      ← 头像慢存
    │   ├── kv.db                   ← 键值设置
    │   └── ...(同约 18 个 .db)
    └── CacheMapping\
        └── *.db                    ← 媒介文献路子映照主要数据库一览

文献名中心表存甚么
message.dbmessage_table消息正文、收收时间、收收者
session.dbConversationInfo会话列表、已读数、最新消息
user.db联系人相干企业成员、内部联系人
CacheMapping/*.dbmapping语音/图片/望频当地路子
group_meeting.db集会相干望频集会记载
crm.dbCRM 相干客户联系记载

3、减稀道理:逐页 AES-128-CBC


wxSQLite3 的减稀粒度是数据库页(企微默认 4096 字节/页),每页皆用一个差别的稀钥+IV 自力减稀。

🔐 每页的减稀历程(以第 N 页为例)RawKey (16 字节)MD5(RawKey + pgno + 魔数)AES Key (16字节)页码 pgno4次 LCG 运算MD5(LCG成果)IV (16字节)AES-128-CBC 解稀稀文页数据明文页数据 ✓

枢纽设想:
    • AES Key = MD5(RawKey || pgno || 0x546c4173)— 每一页 Key 差别• IV = MD5(LCG(LCG(LCG(LCG(pgno+1)))))— 鉴于页码的真随机 IV• 第 1 页前 16 字节是 SQLite 邪术头,解稀前需特别处置

4、稀钥从哪去:wxSQLite3 KDF


企微传进的本初暗码(从内乱存注进获得,16 字节)其实不能间接用做 AES Key,需要颠末一套自研的 KDF(稀钥派死函数):

本初暗码 (16字节)暗码添补到 32字节(不敷部门用 Salt 补齐)软编码 Salt(32字节牢固值)MD5(Salt) × 51次迭代BaseHash (16字节)20轮 RC4混杂 每一轮 Key = k XOR BaseHash减稀后暗码 (32字节)拼交: 本暗码 + 减稀暗码= 64字节MD5(64字节) × 51次迭代✅ RawKey (16字节)

那套 KDF 的特性:
    • 51 次 MD5 迭代(1+50)增加暴力破解易度• RC4混杂 引进非线性变更• 终极产品才是真实用于 AES 减稀的稀钥

5、解锁数据库:二种方法


拿到 RawKey 以后,有二种解锁路子:
方法 A:文献级解稀(导出明文 db)

// 逐页读与 → decrypt_page() → 写出明文文献
decrypt_sqlite_db("message.db", "message_plain.db", raw_key)?;
// 以后用所有 SQLite 东西翻开 message_plain.db方法 B:运行时解锁(举荐,无需降盘)


颠末 SQLCipher 的 PRAGMA 正在翻开时立即解稀:
-- 指定企微专用的减稀计划
PRAGMA cipher = 'wecom';
PRAGMA cipher_use_hmac = off;
PRAGMA cipher_page_size = 1024;
PRAGMA kdf_iter = 4000;
PRAGMA hexkey = 'de03c09a813509f3dfb511155260e82f';
-- 以后间接盘问,SQLCipher通明 解稀
SELECT * FROM message_table LIMIT 5;
方法 B 的益处是没有发生临时明文文献,宁静性更下。

6、跨库盘问的困难


message.db 里的消息很残破,但是碰到媒介消息便卡住了:

语音消息的正文 content字段只存了一个 proto 序列化的 key 路子,
理论的 .silk音频文献路子存留 CacheMapping/*.db的 mapping内外。

需要查 mapping

拼交路子
message_tablecontent = proto(key='xxx')mapping 表key='xxx' → file_nameCache/Voice/xxx.silk

二个数据库,一个盘问— 那恰是 ATTACH DATABASE的用武之天。

7、ATTACH DATABASE:一个跟尾翻开多个库


SQLite 本死撑持正在统一个跟尾上挂载多个数据库文献,挂载后能够用 alias.table跨库 JOIN:
-- 挂载 mapping 数据库,alias = "mapping"
ATTACH DATABASE '/path/to/CacheMapping/xxx.db' AS mapping;

-- 跨库联系关系盘问:消息 JOIN 文献路子
SELECT
    m.send_time,
    m.sender_id,
    mp.file_name          -- 去自 mapping 表
FROM message_table m
JOIN mapping.mapping mp
  ON mp.key = extract_key(m.content)
WHERE m.content_type = 34   -- 语音消息
ORDER BY m.send_time;

-- 用完卸载
DETACH DATABASE mapping;
关于减稀数据库,挂载后借要对于每一个 schema独自 树立 cipher 参数:
ATTACH DATABASE '/path/to/session.db' AS session;
PRAGMA session.cipher = 'wecom';
PRAGMA session.cipher_use_hmac = off;
PRAGMA session.hexkey = 'de03c09a...';
8、DbService:办理多用户多库跟尾


咱们设想了一个 DbService去分歧办理那些跟尾,中心思路:

DbServiceInnerConnEntry(每一用户一份)Arc<Mutex<Connection>>password: Vec<u8>attached: HashSet<String>last_used: InstantArc<RwLock<Inner>>base_pathdefault_dbsconnectionsHashMap<uid, ConnEntry>

枢纽设想决议计划:
设想面挑选启事
跟尾键uid (u64)每一用户一个跟尾,多库颠末 ATTACH 同享
暗码保存ConnEntry.password后绝 attach_db 无需重复传进
克隆价格Arc引用计数Clone 是 O(1) 操纵,便利正在 handler 间通报
空闲超时10 分钟后主动封闭Weak引用 保证 DbService 局部 drop 后清理任务主动参加
时序图:open → attach → query


SQLiteDbServiceAppSQLiteDbServiceApp10 分钟无举动open_user("uid", password)Connection::open("message.db")connPRAGMA hexkey + cipher保留 password 到 ConnEntryattach_db("uid", "session.db")查抄 attached 汇合,包管幂等ATTACH "session.db" AS sessionPRAGMA session.hexkey + cipherquery("uid", sql)conn.prepare(sql)rowsVec<Vec<JsonValue>>主动 drop Connection,DETACH 局部 alias

9、消息导出管讲


有了数据库跟尾以后,message_table里的消息借不克不及间接浏览 —— 年夜大都非文原消息的正文皆是 Protobuf 两退造,需要反序列化后才气获得有效的字段。
消息处置齐过程


7/14/19/101

34

43

48/49

其余

失利

失利
SELECT FROM message_tablecontent_type?图片/心情语音望频一般文献文原消息扫描 local_extra找 Windows 路子如 D:\\xxx.jpg剖析 proto与 local_path 做 key剖析 proto与 local_path 做 key剖析 proto与 local_path 做 key查 mapping 表SELECT file_nameWHERE key=?拼交 Cache 路子复造或者解码文献ParseTextMsgProto遍历 content_itemsfallback: JSON 剖析fallback: 图文混淆天生 HTML 片断写进 messages.html
消息范例比较表

content_type范例content 字段格局处置方法
1一般文原TextMsg proto剖析 content_items
7图片-扫描 local_extra 找路子
14心情-共图片
34语音LocalFileMsg protokey → mapping → .silk → WAV
43望频LocalVideoMsg protokey → mapping → .mp4
48文献LocalFileMsg protokey → mapping → 复造文献
101图文混淆WorkImageTextMsg特别 proto 剖析

10、Protobuf 两退造剖析


企微消息的 content字段是 Protobuf 序列化的两退造。以语音消息为例,其 LocalFileMsg构造只要关心 tag=1的 local_path字段:
Protobuf 编码(tag=1, wire_type=2 暗示 LEN 范例字符串):

字节:  0x0A  0x2F  "/Users/xxx/WXWork/..."
       ↑     ↑     ↑
   tag<<3|2  少度  字符串实质
   (field=1)
脚写最小 varint 解码器只要 ~30 止 Rust 代码,无需引进残破 prost 依靠:
fn read_varint(data: &[u8], pos: usize) -> Option<(u64, usize)> {
    let mut result = 0u64;
    let mut shift = 0u32;
    let mut i = pos;
    loop {
        let b = data as u64;
        result |= (b & 0x7F) << shift;
        i += 1;
        if b & 0x80 == 0 { break; }
        shift += 7;
    }
    Some((result, i - pos))
}
11、语音消息的复原:SILK → WAV


企微语音使用 SILK v3格局(微疑/企微专用的语音编解码器,鉴于 Opus SILK 层)。
SILK v3 文献构造

┌─────────────────────────────────────────────┐
│ 文献头 (10字节): "#!SILK_V3\0"               │
├─────────────────────────────────────────────┤
│ 帧 1: [int16 frame_len][frame_len 字节数据]  │
├─────────────────────────────────────────────┤
│ 帧 2: [int16 frame_len][frame_len 字节数据]  │
├─────────────────────────────────────────────┤
│ ...                                          │
├─────────────────────────────────────────────┤
│ EOF帧: frame_len & 0x8000 (洼地位1)          │
└─────────────────────────────────────────────┘
解码后写进尺度 WAV 容器(16-bit PCM, 24000Hz, mono),便可用所有播搁器播搁。

12、残破架构回忆


📄 导出输出HTML消息 记载WAV 语音文献MP4 望频文献其余附件🔍消息 剖析Protobuf 解码(脚写 varint)mapping 表盘问媒介路子剖析🔧 DbServiceopen_user(uid, pwd)解稀 +翻开 主跟尾attach_db(uid, name)挂载附带库query(uid, sql)跨库联查📂 数据源message.db(主跟尾)session.db(ATTACH)mapping.db(ATTACH)user.db(ATTACH)

十3、踏坑记载


正在完毕过程当中碰到的多少个枢纽成就:
1. ATTACH 后必需对于每一个 schema独自 树立 PRAGMA

-- ❌ 毛病:只对于主跟尾树立,attach 的库没法解稀
PRAGMA hexkey = 'abc...';
ATTACH DATABASE 'session.db' AS session;

-- ✅ 准确:attach 后立即对于 alias 树立
ATTACH DATABASE 'session.db' AS session;
PRAGMA session.hexkey = 'abc...';
PRAGMA session.cipher = 'wecom';2. alias 不克不及取 SQLite保存 名抵触

fn db_alias(db_name: &str) -> String {
    let base = db_name.trim_end_matches(".db");
    match base {
        "main" | "temp" => format!("{base}_db"),  //防止 抵触
        _ => base.to_owned(),
    }
}3. 第 1 页的特别处置


SQLite 第 1 页前 16 字节是邪术头 "SQLite format 3\0",减稀前需要保留并正在解稀后规复:
页 1 计划:
  字节 0~15:SQLite 邪术头(没有到场减稀)
  字节 16~23:页巨细等元疑息(先用 8~15 的数据交流,再减稀)
  字节 24~4095:一般减稀实质
十4、归纳

手艺面枢纽常识
wxSQLite3 减稀逐页 AES-128-CBC,Key/IV 均由页码到场派死
稀钥派死 KDFMD5 × 51 迭代 + RC4 × 20 轮混合
运行时解锁PRAGMA cipher + hexkey无需降盘明文
多库联查ATTACH DATABASE+ 每一库零丁树立 cipher PRAGMA
跟尾办理DbService: uid → 单跟尾,ATTACH 替换多跟尾
消息剖析Protobuf varint 脚动剖析,无需残破 proto 界说
语音复原SILK v3 帧解包 → PCM 解码 → WAV 启拆

企微数据库系统的精巧的地方正在于:消息保存取媒介索引别离。message.db 只存构造化消息记载,媒介文献由 mapping.db 干两级索引,理论数据集降正在 Cache 目次。ATTACH DATABASE 文雅天处置了跨库联查成就,让一条 SQL 就可以完毕"消息 → 文献路子 → 媒介文献"的残破链路盘问。

您需要登录后才可以回帖 登录 | 立即注册 qq_login

本版积分规则

发布主题
阅读排行更多+
用专业创造成效
400-778-7781
周一至周五 9:00-18:00
意见反馈:server@mailiao.group
紧急联系:181-67184787
ftqrcode

扫一扫关注我们

Powered by 职贝云数A新零售门户 X3.5© 2004-2025 职贝云数 Inc.( 蜀ICP备2024104722号 )