"""Decrypt a TuneECU Triumph map file (the download / distribution format). Reverse-engineered from com/tuneecu/l.java `dc()`, the routine p8() calls on any map that isn't a raw ROM-dump size. It is a self-synchronising CBC-style XOR stream cipher, seeded by the (unencrypted) 4-byte header: i5 = header[3] - 24 i2 = {0,1: 0x80808080, 2: 0x84808081, 3: 0x87848284, 87: ...}[i5] key32 = i2 | le_u32(header[0:4]) # header seeds the keystream prev = 0 for i in range(4, len(buf)): # bytes 0..3 stay plaintext c = buf[i] ks = (key32 >> (((i-4) % 4) * 8)) & 0xFF buf[i] = prev ^ c ^ ks # decode prev = c # feedback = ciphertext VERIFIED: decoding 20187Map.hex yields the plaintext strings "Bonneville", "Production silencers", "Mechanical odometer" at offsets 0x1E/0x29/0x3E, matching the map catalogue exactly. Entropy drops 7.99 -> ~6.1 bit/byte. Encode is the same with feedback = the freshly written (cipher) byte; pass encode=True. Usage: python3 decode_map.py 20187Map.hex 20187.dec.bin """ from __future__ import annotations import sys # i2 constant selected by (header[3] - 24), from l.java dc(). _I2 = {0: 0x80808080, 1: 0x80808080, 2: 0x84808081, 3: 0x87848284} def _i2_for(i5: int) -> int: if i5 in _I2: return _I2[i5] if i5 == 87: # the ((i5+3)<<24)|((i5+1)<<8)|i5|((i5+2)<<16) branch return ((i5 + 3) << 24) | ((i5 + 1) << 8) | i5 | ((i5 + 2) << 16) raise ValueError(f"unhandled header[3]-24 = {i5}; add its i2 constant from l.java") def transcode(buf: bytes, *, encode: bool = False) -> bytes: b = bytearray(buf) i5 = b[3] - 24 i2 = _i2_for(i5) key32 = (i2 | (b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24))) & 0xFFFFFFFF prev = 0 for i in range(4, len(b)): c = b[i] ks = (key32 >> (((i - 4) % 4) * 8)) & 0xFF out = prev ^ c ^ ks b[i] = out prev = out if encode else c return bytes(b) def decode(buf: bytes) -> bytes: return transcode(buf, encode=False) def encode(buf: bytes) -> bytes: return transcode(buf, encode=True) if __name__ == "__main__": if len(sys.argv) != 3: print("usage: decode_map.py ", file=sys.stderr) raise SystemExit(2) raw = open(sys.argv[1], "rb").read() dec = decode(raw) open(sys.argv[2], "wb").write(dec) # round-trip sanity: re-encoding must reproduce the original file assert encode(dec) == raw, "round-trip failed" strings = [ dec[o:o + n].decode("latin1") for o, n in ((0x1E, 10), (0x29, 20), (0x3E, 19)) ] print(f"decoded {len(dec)} bytes, round-trip OK") print("header strings:", " / ".join(s.strip() for s in strings))