* feat: 解析合并转发的聊天记录消息(appmsg type=19)+ 新增文件路径查找工具
mcp_server.py:
- _format_app_message_text 增加 app_type=19 分支,解析 <recorditem> 内嵌
XML,把"[链接/文件] xxx的聊天记录"展开成多行 datalist 内容(含发送者/
时间/数据类型)。覆盖 datatype 1/2/3/4/5/6/7/8/17/19/22/23/29/36/37
共 14 种类型;超过 50 条自动截断;空 datalist fallback 到"(待加载)"
- 新增 decode_file_message 工具:从 type=49+sub=6 消息找本地副本路径
(~/Library/.../msg/file/{YYYY-MM}/原文件名.{ext}),返回精确路径
+ size 二次确认,处理同名 (1)(2) 后缀
- 新增 decode_record_item 工具:从 type=49+sub=19 合并记录的第 N 个
dataitem 找本地副本(msg/attach/{table_hash}/*/Rec/*/F/{idx}/{name}),
未下载时给精确"在 wechat 点击哪一项"指引
回归测试:在数千条真实合并转发消息上 ~83% 完美展开 datalist 内容,
剩余的 content 缺失/消息被撤回情况下行为与改前一致(fallback 到原 [链接/文件])。
* fix: hoist subdir_map in decode_record_item to avoid UnboundLocalError
When the chat's attach directory does not exist (no merged-record
attachment ever downloaded for that chat), the if-block defining
`subdir_map` was skipped, so the not-found branch's reference to
`subdir_map.get(datatype, '?')` raised UnboundLocalError instead of
returning the intended guidance message.
Hoist the dict definition above the if-block so both branches can
safely reference it.
Caught by Codex review on PR #65.
* fix: address Codex P2 — large recorditem XML + glob escape
Two issues caught by Codex review on PR #65:
P2-1: _parse_xml_root rejects payloads >20KB, which silently dropped
~330 large merged-record cards (max observed 418KB with 99 dataitems)
back to "[链接/文件]" fallback. Add a dedicated _parse_record_xml with
a 500KB limit for embedded recorditem XML; routes both call sites in
_format_record_message_text and decode_record_item to it. Boosts
overall parse coverage from 83% to ~87% on real-world data.
P2-2: decode_record_item passed datatitle directly to glob, so file
names containing [ ] * ? would be treated as glob patterns rather
than literals — leading to wrong candidates or missed real files. Wrap
datatitle with glob.escape() before the exact-match query.
Full unit-test suite (35 tests) still passes.
* perf+style: speed up decode_file_message + minor consistency fixes
Self-review findings on top of the Codex P1+P2 fixes:
- perf: decode_file_message previously os.walk-ed `msg/file/` and
`msg/attach/` from scratch on every call, scanning ~185k files /
17GB on a real-world install (~6.3s per call). Now first reads
`create_time` from the message and globs only the matching
`msg/file/{YYYY-MM}/` (plus ±1 month for cross-month edge cases),
with the original walk preserved as a fallback. Measured speed-up
~10x on first call, ~750x on warm cache.
- decode_record_item: extend `type_label` to cover datatype 23
(视频号直播) and 36 (小程序/H5) so the not-found message matches
the labels emitted by _format_record_dataitem instead of falling
back to a raw `datatype=23` string.
- decode_record_item: replace unused `sub_type_packed` with `_` to
silence the "name assigned but unused" smell.
- _parse_record_xml: comment now states the empirically observed
~418KB upper bound (was "~50KB"), making the 500KB ceiling
obviously sufficient.
All 35 existing tests still pass.
* fix: address Codex round-3 P2 — multi-shard lookup + size-validate month scan
Two more issues caught by Codex review on PR #65 that I missed during
self-review:
P2-3 (multi-shard local_id lookup): both `decode_file_message` and
`decode_record_item` were resolving a single message-table via the
singular `_find_msg_table_for_user`, but a chat's messages can span
multiple message_N.db shards (search_messages and history-iteration
already use `_find_msg_tables_for_user`). When the requested local_id
lived in a different shard the tools incorrectly returned "找不到
local_id" or — if IDs collide across shards — picked the wrong row.
Now both tools iterate all shards and stop at the first hit; the
not-found message reports how many shards were scanned.
P2-4 (size validation in month-scan fast path): the perf-fix in the
prior commit collected `msg/file/{YYYY-MM}/` matches without verifying
size, so when a same-named-but-different-size copy existed in the
target month the candidate list was non-empty, the walk-fallback was
skipped, and the later `size_match` filter could end up empty —
returning a wrong-size file. Now the month-scan filters by `totallen`
upfront when known, so unmatched candidates don't poison the fallback.
Same one-shot size validation applied to `decode_record_item`'s
exact-name glob branch for symmetry.
These were both "I should have caught" issues — Codex did the
cross-tool consistency check (singular vs plural shard helper) that I
skipped, and stress-tested an edge case (month-scan finds same-name
wrong-size) that I didn't think through when writing the perf fix.
35/35 existing tests still pass. Real-data smoke: decode_file_message
0.96s end-to-end (multi-shard scan + size validation),
decode_record_item 0.03s.
* refactor: reuse _parse_message_content helper for group prefix stripping
Self-review found that decode_file_message and decode_record_item
hand-rolled their own heuristic for stripping group-chat sender
prefixes ("wxid_xxx:\n<xml...>") via a string-startswith check, while
the rest of the project already uses the canonical
`_parse_message_content(content, local_type, is_group)` helper for
exactly this purpose.
Wired both tools to that helper, deriving is_group from the username
suffix `@chatroom`. Existing edge cases (private chat content with
literal "<...>", group content with "wxid_xxx:\n", etc.) still pass.
35/35 tests still pass; 6/6 edge-case smokes still pass.
* fix: address Codex adversarial-review high+medium findings
Adversarial review caught four issues that the surface-level passes
missed. All four are now fixed end-to-end (validated against real
data, not just helper-level smoke):
[high] Large recorditem outer XML actually parsed:
Previous P2 fix added _parse_record_xml(500KB) for the inner CDATA
but the outer appmsg was still gated by _parse_xml_root(20KB), so
any merged-record card whose outer XML exceeded 20KB silently fell
back to "[链接/文件]" and never reached the inner expansion. Now
_parse_xml_root accepts a max_len kwarg, _format_app_message_text
retries with _RECORD_XML_PARSE_MAX_LEN when the default cap rejects
the outer XML, and _format_record_message_text passes the wider cap
for inner parses. Real-data check: a 34KB outer / 67-dataitem card
now expands fully via the get_chat_history → _format_message_text
→ _format_app_message_text → _format_record_message_text chain.
[high] Multi-shard local_id ambiguity:
decode_file_message and decode_record_item previously broke on the
first shard match. Empirically confirmed local_id 171 in the test
account exists in TWO shards as TWO different messages (one type=1
text, one type=6 file at different create_times). Now both tools
scan all shards, fail with an explicit ambiguity error when more
than one row matches, and accept an optional create_time arg from
the user to disambiguate uniquely.
[medium] History output now exposes (local_id, ts) for file and
record cards, and record dataitem rows are prefixed with their
0-based [item_index]. Without these, callers had no way to feed
decode_file_message / decode_record_item a stable identifier.
[medium] decode_file_message now requires appmsg type=6 and an
appattach node, refusing to search the local cache by title/size for
unrelated app messages (links, miniapps, record cards) that happen
to share a title with a real file.
35/35 existing tests still pass. Real-data smokes:
- 34KB outer XML / 67 dataitems expanded end-to-end
- multi-shard ambiguity correctly raised + resolved by ts kwarg
- history output now contains "(local_id=N, ts=T)" suffixes and
"[N]" dataitem prefixes
* fix: address Codex adversarial round-2 high findings
Round-2 adversarial review caught two issues my self-review missed
again. Both are now fixed end-to-end:
[high] decode_record_item also rejects large outer XML (mcp_server.py:2124-2126)
Round-1 high #1 was fixed by adding a wider-limit retry inside
_format_app_message_text, but decode_record_item itself still
parsed the outer appmsg with `_parse_xml_root(xml_text)` at the
default 20KB cap. Same root cause: I patched one caller, missed
the other — exactly the kind of cross-tool inconsistency that
cost two rounds already.
Extracted a shared `_parse_app_message_outer(content)` helper that
encapsulates the "try default cap, fall back to wider limit when
default rejects" pattern. Now used by all three call sites:
_format_app_message_text, decode_file_message, decode_record_item.
Real-data check: a 34KB outer (67 dataitems) parses through every
caller path, not just history rendering.
[high] Record attachment lookup silently picks wrong cached file
Previous lookup had three fallback tiers (filename+size → size only
→ cross-subdir size only) and on multiple matches sorted by mtime
and took newest. Two failure modes:
1. Different forwarded-record cards in the same chat may produce
paths with identical (filename, item_index, datasize), and the
mtime tiebreak lets the tool return another record's file while
reporting "找到本地文件: ✅".
2. Cross-subdir size-only fallback can match files belonging to
unrelated dataitem types entirely.
Now fail-closed:
- Strict filename + size match only when datatitle is known.
- Size-only fallback now ONLY when datatitle is missing
(e.g. datatype=2 thumbnails) AND scoped to the same sub-dir +
item_index — no more cross-Rec leakage.
- Removed the cross-subdir terminal fallback entirely.
- Multiple candidates after strict matching → ambiguity error
listing all candidates with mtime, no silent pick.
35/35 existing tests still pass. Real-data smokes:
- 大 outer 34KB 卡片 _parse_app_message_outer 解析成功
- decode_record_item(local_id, ts) 正确命中 Lec 4 PDF
- 多分片冲突 + 不传 ts → 报歧义错误并提示加 create_time
- 未下载 dataitem → 精确指引"在 wechat 点第 N 项"
* fix: address Codex round-3 adversarial high+medium findings
Round-3 caught two more cross-tool inconsistency issues, both in the
same family I keep missing (修一处忘另一处):
[high] decode_file_message also needs to fail-closed on ambiguity
Round-2 high #2 forced decode_record_item to fail-closed when
multiple cached candidates remain after strict matching, but I
forgot to apply the same change to decode_file_message — it still
silently sorted by mtime and returned candidates[0]. Same root
cause as round-1 high #1: Codex catches what I miss when the same
pattern needs fixing in two places.
Now decode_file_message: strict size filter when totallen is known,
and ambiguity error (not mtime sort) when more than one candidate
remains. Behavioral change: previously returned 逻辑审计论文(1).pdf
on a real test case; now reports both candidates and asks user to
disambiguate. UX regression but safety-correct.
[medium] decode_record_item rejects non-downloadable datatypes
upfront. Previously, dataitems with unknown datatype fell through
to a wildcard `sub='*'` glob over all attach subdirs (F/Img/V/A),
which could match unrelated files for links/locations/cards/
miniapps/nested-record dataitems that have only metadata, no
binary payload. Now reject non-{2,4,5,8} datatypes with a clear
"no local binary, look at history output instead" message before
any filesystem lookup.
35/35 existing tests still pass.
* fix: address Codex adversarial round-4 high findings
Round-4 found three security/correctness issues. All addressed:
[high] Path traversal via untrusted XML titles
title (decode_file_message) and datatitle (decode_record_item) come
from message XML — attacker-controlled in the "malicious chat
partner" threat model. glob.escape does NOT strip path separators
or normalize absolute paths, so e.g. title="/etc/passwd" makes
os.path.join(month_dir, "/etc/passwd") == "/etc/passwd" (POSIX
rule: join drops left when right is absolute), and glob then walks
outside msg/file. If size also matches, the tool returns an
arbitrary system path as a "found wechat file".
Added _safe_basename(name) helper with strict-reject semantics
(per Codex: reject, don't normalize) — any name containing path
separators, .. components, NUL, or absolute-path prefix is
rejected outright. Both decoders sanitize their XML-derived names
before any filesystem operation. Added _path_under_root realpath
check after candidate selection as a second-line defense against
symlink escapes.
[high] decode_file_message and decode_record_item can return cached
files belonging to a DIFFERENT message even when len(candidates)==1
Both tools rely on (filename + size + optional item_index)
heuristic matching against the cache — they have no way to derive
a record-bound or message-bound path from wechat metadata, so
exactly one matching cached file from an unrelated message looks
identical to a correct hit. This is a design limitation: wechat
does not expose record_hash or attach-uuid in the message XML in
any form derivable from outside the client.
Acknowledged in tool output with an explicit ⚠️ "this path is
heuristic, please verify mtime/context/content" warning attached
to every "found local file" response. The match itself is still
the same heuristic — closing this fully would require either
removing the tools or reverse-engineering wechat's path hashing.
Documented the limitation in the warning so callers can manually
verify before trusting downstream Read/PDF results.
35/35 tests still pass; 12/12 path-sanitize edge cases pass.
* fix: address Codex round-5 adversarial findings + md5-strong binding
Codex round 5 caught two more high issues plus a perf/correctness
concern. All real and addressed:
[high] decode_file_message scanned msg/attach in fallback, picking
up unrelated forwarded-record cached files. Outer files only ever
live in msg/file/{YYYY-MM}/; restricted the slow-path walk to that
subtree only. msg/attach holds merged-card and image attachments
whose presence here is a different message's payload, not ours.
[high] **真正根治** record/file 路径绑定问题:用 md5 强校验
Both decode_file_message (`<md5>` in appmsg) and decode_record_item
(`<fullmd5>` in dataitem) now extract the WeChat-supplied md5 and
hash candidate files locally to compare. If md5 doesn't match, the
tool fails closed with an explicit md5-mismatch error rather than
returning a path. The candidate that *does* match is uniquely
bound to the selected message — md5 collisions of distinct files
are cryptographically negligible. This fixes the heuristic-only
warning paths from rounds 3-4 with cryptographic evidence rather
than just user-facing notes.
As a side benefit, md5 dedup also lets decode_file_message return
a result when WeChat creates "(1)/(2)" copies of the same file:
same-md5 candidates are真同一文件副本 (user re-sent or auto-rename),
any one of them is correct.
When XML doesn't ship md5 (rare but possible), behavior reverts to
the previous fail-closed-on-multiple-candidates path with an
explicit "no md5 available, treating as heuristic" note.
[medium] _parse_app_message_outer was retrying every appmsg under
the 500K cap on initial 20K rejection, which made history rendering
O(content_size) on big non-record appmsgs. Added a substring
`<type>19</type>` short-circuit so only true type=19 records pay
the wider parser cost. Verified non-type=19 big XML now returns
None in <0.01ms instead of doing a 500K parse.
35/35 existing tests still pass. Real-data smokes:
- decode_record_item 142,1 → "✅ md5 校验通过,路径与 dataitem 唯一绑定"
- decode_file_message 171 (with same-name (1).pdf copy in cache) →
md5 dedup recognizes both as same content, returns one with
"✅ md5 校验通过"
- non-type=19 big appmsg parses in <1ms (substring short-circuit)
* fix: address Codex round-6 adversarial findings — strict md5 binding + chunked hash
[high] decode_file_message / decode_record_item now fail-closed when
the message XML has no md5/fullmd5 field — instead of returning a
heuristic single-candidate path with a warning. The previous
warning-only approach (rounds 4-5) didn't actually stop downstream
Read/PDF callers from using the wrong path. Now: no md5 = no path
returned, period. The error message lists the heuristic candidates
with mtime so the user can manually pick if absolutely needed,
but the tool itself does not commit to any of them.
Behavioral consequence: messages where wechat omits md5 (rare but
possible — e.g. some image/voice dataitems lack fullmd5) become
not-resolvable via these tools. Acceptable safety/utility tradeoff
per Codex's recommendation.
[medium] md5 verification was reading the entire candidate file into
memory via `_hashlib.md5(_f.read()).hexdigest()`. For 100MB+
attachments (videos in merged-record cards, large PDFs) this could
spike RSS or stall the MCP process. Replaced with a streaming
helper `_md5_file_chunked` (64KB chunks) plus a 500MB hard cap that
returns an explicit error rather than attempting verification on
oversized files.
35/35 existing tests still pass. Real-data smokes:
- decode_file_message 171 (with md5) → "✅ md5 校验通过"
- decode_record_item 142,1 (with fullmd5) → "✅ md5 校验通过"
- _md5_file_chunked size cap 1KB rejection works correctly
* fix: round-7 + revert round-6 over-strict — match real threat model
Two real bugs from Codex round-7 plus a partial revert of round-6
over-strictness that doesn't match this tool's actual threat model.
[high] Group type=19 with 'sender:<?xml...' (no newline) prefix not
stripped (Codex round-7 high #1)
_parse_message_content only split on ':\n', missing real-world
group rows where wechat writes 'wxid_xxx:<?xml ...' or
'wxid_xxx:<msg ...' inline. _format_app_message_text and
decode_record_item both received the prefixed content, parsed it
as raw XML, and failed. Now also strips on regex match against
'<?xml|<msg|<msglist|<voipmsg|<sysmsg' immediately after a sender
token. Verified with 5 prefix shape variants; legacy ':\n' still
works.
[medium] Record images use flat 'Img/0_t' filenames, not 'Img/0/*'
(Codex round-7 medium #2)
decode_record_item's datatype=2 (image) branch globbed for
'*/Rec/*/Img/{idx}/*' but real wechat caches store record images
as flat files: '*/Rec/<id>/Img/0_t', '*/Rec/<id>/Img/0', or
'*/Rec/<id>/Img/0.{ext}'. Added flat-pattern matching for
datatype=2 with the four observed filename shapes. File/voice/
video classes still use the F|A|V/{idx}/{filename} shape they
always did.
[revert] Round-6's "no md5 → fail-closed" is too strict for this
tool's actual usage
This MCP server is invoked locally by the user, paths surface
only in the local Claude conversation, and contacts are not
hostile. Codex round-6's hard fail-closed-on-missing-md5 broke
ergonomics for real wechat messages that lack md5 (some image
and voice dataitems) without a corresponding security gain in
this scenario. Reverted to round-5 behavior:
- md5 present → cryptographic verification, mismatch fails
- md5 absent → heuristic + ⚠️ warning, multiple-candidate
ambiguity still fails closed.
Kept all other round-6 hardening: streaming chunked md5, 500MB
cap, _safe_basename strict reject, _path_under_root realpath
check, multi-shard ambiguity, substring short-circuit for
non-type=19 big XML.
35/35 existing tests still pass; 5/5 group-prefix variants pass;
real-data smokes for both decoders still hit md5-verified paths.
* fix: round-8 — defer ambiguity until after md5 dedup + tighten file fallback
Two more findings, both real:
[high] decode_file_message no-md5 fallback was using `stem in f`
substring matching — `stem='论文'` would happily accept
`某老师论文.pdf`. Tightened to: exact match OR strict `(N)` copy
variant (`xxx(1).pdf`, `xxx (1).pdf`) per wechat's auto-rename
convention. 7/7 unit cases verify legitimate accept and false-
positive reject behavior.
[medium/P1 from GitHub Codex] decode_record_item had a stale early
`len(candidates) > 1 → ambiguity` check left over from round-7
refactor — it ran BEFORE the fullmd5 filter, making the md5
disambiguation block unreachable for the exact case where md5
could safely pick the right file. Removed the early check; md5
filter now runs first (and the post-md5 ambiguity check at line
~2467 still fails closed when md5 is missing AND multi-candidate).
35/35 tests pass. Real-data smokes (decode_file_message and
decode_record_item with their corresponding md5/fullmd5) still hit
the ✅ md5-verified path.
* test: add 29 helper-level regression tests for record-decoder helpers
Locks in the bugs fixed across PR #65's many review rounds so they
don't silently regress:
- _safe_basename (7 cases): strict reject of absolute paths,
parent-dir components, path separators, NUL — round-4 high #1.
- _md5_file_chunked (3 cases): streaming hash equals stdlib hashlib,
size cap rejects oversized files, missing file → error — round-6.
- _parse_message_content (5 cases): both legacy `:\n` and round-7
`:<?xml`/`:<msg` group-prefix shapes strip correctly; private
chat does not strip; bytes content returns the binary marker.
- _parse_app_message_outer (3 cases): small XML uses default cap,
oversized non-type=19 short-circuits (no 500K parse), oversized
type=19 retries successfully — round-5 medium #3 + round-2 P2-1.
- _format_record_dataitem (7 cases): text / file / image / 视频号 /
音乐 fall-through render correctly; unknown datatype falls back
to datadesc or [未知类型 N].
- _format_record_message_text (4 cases): >20KB outer XML expands
via _format_app_message_text end-to-end (regression for the
"P2-1 was a fake fix because I tested helper in isolation" miss);
empty datalist shows 待加载; chatroom marker appended; overflow
produces "…还有 N 条未显示" line.
The two MCP-tool wrappers (decode_file_message / decode_record_item)
lean on module globals + the real wechat cache layout. They are
exercised by real-data smoke runs in the PR description rather than
mocked here — mocking the entire wechat tree would dwarf the actual
logic under test.
64/64 total tests pass (35 existing + 29 new).
* refactor: simplify per /simplify code review (no behavior change)
Three review agents (reuse / quality / efficiency) flagged the
following high-confidence cleanups. All applied; all 64 tests still
pass; real-data smokes still hit md5-verified paths.
[quality] Remove PR-history references in comments
CLAUDE.md is explicit about this: comments should explain
non-obvious WHY, not narrate which Codex round caught what.
Cleared "round-2 high #2", "Codex round-3 medium #1", "round-5",
"round-6 强制", "round-7 实测", "round-8 high #1" from helper
docstrings and inline comments. Kept the substantive WHY (e.g.
"Reject 而不是 normalize because intent is suspicious").
[reuse + quality] Module-level datatype constants
Three places maintained their own copy of the datatype → label /
subdir mapping (_format_record_dataitem if-cascade, decode_record_
item type_label dict, subdir_map literal). Extracted
_RECORD_DATATYPE_LABEL and _RECORD_BINARY_SUBDIR to module top.
Single source of truth.
[quality] Hoist local imports to module top
Removed 7 inline `import glob as glob_mod` / `from datetime import
datetime as _dt` / `from datetime import datetime as _dt, timedelta
as _td` / `import hashlib as _hashlib` calls inside hot paths and
helpers. Aliases collapsed to plain names (datetime, timedelta,
glob, hashlib).
[efficiency] xpath: drop `.//` recursive descent for known-direct children
_format_record_dataitem was using `.//appbranditem/sourcedisplayname`
and `.//finderFeed/desc` even though both are direct children of
the dataitem. Changed to direct-child paths — meaningful for big
cards (50 items × subtree-walk per render).
[efficiency] md5 verification short-circuits on first match
Multiple candidates sharing the same md5 are wechat re-named copies
of the same file (e.g. `xxx (1).pdf`); any one is correct. Added
`break` after the first md5 match to skip hashing remaining
candidates (which can each be 100+ MB).
[quality] Compress _format_record_dataitem if-cascade
Datatypes that just emit `[label]` (2/3/4/5/7/23/37) and the link/
H5 pair (6/36) collapsed into membership checks against
_RECORD_DATATYPE_LABEL.
64/64 tests still pass.
---------
Co-authored-by: jiangbowen <robin@jiangbowendeMacBook-Air.local>
WeChat 4.x Database Decryptor
微信 4.0 (Windows、MacOS、Linux) 本地数据库解密工具。从运行中的微信进程内存提取加密密钥,解密所有 SQLCipher 4 加密数据库,并提供实时消息监听。
更新日志
防失联tg: https://t.me/wechat_decrypt
2025-03-03 — 富媒体内容 & 组合消息修复
- 表情包内联显示: 自动从 emoticon.db 构建 MD5→CDN 映射,支持自定义表情(NonStore)和商店表情(Store),CDN 下载后本地缓存
- 富媒体内容解析: 链接卡片(type 49)、文件、视频号、小程序、引用回复、位置分享等在 Web UI 中完整渲染
- 文字+图片组合消息不再丢失: 修复同时发送文字和图片时只显示最后一条的问题(前端去重 key 增加消息类型)
- 隐藏消息检测: 新增
_check_hidden_messages机制,session.db 只保存最后一条消息摘要,现在会异步查 message DB 找回同一秒内的其他消息 - MonitorDBCache 线程安全: 引入 per-key 锁,防止多线程并发解密同一数据库导致文件损坏
- Web UI 改进: 消息气泡样式优化、群聊发送者显示、图片缩略图点击放大
原理
微信 4.0 使用 SQLCipher 4 加密本地数据库:
- 加密算法: AES-256-CBC + HMAC-SHA512
- KDF: PBKDF2-HMAC-SHA512, 256,000 iterations
- 页面大小: 4096 bytes, reserve = 80 (IV 16 + HMAC 64)
- 每个数据库有独立的 salt 和 enc_key
WCDB (微信的 SQLCipher 封装) 会在进程内存中缓存派生后的 raw key,格式为 x'<64hex_enc_key><32hex_salt>'。三个平台(Windows / Linux / macOS)均可通过扫描进程内存匹配此模式,再通过 HMAC 校验 page 1 确认密钥正确性。
使用方法
环境要求
- Python 3.10+
- 微信 4.x
pip install -r requirements.txt
Windows:
- Windows 10/11
- 微信正在运行
- 需要管理员权限(读取进程内存)
Linux:
- 64-bit Linux
- 需要 root 权限或
CAP_SYS_PTRACE(读取/proc/<pid>/mem) db_dir默认类似~/Documents/xwechat_files/<wxid>/db_storage
macOS:
- macOS 10.15+(Apple Silicon / Intel 均可)
- 微信 4.x(macOS 版)
- Xcode Command Line Tools:
xcode-select --install - 需要对
/Applications/WeChat.app做 ad-hoc 重签名(允许进程内存读取) - 需要 root 权限运行扫描器
db_dir默认类似~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/<hash>/Message
安装依赖
pip install -r requirements.txt
Windows 如果遇到权限不足或全局环境不可写,可以改用:
py -m pip install --user -r requirements.txt
如果需要读取受保护的进程或把依赖安装到系统 Python,也可能需要以管理员身份打开终端。
快速开始
Windows:
python main.py
python main.py decrypt
Linux:
python3 main.py decrypt
macOS(密钥扫描用 C 版本,见下文 macOS 数据库密钥扫描 章节):
# 1. 重新签名(首次及微信升级后各一次)
sudo codesign --force --deep --sign - /Applications/WeChat.app
# 2. 编译并运行扫描器
cc -O2 -o find_all_keys_macos find_all_keys_macos.c -framework Foundation
sudo ./find_all_keys_macos
# 3. 解密
python3 decrypt_db.py
程序会自动完成:配置检测 → 内存扫描提取密钥 → 解密。首次运行会自动检测微信数据目录并生成 config.json。微信只要在运行中即可,无需重启或重新登录。
如果自动检测失败(例如微信安装在非默认位置),手动创建 config.json:
{
"db_dir": "D:\\xwechat_files\\你的微信ID\\db_storage",
"keys_file": "all_keys.json",
"decrypted_dir": "decrypted",
"wechat_process": "Weixin.exe"
}
Linux 版 config.json 示例:
{
"db_dir": "/home/yourname/Documents/xwechat_files/your_wxid/db_storage",
"keys_file": "all_keys.json",
"decrypted_dir": "decrypted",
"wechat_process": "wechat"
}
macOS 版 config.json 示例:
{
"db_dir": "/Users/yourname/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/<hash>/Message",
"keys_file": "all_keys.json",
"decrypted_dir": "decrypted",
"wechat_process": "WeChat"
}
db_dir 路径:Windows 可在微信设置 → 文件管理中找到;Linux 默认在 ~/Documents/xwechat_files/<wxid>/db_storage;macOS 在 ~/Library/Containers/com.tencent.xinWeChat/.../Message(<hash> 是微信随机生成的账号目录)。
Web UI 说明
python main.py 启动后打开 http://localhost:5678 查看实时消息流。
- 30ms 轮询 WAL 文件变化 (mtime)
- 检测到变化后全量解密 + WAL patch (~70ms)
- SSE 实时推送到浏览器
- 总延迟约 100ms
- 图片消息内联预览(支持旧 XOR / V1 / V2 三种 .dat 加密格式)
HTTP API
| 端点 | 说明 |
|---|---|
GET /api/history |
最近消息列表 (JSON) |
GET /api/history?chat=群名 |
按群名/用户名过滤消息 |
GET /api/history?since=1712000000 |
增量拉取(返回该时间戳之后的消息) |
GET /api/history?chat=群名&since=ts&limit=100 |
参数可组合使用 |
GET /api/tags |
所有联系人标签及成员 (JSON) |
GET /api/tags?name=同事 |
按标签名过滤 |
GET /stream |
SSE 实时消息推送 |
将特定群消息存到自己的数据库:监听 /stream 或轮询 /api/history?chat=群名&since=上次时间戳,写入即可。
MCP Server (Claude AI 集成)
将微信数据查询能力接入 Claude Code,让 AI 直接读取你的微信消息。
pip install -r requirements.txt
注册到 Claude Code:
claude mcp add wechat -- python C:\Users\你的用户名\wechat-decrypt\mcp_server.py
或手动编辑 ~/.claude.json:
{
"mcpServers": {
"wechat": {
"type": "stdio",
"command": "python",
"args": ["C:\\Users\\你的用户名\\wechat-decrypt\\mcp_server.py"]
}
}
}
注册后在 Claude Code 中即可使用以下工具:
| Tool | 功能 |
|---|---|
get_recent_sessions(limit) |
最近会话列表(含消息摘要、未读数) |
get_chat_history(chat_name, limit, offset, start_time, end_time) |
指定聊天的消息记录,支持时间范围和分页 |
search_messages(keyword, chat_name, start_time, end_time, limit, offset) |
统一搜索消息;支持全库、单个聊天对象、多个聊天对象、时间范围和分页 |
get_contacts(query, limit) |
搜索/列出联系人 |
get_contact_tags() |
列出所有联系人标签及成员数量 |
get_tag_members(tag_name) |
获取指定标签下的所有联系人,支持模糊匹配 |
get_new_messages() |
获取自上次调用以来的新消息 |
get_voice_messages(chat_name) |
列出某会话所有语音消息(local_id、时长、时间戳) |
decode_voice(chat_name, local_id) |
解码 SILK 语音为本地 WAV 文件 |
transcribe_voice(chat_name, local_id) |
转录语音为文字(自动检测语言) |
前置条件:需要先运行 python main.py 或 python find_all_keys.py 完成密钥提取。
说明:search_messages 的 limit 最大为 500;get_chat_history 支持更大的 limit,但消息很多时仍建议配合 offset 分页读取。
⚠️ 语音转录隐私
transcribe_voice 默认使用本地 Whisper(CPU),数据全程留在本机。transcribe_chat.py 批量 CLI 共享同一份配置。
如需切换到 OpenAI Whisper API(更快、Mandarin 精度更高),在 config.json 中:
{
"transcription_backend": "openai",
"openai_api_key": "sk-..."
}
启用后语音文件会上传至 OpenAI 服务器进行转录。需 pip install openai。
- 成本:约 $0.006 / 分钟(OpenAI 计价)
- 文件 > 25MB 在上传前被拒绝(OpenAI 上限)
- 首次启用云后端时 stderr 会打一行警告
transcription_backend或openai_api_key任一缺失时静默回退 local- 切换后端后,旧缓存条目(backend 不匹配)会自动重新转录
图片解密 (V2 格式)
微信 4.0 (2025-08+) 的 .dat 图片文件使用 AES-128-ECB + XOR 混合加密 (V2 格式)。AES 密钥的获取方式因平台而异:
Windows / Linux(从进程内存扫描):
# 1. 在微信中打开查看 2-3 张图片(点击看大图)
# 2. 立即运行密钥提取(持续监控版):
python find_image_key_monitor.py
# 或单次扫描版:
python find_image_key.py
AES 密钥仅在微信查看图片时临时加载到内存中。如果扫描未找到密钥,请先在微信中查看几张图片,然后立即重新运行脚本。
macOS(从磁盘 kvcomm 缓存派生,无需扫描进程内存):
python find_image_key_macos.py
无需提前在微信中查看图片,无需 root 权限,无需重签名。脚本会扫描 ~/Library/Containers/com.tencent.xinWeChat/.../app_data/net/kvcomm/key_*.statistic 文件名提取派生码 code,配合 db_dir 路径里的 wxid,按 aes_key = MD5(str(code) + cleaned_wxid)[:16] / xor_key = code & 0xFF 的规则推算密钥,并用一张 V2 _t.dat 缩略图做 AES 模板验证。解决 issue #23(macOS 内存扫描器 197K 候选全部失败)。
派生算法的发现归功于 @hicccc77 在 issue #23 的评论,参考实现见其 WeFlow 项目(CC BY-NC-SA 4.0)。本仓库的 find_image_key_macos.py 是基于该算法的独立 Python clean-room 实现。
密钥会自动保存到 config.json 的 image_aes_key / image_xor_key 字段。之后 monitor_web.py 启动时会自动加载,图片消息将显示内联预览。
文件说明
| 文件 | 说明 |
|---|---|
main.py |
一键启动入口 — 自动配置、提取密钥、启动服务 |
config.py |
配置加载器(自动检测微信数据目录) |
find_all_keys.py |
平台分发入口(Windows / Linux) |
find_all_keys_windows.py |
Windows 版内存扫描提 key |
find_all_keys_linux.py |
Linux 版内存扫描提 key |
decrypt_db.py |
全量解密所有数据库 |
mcp_server.py |
MCP Server,让 Claude AI 查询微信数据 |
monitor_web.py |
实时消息监听 (Web UI + SSE + 图片预览) |
monitor.py |
实时消息监听 (命令行) |
decode_image.py |
图片 .dat 文件解密模块 (XOR / V1 / V2) |
find_image_key.py |
从微信进程内存提取图片 AES 密钥(Windows / Linux) |
find_image_key_monitor.py |
持续监控版密钥提取(Windows / Linux,推荐) |
find_image_key_macos.py |
macOS 版图片密钥派生(从磁盘 kvcomm 缓存推算,无需扫描内存) |
latency_test.py |
延迟测量诊断工具 |
find_all_keys_macos.c |
macOS 版内存密钥扫描器 (C, Mach VM API) |
技术细节
WAL 处理
微信使用 SQLite WAL 模式,WAL 文件是预分配固定大小 (4MB)。检测变化时:
- 不能用文件大小 (永远不变)
- 使用 mtime 检测写入
- 解密 WAL frame 时需校验 salt 值,跳过旧周期遗留的 frame
图片 .dat 加密格式
微信本地图片 (.dat) 有三种加密格式:
| 格式 | 时期 | Magic | 加密方式 | 密钥来源 |
|---|---|---|---|---|
| 旧 XOR | ~2025-07 | 无 | 单字节 XOR | 自动检测 (对比 magic bytes) |
| V1 | 过渡期 | 07 08 V1 08 07 |
AES-ECB + XOR | 固定 key: cfcd208495d565ef |
| V2 | 2025-08+ | 07 08 V2 08 07 |
AES-128-ECB + XOR | 从进程内存提取 |
V2 文件结构: [6B signature] [4B aes_size LE] [4B xor_size LE] [1B padding] + [AES-ECB encrypted] [raw unencrypted] [XOR encrypted]
数据库结构
解密后包含约 26 个数据库:
session/session.db- 会话列表 (最新消息摘要)message/message_*.db- 聊天记录contact/contact.db- 联系人media_*/media_*.db- 媒体文件索引- 其他: head_image, favorite, sns, emoticon 等
macOS 数据库密钥扫描 (WeChat 4.x)
macOS 版微信 4.x 使用 SQLCipher 4 加密本地数据库,密钥格式为 x'<64hex_key><32hex_salt>'。C 版扫描器通过 Mach VM API 扫描微信进程内存提取密钥。
前置条件
- macOS (Apple Silicon / Intel)
- WeChat 4.x (macOS 版)
- Xcode Command Line Tools:
xcode-select --install - 微信需要 ad-hoc 签名(或安装了防撤回补丁):
sudo codesign --force --deep --sign - /Applications/WeChat.app
编译和使用
# 编译
cc -O2 -o find_all_keys_macos find_all_keys_macos.c -framework Foundation
# 运行(自动查找微信进程、扫描内存、匹配 DB salt)
sudo ./find_all_keys_macos
# 或指定 PID
sudo ./find_all_keys_macos <pid>
输出 all_keys.json,格式兼容 decrypt_db.py,可直接用于解密:
python3 decrypt_db.py
免责声明
本工具仅用于学习和研究目的,用于解密自己的微信数据。请遵守相关法律法规,不要用于未经授权的数据访问。