feat(sns): 增强朋友圈时间线/媒体获取与实时同步

- 新增 /api/sns/users:按发圈数统计联系人(支持 keyword/limit)
- 新增 /api/sns/realtime/sync_latest:WCDB 实时增量同步到解密库(append-only),并持久化 sync state
- 朋友圈媒体优先走“远程下载+解密”:图片支持 wcdb_decrypt_sns_image,视频/实况支持 ISAAC64(WeFlow 逻辑)
- 增加 WeFlow WASM keystream(Node) 优先 + Python ISAAC64 fallback,提升兼容性
- wcdb_api.dll 支持多路径自动发现/环境变量覆盖,并在状态信息中回传实际使用路径
This commit is contained in:
2977094657
2026-02-17 23:40:10 +08:00
parent 35a2266b1c
commit 6e127b2e32
5 changed files with 2706 additions and 108 deletions

View File

@@ -0,0 +1,169 @@
from __future__ import annotations
"""ISAAC-64 PRNG (WeFlow compatible).
WeChat SNS live photo/video decryption uses a keystream generated by ISAAC-64 and
XORs the first 128KB of the mp4 file. WeFlow's implementation reverses the
generated byte array, so we mirror that behavior for compatibility.
"""
from typing import Any
_MASK_64 = 0xFFFFFFFFFFFFFFFF
def _u64(v: int) -> int:
return int(v) & _MASK_64
class Isaac64:
def __init__(self, seed: Any):
seed_text = str(seed).strip()
if not seed_text:
seed_val = 0
else:
try:
# WeFlow seeds with BigInt(seed), where seed is usually a decimal string.
seed_val = int(seed_text, 0)
except Exception:
seed_val = 0
self.mm = [_u64(0) for _ in range(256)]
self.aa = _u64(0)
self.bb = _u64(0)
self.cc = _u64(0)
self.randrsl = [_u64(0) for _ in range(256)]
self.randrsl[0] = _u64(seed_val)
self.randcnt = 0
self._init(True)
def _init(self, flag: bool) -> None:
a = b = c = d = e = f = g = h = _u64(0x9E3779B97F4A7C15)
def mix() -> tuple[int, int, int, int, int, int, int, int]:
nonlocal a, b, c, d, e, f, g, h
a = _u64(a - e)
f = _u64(f ^ (h >> 9))
h = _u64(h + a)
b = _u64(b - f)
g = _u64(g ^ _u64(a << 9))
a = _u64(a + b)
c = _u64(c - g)
h = _u64(h ^ (b >> 23))
b = _u64(b + c)
d = _u64(d - h)
a = _u64(a ^ _u64(c << 15))
c = _u64(c + d)
e = _u64(e - a)
b = _u64(b ^ (d >> 14))
d = _u64(d + e)
f = _u64(f - b)
c = _u64(c ^ _u64(e << 20))
e = _u64(e + f)
g = _u64(g - c)
d = _u64(d ^ (f >> 17))
f = _u64(f + g)
h = _u64(h - d)
e = _u64(e ^ _u64(g << 14))
g = _u64(g + h)
return a, b, c, d, e, f, g, h
for _ in range(4):
mix()
for i in range(0, 256, 8):
if flag:
a = _u64(a + self.randrsl[i])
b = _u64(b + self.randrsl[i + 1])
c = _u64(c + self.randrsl[i + 2])
d = _u64(d + self.randrsl[i + 3])
e = _u64(e + self.randrsl[i + 4])
f = _u64(f + self.randrsl[i + 5])
g = _u64(g + self.randrsl[i + 6])
h = _u64(h + self.randrsl[i + 7])
mix()
self.mm[i] = a
self.mm[i + 1] = b
self.mm[i + 2] = c
self.mm[i + 3] = d
self.mm[i + 4] = e
self.mm[i + 5] = f
self.mm[i + 6] = g
self.mm[i + 7] = h
if flag:
for i in range(0, 256, 8):
a = _u64(a + self.mm[i])
b = _u64(b + self.mm[i + 1])
c = _u64(c + self.mm[i + 2])
d = _u64(d + self.mm[i + 3])
e = _u64(e + self.mm[i + 4])
f = _u64(f + self.mm[i + 5])
g = _u64(g + self.mm[i + 6])
h = _u64(h + self.mm[i + 7])
mix()
self.mm[i] = a
self.mm[i + 1] = b
self.mm[i + 2] = c
self.mm[i + 3] = d
self.mm[i + 4] = e
self.mm[i + 5] = f
self.mm[i + 6] = g
self.mm[i + 7] = h
self._isaac64()
self.randcnt = 256
def _isaac64(self) -> None:
self.cc = _u64(self.cc + 1)
self.bb = _u64(self.bb + self.cc)
for i in range(256):
x = self.mm[i]
if (i & 3) == 0:
# aa ^= ~(aa << 21)
self.aa = _u64(self.aa ^ (_u64(self.aa << 21) ^ _MASK_64))
elif (i & 3) == 1:
self.aa = _u64(self.aa ^ (self.aa >> 5))
elif (i & 3) == 2:
self.aa = _u64(self.aa ^ _u64(self.aa << 12))
else:
self.aa = _u64(self.aa ^ (self.aa >> 33))
self.aa = _u64(self.mm[(i + 128) & 255] + self.aa)
y = _u64(self.mm[(x >> 3) & 255] + self.aa + self.bb)
self.mm[i] = y
self.bb = _u64(self.mm[(y >> 11) & 255] + x)
self.randrsl[i] = self.bb
def get_next(self) -> int:
if self.randcnt == 0:
self._isaac64()
self.randcnt = 256
idx = 256 - self.randcnt
self.randcnt -= 1
return _u64(self.randrsl[idx])
def generate_keystream(self, size: int) -> bytes:
"""Generate a keystream of `size` bytes (must be multiple of 8)."""
if size <= 0:
return b""
if size % 8 != 0:
raise ValueError("ISAAC64 keystream size must be multiple of 8 bytes.")
out = bytearray()
count = size // 8
for _ in range(count):
out.extend(int(self.get_next()).to_bytes(8, "little", signed=False))
# WeFlow reverses the entire byte array (Uint8Array.reverse()).
out.reverse()
return bytes(out)