Commit Graph

2 Commits

Author SHA1 Message Date
Belugary
216f44a99f fix(image): reject corrupted V2 image when AES or XOR key is wrong (#81)
`v2_decrypt_file` previously wrote files to disk even when the keys were
wrong, producing garbage output with no way for the caller to detect the
failure:

1. Wrong AES key -> `detect_image_format` returns 'bin' (magic does not
   match any known format) -> a `.bin` file of random bytes was written.
2. Wrong XOR key with correct AES key -> file header looks like a valid
   jpg/png (the AES segment decrypts correctly) but the trailing XOR
   segment is scrambled -> callers get a half-valid image file that
   image viewers either render as truncated or fail to open.

Both cases now return `(None, None)`:

- `fmt == 'bin'` -> fail fast, no file written.
- `xor_size >= 2` -> validate trailer magic by format:
    * jpg must end with FF D9 (EOI marker)
    * png must contain IEND chunk in the last 12 bytes
  Other formats (gif/bmp/tif/webp/hevc/wxgf) lack a mandatory trailer
  signature, so they skip the check to avoid false rejection.

Also fixes a latent bug in `test_decode_image_v1_no_aes_key_uses_fixed_key`:
the test built the synthetic .dat with `TEST_XOR_KEY=0x37` but constructed
`ImageResolver` without `xor_key=`, defaulting to `0x88`. The XOR segment
was always scrambled — the test passed because the AES segment alone was
enough for `detect_image_format` to return 'png' from the header, and no
trailer validation existed to catch the corruption. The new trailer check
surfaces this, so the test now passes `xor_key=TEST_XOR_KEY` explicitly.

Tests: 5 new cases (wrong AES key / wrong XOR for jpg / wrong XOR for
png / xor_size=0 bypass / wxgf bypass). All 156 existing tests still pass.
2026-05-12 16:18:37 +08:00
Belugary
c29e8dd868 fix: ImageResolver 支持微信 4.0+ V2 加密图片格式 (#61)
ImageResolver.decode_image 之前只调 xor_decrypt_file(老格式 XOR-only
路径),微信 4.0+(2025-08+)已经改用 V2 AES-128-ECB + XOR 混合加密,
导致 mcp_server.py 注册的 decode_image MCP 工具对 V2 .dat 文件返回的
"解密"内容是错的——Claude AI 通过 MCP 调用看不到 V2 时代的图片。

monitor_web.py 早已正确处理 V2(line 41-42, 791-795:从 _cfg 读
image_aes_key / image_xor_key 后调 decrypt_dat_file 自动 magic 分发),
本次把 MCP 路径补齐,行为与 monitor_web.py 对齐。

改动:
- ImageResolver.__init__ 增加 aes_key=None, xor_key=0x88 关键字参数
  (默认值保持向后兼容,老调用方无需改动)
- ImageResolver.decode_image 把 xor_decrypt_file 换成 decrypt_dat_file,
  按 magic 自动分发 V2 / V1 / 老 XOR
- V2 文件 + 缺 aes_key 时早期返回结构化错误信息,避免在 v2_decrypt_file
  内静默失败成笼统的"解密失败"
- v2_decrypt_file 入口接受 xor_key 字符串形式(int(_, 0) 解析),
  与 aes_key 已有的 str→bytes 处理对称,允许 config.json 写 "0x88"
- mcp_server.py 实例化时从 _cfg 读 image_aes_key / image_xor_key 注入

兼容性:
- ImageResolver 老调用方(不传 keys)继续走老 XOR 路径,零 breaking
- V1 magic(\x07\x08V1)不会被 is_v2_format 拦截,走 decrypt_dat_file
  内置固定 key,所以 aes_key=None 也能解 V1 文件
- 整 repo 只有 mcp_server.py 一处生产调用 ImageResolver(...),已 grep 确认

测试覆盖(11 个新测试,tests/test_decode_image_v2.py):
- v2_decrypt_file 合成数据 round-trip 字节级相等
- decrypt_dat_file 按 magic 自动分发 V2 / V1 / 老 XOR 三条路径
- aes_key 接受 str(来自 config.json)和 bytes 两种形式
- xor_key 接受 str(如 "0x88")和 int 两种形式
- V2 wxgf 裸流返回 fmt='hevc'(HEVC→JPEG 转换是 monitor_web 职责,
  不在 ImageResolver 内做,保留 .hevc 输出)
- ImageResolver 端到端:from local_id to decrypted file
- ImageResolver(aes_key=None) + V1 文件走固定 key 路径
- ImageResolver(aes_key=None) + V2 文件返回 success=False + 友好错误
- ImageResolver 默认参数 + 老 XOR .dat 保持向后兼容

测试 46 个全部通过(11 新 + 35 旧)。

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:05:53 +08:00