Files
boxsim-fileformat-rev/boxsim/krypt.py
T

55 lines
1.7 KiB
Python
Raw Normal View History

"""Boxsim file scrambler: the XOR layer of the MyIni2CK container.
Reverse engineered from Boxsim 2.1 (Init at VA 0xa12be4, next byte at 0xa12c9c).
A table-shuffled combination of three linear congruential generators, seeded
with the same constants for every file Boxsim has ever written.
"""
M32 = 1 << 32
SEED1 = 123
SEED2 = 456
# Compared against a global at VA 0xb10588, which the image ships as 0xffffffff
# and no code path ever writes.
THRESHOLD = 0xFFFFFFFF
class KryptPRNG:
"""Keystream generator for the XOR layer."""
__slots__ = ("s1", "s2", "s3", "table", "thr")
def __init__(self, s1: int = SEED1, s2: int = SEED2, threshold: int = THRESHOLD):
self.s1, self.s2, self.thr = s1, s2, threshold
self.s3 = (s1 % 0xFFFE) * (s2 % 0x7F23)
self.table = [0] * 256
for i in range(256):
self._step()
self.table[i] = (self.s1 + self.s2) % M32
def _step(self) -> None:
self.s1 = (self.s1 * 0x10B95 + 1) % M32
self.s2 = (self.s2 * 0x10BA9 + 1) % M32
if self.s2 >= self.thr:
self.s2 = 0
self.s3 = (self.s3 * 0x1155D + 1) % M32
def next_byte(self) -> int:
self._step()
idx = self.s3 & 0xFF
out = self.table[idx]
self.table[idx] = (self.s1 + self.s2) % M32
return out & 0xFF
def keystream(self, n: int) -> bytes:
return bytes([self.next_byte() for _ in range(n)])
def unscramble(body: bytes) -> bytes:
"""XOR-decrypt the container body (everything after the 8-byte magic)."""
ks = KryptPRNG().keystream(len(body))
return bytes(c ^ k for c, k in zip(body, ks, strict=True))
# The layer is symmetric, but keep both names so call sites read correctly.
scramble = unscramble