|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -"""ISAAC-64 PRNG (WeFlow compatible). |
| 3 | +"""ISAAC-64 PRNG (best-effort fallback). |
4 | 4 |
|
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. |
8 | 18 | """ |
9 | 19 |
|
10 | | -from typing import Any |
| 20 | +from typing import Any, Literal |
11 | 21 |
|
12 | 22 | _MASK_64 = 0xFFFFFFFFFFFFFFFF |
13 | 23 |
|
@@ -143,27 +153,58 @@ def _isaac64(self) -> None: |
143 | 153 | self.bb = _u64(self.mm[(y >> 11) & 255] + x) |
144 | 154 | self.randrsl[i] = self.bb |
145 | 155 |
|
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 | + """ |
147 | 161 | if self.randcnt == 0: |
148 | 162 | self._isaac64() |
149 | 163 | self.randcnt = 256 |
150 | | - idx = 256 - self.randcnt |
151 | 164 | 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: |
157 | 204 | return b"" |
158 | | - if size % 8 != 0: |
159 | | - raise ValueError("ISAAC64 keystream size must be multiple of 8 bytes.") |
160 | 205 |
|
| 206 | + blocks = (want + 7) // 8 |
161 | 207 | 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