63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
"""Encrypt/decrypt Coruna secondary type-0x01 .min.js packs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import lzma
|
|
import struct
|
|
|
|
from Crypto.Cipher import ChaCha20
|
|
|
|
WRAP_MAGIC = b"\x0d\xf0\xed\x0b" # 0x0BEDF00D LE
|
|
F00D_MAGIC = 0xF00DBEEF
|
|
|
|
|
|
def build_f00dbeef_type01(dylib: bytes) -> bytes:
|
|
"""Single-entry F00DBEEF used by this campaign's secondary packs."""
|
|
header = struct.pack(
|
|
"<6I",
|
|
F00D_MAGIC,
|
|
1, # version / entry-count field as in sample
|
|
0x00010000, # type 0x01
|
|
3,
|
|
0x18, # payload offset
|
|
len(dylib),
|
|
)
|
|
return header + dylib
|
|
|
|
|
|
def wrap_xz(plaintext: bytes) -> bytes:
|
|
compressed = lzma.compress(plaintext, format=lzma.FORMAT_XZ)
|
|
return WRAP_MAGIC + struct.pack("<I", len(plaintext)) + compressed
|
|
|
|
|
|
def unwrap_xz(blob: bytes) -> bytes:
|
|
if blob[:4] != WRAP_MAGIC:
|
|
raise ValueError(f"bad wrap magic: {blob[:4]!r}")
|
|
expected = struct.unpack_from("<I", blob, 4)[0]
|
|
plain = lzma.decompress(blob[8:])
|
|
if len(plain) != expected:
|
|
raise ValueError(f"xz size mismatch: {len(plain)} != {expected}")
|
|
return plain
|
|
|
|
|
|
def chacha_crypt(data: bytes, key: bytes) -> bytes:
|
|
if len(key) != 32:
|
|
raise ValueError("ChaCha20 key must be 32 bytes")
|
|
return ChaCha20.new(key=key, nonce=b"\x00" * 8).encrypt(data)
|
|
|
|
|
|
def encrypt_secondary_minjs(dylib: bytes, key: bytes) -> bytes:
|
|
return chacha_crypt(wrap_xz(build_f00dbeef_type01(dylib)), key)
|
|
|
|
|
|
def decrypt_secondary_minjs(blob: bytes, key: bytes) -> bytes:
|
|
plain = unwrap_xz(chacha_crypt(blob, key))
|
|
if struct.unpack_from("<I", plain, 0)[0] != F00D_MAGIC:
|
|
raise ValueError("not F00DBEEF after decrypt")
|
|
offset = struct.unpack_from("<I", plain, 16)[0]
|
|
size = struct.unpack_from("<I", plain, 20)[0]
|
|
dylib = plain[offset : offset + size]
|
|
if len(dylib) != size:
|
|
raise ValueError("truncated dylib in F00DBEEF")
|
|
return dylib
|