100 lines
3.7 KiB
Python
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)
|