454 lines
12 KiB
Python
454 lines
12 KiB
Python
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 "<tr>" + "".join(f"<{elem}>{i}</{elem}>" for i in values) + "</tr>"
|
|
|
|
|
|
def format_code(code):
|
|
return f"<code>{code}</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 = """
|
|
<style>
|
|
body {
|
|
font-family: sans-serif;
|
|
}
|
|
table {
|
|
white-space: nowrap;
|
|
overflow-x: auto;
|
|
border-collapse: collapse;
|
|
}
|
|
th {
|
|
border-bottom: 3px solid #000;
|
|
position: sticky;
|
|
top: 0;
|
|
text-align: left;
|
|
background: #fff;
|
|
}
|
|
td {
|
|
border-bottom: 1px solid #ddd;
|
|
}
|
|
th, td {
|
|
border-left: 1px solid #ddd;
|
|
}
|
|
th:last-child, td:last-child {
|
|
border-right: 1px solid #ddd;
|
|
}
|
|
th, td {
|
|
margin: 0;
|
|
padding: 2px 4px;
|
|
}
|
|
</style>
|
|
"""
|
|
|
|
|
|
def gen_file_ring(path: str, platform: str, appdata: list[AppDataRing]):
|
|
with open(path, "w") as kcf_html:
|
|
print(STYLES, file=kcf_html)
|
|
print("<table><thead>", file=kcf_html)
|
|
print(format_row(
|
|
"Game ID", "Region", "Model Type",
|
|
"Develop Flag", "ALL.Net", "Deliver", "Binding", "Billing", "Rental",
|
|
"DVD Enabled", "Platform ID", "IPv4 Subnet",
|
|
"AES Key", "AES IV", "AppBoot Seed", "Keyfile",
|
|
"App Data", "Trailing Data",
|
|
elem="th"
|
|
), file=kcf_html)
|
|
print("</thead></tbody>", file=kcf_html)
|
|
appdata.sort(key=lambda x: (x.platform_id, x.network_addr, x.game_id, x.format_type, x.system_flag))
|
|
|
|
for i in appdata:
|
|
if i.platform_id != platform:
|
|
continue
|
|
|
|
print(format_row(
|
|
i.game_id,
|
|
i.parse_region(),
|
|
i.parse_function_type(),
|
|
"X" if i.system_flag & SF_DEVELOP else "",
|
|
"X" if i.system_flag & SF_ALL_NET else "",
|
|
"X" if i.system_flag & SF_DELIVER else "",
|
|
"X" if i.system_flag & SF_BINDING else "",
|
|
"X" if i.system_flag & SF_BILLING else "",
|
|
"X" if i.system_flag & SF_RENTAL else "",
|
|
"X" if i.dvd_flag else "",
|
|
i.parse_platform_id(),
|
|
".".join(map(str, i.network_addr)),
|
|
format_code(i.key.hex().upper()),
|
|
format_code(i.iv.hex().upper()),
|
|
format_code(i.seed.hex().upper()),
|
|
format_code(i.keyfile.hex().upper()),
|
|
""
|
|
if all(j == 0 for j in i.app_data)
|
|
else format_code(i.app_data.hex().upper()),
|
|
format_code(i.extra.hex().upper()),
|
|
), file=kcf_html)
|
|
print("</tbody></table>", file=kcf_html)
|
|
|
|
|
|
def main():
|
|
appdata_ring: list[AppDataRing] = []
|
|
appdata_nu: list[AppDataNu] = []
|
|
|
|
raw = set()
|
|
for i in os.listdir(SOURCE):
|
|
raw.add(open(os.path.join(SOURCE, i), "rb").read())
|
|
|
|
for i in raw:
|
|
ring, nu = parse_kcf(i)
|
|
|
|
if ring:
|
|
appdata_ring.append(ring)
|
|
if nu:
|
|
appdata_nu.append(nu)
|
|
|
|
gen_file_ring(OUT + "kcf-ringedge.html", "AAL", appdata_ring)
|
|
gen_file_ring(OUT + "kcf-ringwide.html", "AAM", appdata_ring)
|
|
gen_file_ring(OUT + "kcf-ringedge2.html", "AAS", appdata_ring)
|
|
gen_file_ring(OUT + "kcf-elefun.html", "AAR", appdata_ring)
|
|
|
|
with open(OUT + "kcf-nu.html", "w") as kcf_nu:
|
|
print(STYLES, file=kcf_nu)
|
|
print("<table><thead>", file=kcf_nu)
|
|
print(format_row(
|
|
"Game ID", "Region", "Model Type",
|
|
"Develop Flag", "ALL.Net", "Deliver", "Binding", "Billing", "Rental",
|
|
"Billing Form", "Platform ID",
|
|
"IPv4 Subnet", "IPv6 Subnet",
|
|
"Game Key", "Game IV", "Opt Key", "Opt IV",
|
|
elem="th"
|
|
), file=kcf_nu)
|
|
print("</thead></tbody>", file=kcf_nu)
|
|
appdata_nu.sort(key=lambda x: (x.platform_id, x.network_addr, x.game_id, x.format_type, x.system_flag))
|
|
|
|
for i in appdata_nu:
|
|
ipv6 = ":".join(f"{j:04x}" for j in i.network_addr_6)
|
|
ipv6 = re.sub(r"/(^(0000:?)+)|(:(0000:)+)|((:0000)+$)/", "::", ipv6)
|
|
|
|
print(format_row(
|
|
i.game_id,
|
|
i.parse_region(),
|
|
i.parse_function_type(),
|
|
"X" if i.system_flag & SF_DEVELOP else "",
|
|
"X" if i.system_flag & SF_ALL_NET else "",
|
|
"X" if i.system_flag & SF_DELIVER else "",
|
|
"X" if i.system_flag & SF_BINDING else "",
|
|
"X" if i.system_flag & SF_BILLING else "",
|
|
"X" if i.system_flag & SF_RENTAL else "",
|
|
i.billing_form,
|
|
i.parse_platform_id(),
|
|
".".join(map(str, i.network_addr)),
|
|
ipv6,
|
|
format_code(i.gkey.hex().upper()),
|
|
format_code(i.giv.hex().upper()),
|
|
format_code(i.okey.hex().upper()),
|
|
format_code(i.oiv.hex().upper()),
|
|
), file=kcf_nu)
|
|
print("</tbody></table>", file=kcf_nu)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|