ksjsb/core/weapon_d0.py
2026-07-30 20:25:56 +08:00

100 lines
3.7 KiB
Python

"""d0 weapon SDK crypto — pure-computation port of ``com.kuaishou.weapon.ks.d0``.
Reverse-engineered from the APK Java fallback path (``d0.java``,
``l.java``, ``j1.java``, ``i0.java``, ``q.java`` under
``out/jadx/sources/com/kuaishou/weapon/ks/``). The native JNI path
(``W.dc/dr/ar/ac``) is the accelerated equivalent; the Java fallback produces
byte-identical output and is what we reproduce here.
Layer map (verified against the jadx sources):
q = ``android.util.Base64`` with flag NO_WRAP (standard alphabet)
i0 = gzip (``a``=compress, ``b``=decompress)
j1 = RC4 (KSA ``b(str)`` + PRGA ``a(data,key)``; symmetric)
l = AES/CBC/PKCS5Padding (``a``=decrypt(key,iv,data), ``c``=encrypt(key,iv,data))
Key derivation (``d0.java:22`` / ``41`` / ``192``)::
raw = base64_decode("a3NyaXNrY3RsYnVzaW5zc3Z4cHprd3NwYWlvcXBrc3M=")
= b"ksriskctlbusinssvxpzkwspaioqpkss"
key16 = first 16 chars -> "ksriskctlbusinss" (pad with '0' / truncate to 16)
The same ``key16`` is used as BOTH the AES key and the AES IV — ``l.a``/``l.c``
are always called with ``(key16, key16, data)``.
Transforms::
d0.encrypt(str) = base64( AES-CBC-enc(key16,key16, RC4(key16, gzip(plaintext))) )
d0.decrypt(str) = gunzip( RC4(key16, AES-CBC-dec(key16,key16, base64_decode(ct))) )
AES-CBC primitives are reused from :mod:`core.h5_kww_alg` (vendored pure-Python
AES-128), so no new third-party crypto dependency is introduced.
"""
from __future__ import annotations
import base64
import gzip
from .h5_kww_alg import kwf_aes_cbc_decrypt, kwf_aes_cbc_encrypt
_KEY_B64 = "a3NyaXNrY3RsYnVzaW5zc3Z4cHprd3NwYWlvcXBrc3M="
def _derive_key16() -> bytes:
"""Reproduce ``d0.java`` key normalization: pad-with-'0' or truncate to 16."""
raw = base64.b64decode(_KEY_B64) # q.a(bytes, 2)
s = raw.decode("latin-1") # new String(bytes); bytes are ASCII
if len(s) < 16:
s = s + "0" * (16 - len(s))
elif len(s) > 16:
s = s[:16]
return s[:16].encode("latin-1")
KEY16 = _derive_key16() # b"ksriskctlbusinss"
def _rc4(key: bytes, data: bytes) -> bytes:
"""Textbook RC4 — port of ``j1.b`` (KSA) + ``j1.a`` (PRGA). Symmetric."""
s = list(range(256))
j = 0
klen = len(key)
for i in range(256):
j = (key[i % klen] + s[i] + j) & 0xFF
s[i], s[j] = s[j], s[i]
out = bytearray(len(data))
i = 0
j = 0
for n in range(len(data)):
i = (i + 1) & 0xFF
j = (s[i] + j) & 0xFF
s[i], s[j] = s[j], s[i]
out[n] = s[(s[i] + s[j]) & 0xFF] ^ data[n]
return bytes(out)
def decrypt(ciphertext_b64: str) -> str:
"""``d0.a(str)`` / ``d0.a(str,key)`` / static ``d0.b(str)`` — decrypt a d0 blob.
Used by ``p1.java`` to decrypt the config-pull ``antispamSdkRsp`` field, and
by ``d0.b(str)`` to decode hardcoded host/path strings.
"""
raw = base64.b64decode(ciphertext_b64) # q.a(bytes, 2)
aes_out = kwf_aes_cbc_decrypt(raw, KEY16, KEY16) # l.a(key16,key16,_)
rc4_out = _rc4(KEY16, aes_out) # j1.b(_,key16)
plain = gzip.decompress(rc4_out) # i0.b(_)
return plain.decode("utf-8") # new String(bytes)
def encrypt(plaintext: str) -> str:
"""``d0.c(str)`` / ``d0.b(str,key)`` — encrypt a string into a d0 blob.
Used to encrypt the config-pull request body.
"""
data = plaintext.encode("utf-8") # str.getBytes()
gz = gzip.compress(data) # i0.a(_)
rc4_out = _rc4(KEY16, gz) # j1.c(_,key16)
aes_out = kwf_aes_cbc_encrypt(rc4_out, KEY16, KEY16) # l.c(key16,key16,_)
return base64.b64encode(aes_out).decode("ascii") # q.c(_,2)