mirror of
https://gitea.tendokyu.moe/beerpsi/x.git
synced 2024-11-24 07:10:11 +01:00
makesegafs
This commit is contained in:
parent
20c41426c6
commit
2f654ec64f
@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
Miscellaneous scripts that don't need their own repo.
|
Miscellaneous scripts that don't need their own repo.
|
||||||
|
|
||||||
|
## `app`/`opt`/`pack`
|
||||||
|
- `segafs/makesegafs.py`: Create an `app`/`opt`/`pack` (same thing) from an unencrypted
|
||||||
|
disk image.
|
||||||
|
- Dependencies: `pip install construct PyCryptodome`
|
||||||
|
- You will need to find the BootID key/IV and the HMAC key yourself if you want to
|
||||||
|
make fake apps for whatever reason. Or just don't.
|
||||||
|
|
||||||
## Patchers
|
## Patchers
|
||||||
- `patchers/b2spatch.mjs`: Convert from BemaniPatcher to Spice2x JSON
|
- `patchers/b2spatch.mjs`: Convert from BemaniPatcher to Spice2x JSON
|
||||||
- Requires `cheerio`: `npm i cheerio`
|
- Requires `cheerio`: `npm i cheerio`
|
||||||
|
218
segafs/makesegafs.py
Normal file
218
segafs/makesegafs.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
from math import ceil
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Hash import HMAC, SHA1
|
||||||
|
from construct import Bytes, Const, Int16ul, Int32ul, Int64ul, Int8ul, Struct
|
||||||
|
|
||||||
|
# ---- Configuration
|
||||||
|
ENCRYPTION_KEY = bytes.fromhex("")
|
||||||
|
INPUT_FILE = "" # Should not be encrypted.
|
||||||
|
OUTPUT_FILE = ""
|
||||||
|
BOOTID = {
|
||||||
|
"type": 0x0201, # 0x0201: Option, 0x0000: Pack, 0x0101: App
|
||||||
|
"sequence_number": 0x0000, # Set to 256 (0x0100) for options.
|
||||||
|
"game_id": b"SDGS",
|
||||||
|
"game_timestamp": {
|
||||||
|
"year": 2023,
|
||||||
|
"month": 12,
|
||||||
|
"day": 28,
|
||||||
|
"hour": 16,
|
||||||
|
"minute": 34,
|
||||||
|
"second": 43,
|
||||||
|
"milli": 0,
|
||||||
|
},
|
||||||
|
"game_version": {
|
||||||
|
"release": 0,
|
||||||
|
"minor": 30,
|
||||||
|
"major": 1,
|
||||||
|
},
|
||||||
|
"block_size": 0x40000,
|
||||||
|
"header_block_count": 8,
|
||||||
|
"unk1": 0,
|
||||||
|
"hw_family": b"ACA",
|
||||||
|
"hw_generation": 0,
|
||||||
|
"org_timestamp": {
|
||||||
|
"year": 0,
|
||||||
|
"month": 0,
|
||||||
|
"day": 0,
|
||||||
|
"hour": 0,
|
||||||
|
"minute": 0,
|
||||||
|
"second": 0,
|
||||||
|
"milli": 0,
|
||||||
|
},
|
||||||
|
"org_version": {
|
||||||
|
"release": 0,
|
||||||
|
"minor": 0,
|
||||||
|
"major": 0,
|
||||||
|
},
|
||||||
|
"os_version": {
|
||||||
|
"release": 1,
|
||||||
|
"minor": 54,
|
||||||
|
"major": 80,
|
||||||
|
},
|
||||||
|
"strings": b"\x00" * 0x27AC, # Depending on the app/opt/pack, this might have some text in it.
|
||||||
|
}
|
||||||
|
# ----
|
||||||
|
|
||||||
|
# ---- Keys
|
||||||
|
# The BootID (app/opt/pack header) encryption key and IV.
|
||||||
|
BTKEY = bytes.fromhex("")
|
||||||
|
BTIV = bytes.fromhex("")
|
||||||
|
|
||||||
|
# The HMAC key that ensures the app/opt/pack created is authentic.
|
||||||
|
SIGKEY = bytes.fromhex("")
|
||||||
|
# ----
|
||||||
|
|
||||||
|
# ---- Constants. Don't edit.
|
||||||
|
EXFAT_HEADER = bytes.fromhex("EB769045584641542020200000000000")
|
||||||
|
OPTION_IV = bytes.fromhex("C063BF6F562D084D7963C987F5281761")
|
||||||
|
# ----
|
||||||
|
|
||||||
|
# ---- Script
|
||||||
|
Timestamp = Struct(
|
||||||
|
"year" / Int16ul,
|
||||||
|
"month" / Int8ul,
|
||||||
|
"day" / Int8ul,
|
||||||
|
"hour" / Int8ul,
|
||||||
|
"minute" / Int8ul,
|
||||||
|
"second" / Int8ul,
|
||||||
|
"milli" / Int8ul,
|
||||||
|
)
|
||||||
|
|
||||||
|
Version = Struct(
|
||||||
|
"release" / Int8ul,
|
||||||
|
"minor" / Int8ul,
|
||||||
|
"major" / Int16ul,
|
||||||
|
)
|
||||||
|
|
||||||
|
BootID = Struct(
|
||||||
|
"length" / Const(0x2800, Int32ul),
|
||||||
|
"magic" / Const(b"BTID", Bytes(4)),
|
||||||
|
"type" / Int16ul,
|
||||||
|
"sequence_number" / Int16ul,
|
||||||
|
"game_id" / Bytes(4),
|
||||||
|
"game_timestamp" / Timestamp,
|
||||||
|
"game_version" / Version,
|
||||||
|
"block_count" / Int64ul,
|
||||||
|
"block_size" / Int64ul,
|
||||||
|
"header_block_count" / Int64ul,
|
||||||
|
"unk1" / Int64ul,
|
||||||
|
"hw_family" / Bytes(3),
|
||||||
|
"hw_generation" / Int8ul,
|
||||||
|
"org_timestamp" / Timestamp,
|
||||||
|
"org_version" / Version,
|
||||||
|
"os_version" / Version,
|
||||||
|
"strings" / Bytes(0x27AC),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_page_iv(iv: bytes, offset: int):
|
||||||
|
return bytes(x ^ (offset >> (8 * (i % 8))) & 0xFF for (i, x) in enumerate(iv))
|
||||||
|
|
||||||
|
|
||||||
|
iv = secrets.token_bytes(16)
|
||||||
|
|
||||||
|
if BOOTID["type"] == 0x0201:
|
||||||
|
iv = bytes(x ^ EXFAT_HEADER[i] ^ OPTION_IV[i] for (i, x) in enumerate(iv))
|
||||||
|
|
||||||
|
print(f"Generated IV: {iv.hex()}")
|
||||||
|
|
||||||
|
filesize = os.stat(INPUT_FILE).st_size
|
||||||
|
BOOTID["block_count"] = ceil(filesize / BOOTID["block_size"]) + 8
|
||||||
|
|
||||||
|
bullshit = secrets.token_bytes(BOOTID["block_size"])
|
||||||
|
bullshit_crc32 = zlib.crc32(bullshit)
|
||||||
|
block_crc32s = [0, bullshit_crc32, bullshit_crc32, bullshit_crc32, bullshit_crc32, bullshit_crc32, bullshit_crc32, bullshit_crc32]
|
||||||
|
|
||||||
|
with open(INPUT_FILE, "rb") as fin, open(OUTPUT_FILE, "w+b") as fout:
|
||||||
|
# Write the bootID.
|
||||||
|
cipher = AES.new(BTKEY, AES.MODE_CBC, BTIV)
|
||||||
|
bootid = BootID.build(BOOTID)
|
||||||
|
bootid_crc32 = zlib.crc32(bootid)
|
||||||
|
bootid_bytes = cipher.encrypt(struct.pack("<I", bootid_crc32) + bootid)
|
||||||
|
|
||||||
|
_ = fout.write(bootid_bytes)
|
||||||
|
|
||||||
|
# Ignore the CRC32s and the signature for now, we'll come back later.
|
||||||
|
# We'll generate random bytes for them though.
|
||||||
|
_ = fout.write(secrets.token_bytes(BOOTID["block_size"] - 0x2800))
|
||||||
|
|
||||||
|
# Bullshit out 7 random blocks for the header.
|
||||||
|
for i in range(BOOTID["header_block_count"] - 1):
|
||||||
|
_ = fout.write(bullshit)
|
||||||
|
|
||||||
|
# Encrypt the contents of the file.
|
||||||
|
total_written = 0
|
||||||
|
to_read = filesize
|
||||||
|
block_crc32 = 0
|
||||||
|
|
||||||
|
while to_read > 0:
|
||||||
|
page_iv = get_page_iv(iv, total_written)
|
||||||
|
contents = fin.read(4096)
|
||||||
|
contents_len = len(contents)
|
||||||
|
|
||||||
|
to_read -= contents_len
|
||||||
|
|
||||||
|
if contents_len < 4096:
|
||||||
|
contents += b"\x00" * (4096 - contents_len)
|
||||||
|
|
||||||
|
cipher = AES.new(ENCRYPTION_KEY, AES.MODE_CBC, page_iv)
|
||||||
|
encrypted = cipher.encrypt(contents)
|
||||||
|
total_written += fout.write(encrypted)
|
||||||
|
block_crc32 = zlib.crc32(encrypted, block_crc32)
|
||||||
|
|
||||||
|
if (total_written % BOOTID["block_size"]) == 0:
|
||||||
|
block_crc32s.append(block_crc32)
|
||||||
|
block_crc32 = 0
|
||||||
|
|
||||||
|
# Pad with null bytes if we have an unfinished block.
|
||||||
|
if (total_written % BOOTID["block_size"]) != 0:
|
||||||
|
null_byte_count = BOOTID["block_size"] - (total_written % BOOTID["block_size"])
|
||||||
|
|
||||||
|
while null_byte_count > 0:
|
||||||
|
page_iv = get_page_iv(iv, total_written)
|
||||||
|
cipher = AES.new(ENCRYPTION_KEY, AES.MODE_CBC, page_iv)
|
||||||
|
encrypted = cipher.encrypt(b"\x00" * 4096)
|
||||||
|
total_written += fout.write(encrypted)
|
||||||
|
block_crc32 = zlib.crc32(encrypted, block_crc32)
|
||||||
|
|
||||||
|
null_byte_count -= 4096
|
||||||
|
|
||||||
|
block_crc32s.append(block_crc32)
|
||||||
|
block_crc32 = 0
|
||||||
|
|
||||||
|
# We can finally revisit the CRC32s.
|
||||||
|
_ = fout.seek(0x2A04)
|
||||||
|
|
||||||
|
for crc32 in block_crc32s[1:]:
|
||||||
|
_ = fout.write(struct.pack("<I", crc32))
|
||||||
|
|
||||||
|
_ = fout.seek(0)
|
||||||
|
block_0_crc32 = zlib.crc32(fout.read(0x2800))
|
||||||
|
|
||||||
|
# Skip the HMAC signature and the first CRC32, which we're trying to calculate.
|
||||||
|
_ = fout.seek(0x204, os.SEEK_CUR)
|
||||||
|
block_0_crc32 = zlib.crc32(fout.read(BOOTID["block_size"] - 0x2800 - 0x204), block_0_crc32)
|
||||||
|
|
||||||
|
_ = fout.seek(0x2A00)
|
||||||
|
_ = fout.write(struct.pack("<I", block_0_crc32))
|
||||||
|
|
||||||
|
# Calculate the HMAC signature.
|
||||||
|
_ = fout.seek(0x2A00)
|
||||||
|
hmac = HMAC.new(SIGKEY, digestmod=SHA1)
|
||||||
|
to_read = BOOTID["block_size"] * 8 - 0x2A00
|
||||||
|
|
||||||
|
while to_read > 0:
|
||||||
|
block = fout.read(min(0x1000, to_read))
|
||||||
|
if len(block) == 0:
|
||||||
|
raise Exception("Ran out of data making the signature.")
|
||||||
|
|
||||||
|
_ = hmac.update(block)
|
||||||
|
to_read -= len(block)
|
||||||
|
|
||||||
|
_ = fout.seek(0x2800)
|
||||||
|
_ = fout.write(hmac.digest())
|
||||||
|
|
Loading…
Reference in New Issue
Block a user