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.
This commit is contained in:
Belugary
2026-05-12 16:18:37 +08:00
committed by GitHub
parent d86e0acad1
commit 216f44a99f
2 changed files with 98 additions and 1 deletions

View File

@@ -189,6 +189,22 @@ def v2_decrypt_file(dat_path, out_path=None, aes_key=None, xor_key=0x88):
# wxgf (HEVC 裸流) 格式
if decrypted[:4] == b'wxgf':
fmt = 'hevc'
elif fmt == 'bin':
# detect_image_format 返回 'bin' = magic 不匹配任何已知图片格式,
# 通常说明 AES key 错(解密后产生随机字节)。拒绝写出无意义的 .bin
# 垃圾文件,让 caller 知道解密失败。
return None, None
elif xor_size >= 2:
# XOR key 错时 AES/raw 段可能产生合法 magic(看似正常 jpg/png 头),
# 但 XOR 段乱码。用尾部 magic 验证 XOR key 正确性:
# - JPG 必须以 FF D9 (EOI marker) 收尾
# - PNG 末尾 12 字节必须含 IEND chunk
# 其他格式 (gif/bmp/tif/webp/hevc) 缺乏强制 trailer signature,
# 不做校验以避免误杀。xor_size < 2 时无 XOR 段或样本过小,跳过。
if fmt == 'jpg' and decrypted[-2:] != b'\xff\xd9':
return None, None
if fmt == 'png' and b'IEND' not in decrypted[-12:]:
return None, None
if out_path is None:
base = os.path.splitext(dat_path)[0]