import struct import zlib import re import io import os from dataclasses import dataclass from Crypto.Cipher import AES SOURCE = "./kcf" OUT = "templates/pages/sega/software/security/" APPDATA_BINARY_LEN = 288 # RingEdge APPDATA_BINARY_LEN2 = 416 # RingEdge 2 APPDATA_NU_BINARY_LEN = 112 # Nu FUNCTION_TYPE = { 1: "Server (SV)", 2: "Satellite (ST)", 3: "Live (LV)", 4: "Terminal (??)", 5: "UNKNOWN:5", 11: "UNKNOWN:11", 12: "UNKNOWN:12", 21: "UNKNOWN:21", 22: "UNKNOWN:22", 98: "Standalone (??)", } PLATFORM = { "AAM": "RingWide", "AAL": "RingEdge", "AAS": "RingEdge 2", "AAR": "ELEFUN", "AAV": "Nu", "AAZ": "Nu Lv 3.1", "AAW": "Nu SX", "ACA": "ALLS X", "ACB": "ALLS X2", } SF_DEVELOP = 1 << 0 SF_ALL_NET = 1 << 2 SF_DELIVER = 1 << 3 SF_BINDING = 1 << 4 SF_BILLING = 1 << 5 SF_RENTAL = 1 << 6 class Stream: def __init__(self, stream): self.stream = stream def read(self, n): return self.stream.read(n) def seek(self, n): self.stream.seek(n) def crc32(self, nbytes): start = self.stream.tell() data = self.stream.read(nbytes) self.stream.seek(start) return zlib.crc32(data) & 0xffffffff def skip(self, n): self.read(n) def unpack(self, fmt): return struct.unpack("<" + fmt, self.stream.read(struct.calcsize(fmt))) def u8(self): return self.unpack("B")[0] def u16(self): return self.unpack("H")[0] def u32(self): return self.unpack("I")[0] def ipv4(self): return self.unpack("4B") def ipv6(self): return self.unpack("8H") def str(self, n): return self.unpack(f"{n}s")[0].decode("latin-1") class AppData: system_flag: int function_type: int region: int platform_id: str def parse_region(self): if self.region == 0xFF: return "ALL" regions = [] if self.region & 1: regions.append("JPN") if self.region & 2: regions.append("USA") if self.region & 4: regions.append("EXP") if self.region & 8: regions.append("CHN") if self.region & ~(1 | 2 | 4 | 8): regions.append(f"ex:{self.region}") return "/".join(regions) def parse_system_flag(self): """ [bit 0] Develop: 1: Key chip for development 0: Key chip for mass production [bit 2] ALL.Net: 1: ON (use) 0: OFF (do not use) [bit 3] Deliver (LAN installation): 1: Local FDC Server 0: Client Ethernet network: Used & Mode: 1 only when server [bit 4] Binding: 1: ON 0: OFF * Not used in ELEFUN (= 0) (cannot be set as = 1) [bit 5] Billing: Requires ALL.Net to be ON 1: ON 0: OFF (no charge) * Not used in ELEFUN (= 0) (cannot be set as = 1) [bit 6] Rental (P-ras charging): Requires ALL.Net to be ON Requires Billing to be ON * Not used in ELEFUN (= 0) (cannot be set as = 1) 1: P-ras billing (rental billing) 0: pay-as-you-go billing (net billing) """ parsed = [] if self.system_flag & 1: parsed.append("Develop") if self.system_flag & 2: parsed.append("2") if self.system_flag & 4: parsed.append("ALL.Net") if self.system_flag & 8: parsed.append("Deliver") if self.system_flag & 16: parsed.append("Binding") if self.system_flag & 32: parsed.append("Billing") if self.system_flag & 64: parsed.append("Rental") if self.system_flag & 128: parsed.append("128") return ",".join(parsed) def parse_function_type(self): return FUNCTION_TYPE[self.function_type] def parse_platform_id(self): return f"{PLATFORM[self.platform_id]} ({self.platform_id})" @dataclass class AppDataRing(AppData): # Appboot format_type: int game_id: str region: int function_type: int system_flag: int platform_id: str dvd_flag: int network_addr: tuple[int] # Misc app_data: bytes # Crypto key: bytes iv: bytes seed: bytes keyfile: bytes # Misc (2) extra: bytes @dataclass class AppDataNu(AppData): # Appboot format_type: int game_id: str region: int function_type: int system_flag: int billing_form: int platform_id: str network_addr: tuple[int] network_addr_6: tuple[int] # Crypto gkey: bytes giv: bytes okey: bytes oiv: bytes def scramble(val, a, b, c, d): ret = bytearray(val) ret[a], ret[b] = ret[b], ret[a] ret[c], ret[d] = ret[d], ret[c] return bytes(ret) def parse_appdata_ring(stream: Stream): crc = stream.u32() # idk how to make this pass. it doesn't match? format_type = stream.u8() stream.skip(3) game_id = stream.str(4) region = stream.u8() function_type = stream.u8() system_flag = stream.u8() stream.skip(1) platform_id = stream.str(3) dvd_flag = stream.u8() network_addr = stream.ipv4() app_data = stream.read(216) seed = scramble(stream.read(16), 1, 8, 12, 15) key = scramble(stream.read(16), 0, 4, 2, 14) iv = scramble(stream.read(16), 0, 11, 5, 15) # AAL, AAS, AAR has: extra = stream.read(128) # For AAR, extra=FF....FF # if extra and platform_id != 'AAR': # print(extra[:64].hex(), platform_id, game_id, function_type) cipher = AES.new(key, AES.MODE_CBC, iv) keyfile = cipher.encrypt(seed) return AppDataRing( format_type, game_id, region, function_type, system_flag, platform_id, dvd_flag, network_addr, app_data, key, iv, seed, keyfile, extra ) def parse_appdata_nu(stream: Stream): crc = stream.u32() format_type = stream.u8() stream.skip(3) game_id = stream.str(4) region = stream.u8() function_type = stream.u8() system_flag = stream.u8() billing_form = stream.u8() platform_id = stream.str(3) stream.skip(1) network_addr = stream.ipv4() stream.skip(8) ipv6_addr = stream.ipv6() gkey = stream.read(16) giv = stream.read(16) okey = stream.read(16) oiv = stream.read(16) return AppDataNu( format_type, game_id, region, function_type, system_flag, billing_form, platform_id, network_addr, ipv6_addr, gkey, giv, okey, oiv ) def format_row(*values, elem="td"): return "
{code}
"
def parse_kcf(data):
kcf = io.BytesIO(data)
nbytes = len(data)
if nbytes == APPDATA_BINARY_LEN:
return parse_appdata_ring(Stream(kcf)), None
elif nbytes == APPDATA_BINARY_LEN2:
return parse_appdata_ring(Stream(kcf)), None
elif nbytes == APPDATA_NU_BINARY_LEN:
return None, parse_appdata_nu(Stream(kcf))
else:
raise ValueError
STYLES = """
"""
def gen_file_ring(path: str, platform: str, appdata: list[AppDataRing]):
with open(path, "w") as kcf_html:
print(STYLES, file=kcf_html)
print("