firebeat/utils/dongle_dump_utils

341 lines
12 KiB
Python
Executable File

#! /usr/bin/env python3
import argparse
import os
import sys
from typing import Dict, List
from arcadeutils.binary import BinaryDiff
from firebeat import FirebeatExe
donglepasswords: Dict[str, Dict[str, List[bytes]]] = {
# BMIII
'972': {
'ids': [b'bm3Secu1', b'bm3Secu2', b'bm3Secu3'],
'passwords': [b'Fragile-', b'CloseToT', b'heEdgeAn'],
},
# BMIII Core Remix
'A05': {
'ids': [b'bm3Secu1', b'bm3Secu2', b'bm3Secu3'],
'passwords': [b'AndYouAn', b'dI-LADDE', b'R-talk-o'],
},
# BMIII 6th Mix
'A21': {
'ids': [b'bm3Secu1', b'bm3Secu2', b'bm3Secu3'],
'passwords': [b'ONE-TACT', b'KANNON-K', b'AIR-KEY!'],
},
# BMIII 7th Mix
'B07': {
'ids': [b'bm3Secu1', b'bm3Secu2', b'bm3Secu3'],
'passwords': [b'EQ-2-ROK', b'EQ-3-SOV', b'EQ-4-SOL'],
},
# BMIII Final
'C01': {
'ids': [b'bm3Secu1', b'bm3Secu2', b'bm3Secu3'],
'passwords': [b'PutIPmuP', b'JDoTysaE', b'PuCDlRoW'],
},
# Keyboard Heaven and Keyboard Mania 1stMIX
'974': {
'ids': [b'KonamiKe', b'yboardMa', b'niaGQ974'],
'passwords': [b'FullScal', b'eKeyboar', b'dSimulat'],
},
# Keyboard Mania 2nd Mix
'A01': {
'ids': [b'Secur001', b'Secur002', b'Secur003'],
'passwords': [b'm@1QlakI', b'pUre4eta', b'nkmrTYUN'],
},
# Keyboard Mania 3rd Mix
'A12': {
'ids': [b'Secur001', b'Secur002', b'Secur003'],
'passwords': [b'RoboHELP', b'1A400060', b'!uoykcuf'],
},
# Pop'n Music Mickey Tunes Regular and Update Revision
'976': {
'ids': [b'iDCec001', b'iDCec002', b'iDCec003'],
'passwords': [b'IdpAS001', b'IdpAS002', b'IdpAS003'],
},
# Pop'n Music Animelo
'987': {
'ids': [b'GQ987ID1', b'GQ987ID2', b'GQ987ID3'],
'passwords': [b'HisJs0sN', b'H3TfkjsU', b'Mdfiu6IH'],
},
# Pop'n Music Animelo 2
'A02': {
'ids': [b'A02ID000', b'A02ID001', b'A02ID002'],
'passwords': [b'A02PS000', b'A02PS001', b'A02PS002'],
},
# Pop'n Music 4
'986': {
'ids': [b'Secur001', b'Secur002', b'Secur003'],
'passwords': [b'thanx2re', b'versEng.', b'ths_WARN'],
},
# Pop'n Music 5
'A04': {
'ids': [b'Secur001', b'Secur002', b'Secur003'],
'passwords': [b'THANX2RE', b'VERSeNG.', b'THS_warn'],
},
# Pop'n Music 6
'A16': {
'ids': [b'iamlxhwe', b'fi,ahfil', b'amwhgaci'],
'passwords': [b'alxwhfex', b'mhaiwe,c', b'aiwx,hai'],
},
# Pop'n Music 7
'B00': {
'ids': [b'aweklcfy', b'iwaerioa', b'cnwrcawg'],
'passwords': [b'limsuryl', b'tvnaesir', b'utvoiaew'],
},
# Pop'n Music 8
'B30': {
'ids': [b'IZCDMNKX', b'CNIDEZOV', b'SXKDWHWT'],
'passwords': [b'MHQZOTAR', b'CVWZGFEP', b'SFCVYRIN'],
},
# ParaParaParadise 1.0, 1.1 and ParaParaDancing
'977': {
'ids': [b'&Natsumi', b'Yu3minaZ', b'zxZ:cZxc'],
'passwords': [b'Beekids#', b'Bunbun01', b'Chamber*'],
},
# ParaParaParadise 1stMIX Plus
'A11': {
'ids': [b'&hshiuwg', b'iwo;2_90', b'1YU:Ohs7'],
'passwords': [b'Ecuador#', b'JLKeiu30', b'J8923G21'],
},
}
# Stupid little inline data validator.
for pw, items in donglepasswords.items():
if len(items['ids']) != 3:
raise Exception(f"Bad number of IDs for {pw}!")
if len(items['passwords']) != 3:
raise Exception(f"Bad number of IDs for {pw}!")
for idx in items['ids']:
if len(idx) != 8:
raise Exception(f"Bad ID length for {pw}!")
for idx in items['passwords']:
if len(idx) != 8:
raise Exception(f"Bad password length for {pw}!")
# CRC utility matching Dallas DS1991 ROM ID.
def crc8(data: bytes) -> int:
# Start with 0x00, polynomial X^8 + X^5 + X^4 + 1
crc = 0x0
poly = 0x118
for byte in data:
for bit in range(8):
if bool((crc ^ byte) & 0x01):
crc ^= poly
crc >>= 1
byte >>= 1
return crc & 0xFF
def main() -> int:
# Create the argument parser
parser = argparse.ArgumentParser(
description="Utility to streamline dumping dongles on a Firebeat.",
epilog=(
f"Currently supports working with a modified BMIII The Final image to dump dongles on "
f"an actual firebeat. The following games are supported:{os.linesep}"
f" - 972 - BeatmaniaIII{os.linesep}"
f" - A05 - BeatmaniaIII Core Remix{os.linesep}"
f" - A21 - BeatmaniaIII 6th Mix{os.linesep}"
f" - B07 - BeatmaniaIII 7th Mix{os.linesep}"
f" - C01 - BeatmaniaIII The Final{os.linesep}"
f" - 974 - Keyboard Mania 1stMIX and Keyboard Heaven{os.linesep}"
f" - A01 - Keyboard Mania 2ndMIX{os.linesep}"
f" - A12 - Keyboard Mania 3rdMIX{os.linesep}"
f" - 976 - Pop'n Music Mickey Tunes{os.linesep}"
f" - 987 - Pop'n Music Animelo{os.linesep}"
f" - A02 - Pop'n Music Animelo 2{os.linesep}"
f" - 986 - Pop'n Music 4{os.linesep}"
f" - A04 - Pop'n Music 5{os.linesep}"
f" - A16 - Pop'n Music 6{os.linesep}"
f" - B00 - Pop'n Music 7{os.linesep}"
f" - B30 - Pop'n Music 8{os.linesep}"
f" - 977 - ParaParaParadise, ParaParaParadise 1.1 and ParaParaDancing {os.linesep}"
f" - A11 - ParaParaParadise 1stMIX Plus{os.linesep}"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(help='commands', dest='command',)
# Parser for executable checking
check_parser = subparsers.add_parser('check', help='Check a Firebeat exe to see if it is set up properly for dongle dumping.')
check_parser.add_argument(
'exe',
metavar='EXE',
type=str,
help='The EXE file (FIREBEAT.EXE or HIKARU.EXE) that we should check.',
)
check_parser.add_argument(
'--patch-file',
metavar='FILE',
type=str,
default=os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../patches/dongledumper.patch')),
help='Path to the dongledumper.patch file if it cannot be found automatically.',
)
# Parser for executable password updating
password_parser = subparsers.add_parser('password', help='Update a Firebeat exe to contain the correct passwords for dumping a dongle.')
password_parser.add_argument(
'exe',
metavar='EXE',
type=str,
help='The EXE file (FIREBEAT.EXE or HIKARU.EXE) that we should update the passwords for.',
)
password_parser.add_argument(
'--game',
type=str,
default=None,
choices=list(x for x in donglepasswords),
help='The game that we want to dump the dongle of.',
)
# Parser for dongle validation
validate_parser = subparsers.add_parser('validate', help='Check a Firebeat dongle dump to see if it appears correct.')
validate_parser.add_argument(
'dongle',
metavar='DONGLE',
type=str,
help='The dongle binary file that we should validate.',
)
# Grab what we're doing
args = parser.parse_args()
if args.command == 'check':
with open(args.patch_file, "r") as fp:
differences = fp.readlines()
differences = [d.strip() for d in differences if d.strip()]
try:
with open(args.exe, "rb") as fp:
data = fp.read()
unpacked = FirebeatExe.exe_to_raw(data, is_ppp=False)
except Exception as e:
print(f"Failed to unpack {args.exe}: {str(e)}", file=sys.stderr)
return 1
patched = False
unpatched = False
try:
BinaryDiff.patch(unpacked, differences, reverse=False)
unpatched = True
except Exception:
# It wasn't pristine.
pass
try:
BinaryDiff.patch(unpacked, differences, reverse=True)
patched = True
except Exception:
# It wasn't patched.
pass
if not patched and not unpatched:
print(f"{args.exe} is unknown, possibly the wrong game binary or patched with an older version of dongledumper.patch?")
elif patched and not unpatched:
print(f"{args.exe} is patched for dongle dumping!")
# Now, check the password fields and see what game it is for.
ids = unpacked[0x94894:0x948AC]
passwords = unpacked[0x948AC:0x948C4]
recognized = False
for game, expected in donglepasswords.items():
if ids != b''.join(expected['ids']):
continue
if passwords != b''.join(expected['passwords']):
continue
print(f"Passwords can be used for {game} dongles!")
recognized = True
if not recognized:
print("Passwords are not recognized!")
elif not patched and unpatched:
print(f"{args.exe} is unpatched. You can patch it by running `./utils/exe_utils patch {args.exe} {args.exe}.patched --patch-file {args.patch_file}`")
else:
raise Exception("Logic error!")
elif args.command == 'password':
try:
with open(args.exe, "rb") as fp:
data = fp.read()
unpacked = FirebeatExe.exe_to_raw(data, is_ppp=False)
except Exception as e:
print(f"Failed to unpack {args.exe}: {str(e)}", file=sys.stderr)
return 1
if not args.game:
raise Exception("Please provide a game to dump dongles for!")
ids = b''.join(donglepasswords[args.game]['ids'])
passwords = b''.join(donglepasswords[args.game]['passwords'])
new = unpacked[:0x94894] + ids + unpacked[0x948AC:]
new = new[:0x948AC] + passwords + new[0x948C4:]
if len(new) != len(unpacked):
raise Exception("Logic error, bad IDs or passwords in data!")
packed = FirebeatExe.raw_to_exe(new, is_ppp=False)
with open(args.exe, "wb") as fp:
fp.write(packed)
print(f"{args.exe} updated to dump {args.game} dongles!")
elif args.command == 'validate':
with open(args.dongle, "rb") as fp:
data = fp.read()
print(f"Examining {args.dongle}...")
# 3 Enclaves consisting of an 8-byte ID, 8-byte password and 48 byte enclave.
# A single ROM ID at the end of the file.
if len(data) != (8 + (3 * (48 + 8 + 8))):
print('Dongle file is the wrong length!')
return 1
# Validate the last 8 bytes
if data[-1] != 0x02:
print('ROM ID product code should be 0x02!')
return 1
# Validate the CRC (should get 0x00)
if crc8(data[-8:][::-1]) != 0x00:
print('CRC for last 8 bytes is not correct!')
return 1
# Figure out IDs and passwords
ids = data[0:8] + data[64:72] + data[128:136]
passwords = data[8:16] + data[72:80] + data[136:144]
recognized = False
for game, expected in donglepasswords.items():
if ids != b''.join(expected['ids']):
continue
if passwords != b''.join(expected['passwords']):
continue
print(f"Dongle can be used for {game}!")
recognized = True
if not recognized:
print("The game this dongle goes to is unrecognized!")
return 1
print("This dongle looks good!")
return 0
else:
print("Please specify a valid command!", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())