Skip to content

Commit 1dca69a

Browse files
committed
improvement(sns-media): 统一朋友圈远程媒体下载/解密/缓存逻辑
- 新增 sns_media 模块:CDN URL 归一化、远程下载、图片 wcdb_api 解密、视频 WxIsaac64(WeFlow WASM)/ISAAC64 兜底解密与缓存 - routers/sns 与 sns_export_service 复用该模块,收敛重复实现 - 调整 ISAAC64 兜底实现:明确 keystream 生成与字节序格式,作为 WASM 不可用时的 best-effort - 增加单测覆盖:URL 改写、视频异或解密、缓存命中/升级、解密失败
1 parent 3d989e4 commit 1dca69a

5 files changed

Lines changed: 993 additions & 412 deletions

File tree

src/wechat_decrypt_tool/isaac64.py

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
from __future__ import annotations
22

3-
"""ISAAC-64 PRNG (WeFlow compatible).
3+
"""ISAAC-64 PRNG (best-effort fallback).
44
5-
WeChat SNS live photo/video decryption uses a keystream generated by ISAAC-64 and
6-
XORs the first 128KB of the mp4 file. WeFlow's implementation reverses the
7-
generated byte array, so we mirror that behavior for compatibility.
5+
In this repo, Moments (SNS) *video* decryption uses a keystream generator that
6+
matches WeFlow's WxIsaac64 (WASM) behavior and XORs only the first 128KB of the
7+
MP4.
8+
9+
This module provides a pure-Python ISAAC-64 implementation so the backend can
10+
still attempt to generate a keystream when the WASM helper is unavailable.
11+
12+
Notes:
13+
- Moments *image* decryption is handled via `wcdb_api.dll` (`wcdb_decrypt_sns_image`)
14+
because "ISAAC-64 full-file XOR" is not reliably reproducible for images across
15+
different versions/samples.
16+
- This ISAAC-64 implementation may not perfectly match WxIsaac64; treat it as
17+
best-effort.
818
"""
919

10-
from typing import Any
20+
from typing import Any, Literal
1121

1222
_MASK_64 = 0xFFFFFFFFFFFFFFFF
1323

@@ -143,27 +153,58 @@ def _isaac64(self) -> None:
143153
self.bb = _u64(self.mm[(y >> 11) & 255] + x)
144154
self.randrsl[i] = self.bb
145155

146-
def get_next(self) -> int:
156+
def rand_u64(self) -> int:
157+
"""Return the next ISAAC-64 output as an unsigned 64-bit integer.
158+
159+
Note: The original reference `rand()` consumes `randrsl[]` in reverse order.
160+
"""
147161
if self.randcnt == 0:
148162
self._isaac64()
149163
self.randcnt = 256
150-
idx = 256 - self.randcnt
151164
self.randcnt -= 1
152-
return _u64(self.randrsl[idx])
153-
154-
def generate_keystream(self, size: int) -> bytes:
155-
"""Generate a keystream of `size` bytes (must be multiple of 8)."""
156-
if size <= 0:
165+
return _u64(self.randrsl[self.randcnt])
166+
167+
# Backward-compatible alias (older callers used `get_next()`).
168+
def get_next(self) -> int: # pragma: no cover
169+
return self.rand_u64()
170+
171+
KeystreamWordFormat = Literal["raw_le", "raw_be", "be_swap32", "le_swap32"]
172+
173+
@staticmethod
174+
def _raw_to_bytes(raw: int, word_format: KeystreamWordFormat) -> bytes:
175+
"""Serialize one 64-bit `rand()` output to 8 bytes.
176+
177+
- raw_le/raw_be: direct endianness of the 64-bit integer.
178+
- be_swap32: big-endian bytes with 32-bit halves swapped (BE(lo32)||BE(hi32)).
179+
This matches the byte layout implied by the doc's `htonl(hi32)||htonl(lo32)`
180+
pattern when the resulting u64 is read as bytes on little-endian hosts.
181+
- le_swap32: little-endian bytes with 32-bit halves swapped.
182+
"""
183+
v = _u64(raw)
184+
if word_format == "raw_le":
185+
return int(v).to_bytes(8, "little", signed=False)
186+
if word_format == "raw_be":
187+
return int(v).to_bytes(8, "big", signed=False)
188+
if word_format == "be_swap32":
189+
b = int(v).to_bytes(8, "big", signed=False)
190+
return b[4:8] + b[0:4]
191+
if word_format == "le_swap32":
192+
b = int(v).to_bytes(8, "little", signed=False)
193+
return b[4:8] + b[0:4]
194+
raise ValueError(f"Unknown ISAAC64 word_format: {word_format}")
195+
196+
def generate_keystream(self, size: int, *, word_format: KeystreamWordFormat = "be_swap32") -> bytes:
197+
"""Generate a keystream of `size` bytes.
198+
199+
This mirrors the decryption loop behavior: produce a new 8-byte keyblock
200+
for every 8 bytes of input, and slice for tail bytes.
201+
"""
202+
want = int(size or 0)
203+
if want <= 0:
157204
return b""
158-
if size % 8 != 0:
159-
raise ValueError("ISAAC64 keystream size must be multiple of 8 bytes.")
160205

206+
blocks = (want + 7) // 8
161207
out = bytearray()
162-
count = size // 8
163-
for _ in range(count):
164-
out.extend(int(self.get_next()).to_bytes(8, "little", signed=False))
165-
166-
# WeFlow reverses the entire byte array (Uint8Array.reverse()).
167-
out.reverse()
168-
return bytes(out)
169-
208+
for _ in range(blocks):
209+
out.extend(self._raw_to_bytes(self.rand_u64(), word_format))
210+
return bytes(out[:want])

0 commit comments

Comments
 (0)