diff --git a/segafs/makesegafs.py b/segafs/makesegafs.py index 1e1733f..2a4ae4d 100644 --- a/segafs/makesegafs.py +++ b/segafs/makesegafs.py @@ -1,256 +1,270 @@ -# pyright: reportMissingTypeStubs=false, reportOperatorIssue=false, reportUnknownArgumentType=false, reportUnknownMemberType=false -from math import ceil -import os -import secrets -import struct -import time -import zlib - -from Crypto.Cipher import AES, PKCS1_OAEP -from Crypto.Hash import HMAC, SHA1 -from Crypto.PublicKey import RSA -from construct import ( - Bytes, - Const, - IfThenElse, - 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": b"A041", - "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("09ca5efd30c9aaef3804d0a7e3fa7120") -BTIV = bytes.fromhex("b155c22c2e7f0491fa7f0fdc217aff90") -SIGKEY = bytes.fromhex( - "e1bdcb2d5e9ed3b5de234364dfa4d126849edff769fc6c28fba5f43bc482bd7479d676afce8188e1d3a6852f4ebce45cde46bd15e8ee5fe84d197f945a54518f" -) -HEADER_META_PUBKEY = RSA.import_key("""-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsRMLnJuczNpfoqPpHQ3o -5XNkjKXO6P3ToV/45Az5dNaHVL7uEu9vPI7a2KYFQnNYgD3UUHFahfTcljzLOkcH -1aVrhm8gaB/5mygjUJWcN+kKyB7sASqhL22RC7NlxtDY15ozli/b0MagVoaBAV5D -MytUCa73GPRGY0x9v/wTvtmFclYCWjJ9c2QzrCrQ9eNTVyETwh5q6qKEARHGZgCX -rWmdCsa/+oS+3pLbUGFlHCSZtCvvWCJgmgurlTGAGzoxrieO6XDEg2AGiRprWWL2 -BGNh7gwgnSq6FWnKSf2Qe7xoFcTpV5QhNFBQjrq0KnBDRfz5EXJnMoKxNYL6reqR -uwIDAQAB ------END PUBLIC KEY----- -""") -# ---- - -# ---- 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" / IfThenElse(lambda ctx: ctx.type == 0x0201, Bytes(4), 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 - -key = secrets.token_bytes(16) -iv = secrets.token_bytes(16) -encrypted_keypair = PKCS1_OAEP.new(HEADER_META_PUBKEY).encrypt(key + iv) -header_meta = struct.pack(" 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(" 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()) +# pyright: reportMissingTypeStubs=false, reportOperatorIssue=false, reportUnknownArgumentType=false, reportUnknownMemberType=false +from math import ceil +import os +import secrets +import struct +import time +import zlib + +from Crypto.Cipher import AES, PKCS1_OAEP +from Crypto.Hash import HMAC, SHA1 +from Crypto.PublicKey import RSA +from construct import ( + Bytes, + Const, + IfThenElse, + Int16ul, + Int32ul, + Int64ul, + Int8ul, + Struct, +) + +# ---- Configuration +ENCRYPTION_KEY = bytes.fromhex("") +INPUT_FILE = "" # Should not be encrypted. +OUTPUT_FILE = "" +BOOTID = { + "unk1": 0x01, # Ocassionally you'll see this be 0. + "type": 0x01, # 0: OS, 1: App, 2: Option + "sequence_number": 0, # Only applies to app patches. + + # If this is enabled, the container should be decrypted with a derived IV. The derivation algorithm is unknown. + "derive_iv": 0, + + "game_id": b"SDGS", + "game_timestamp": { + "year": 2023, + "month": 12, + "day": 28, + "hour": 16, + "minute": 34, + "second": 43, + "milli": 0, + }, + "game_version": b"A041", + "block_size": 0x40000, + "header_block_count": 8, + "unk2": 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, + }, + + # Depending on the app/opt/pack, this might have some text in it. + "strings": b"\x00" * 0x27AC, +} +# ---- + +# ---- Keys +# The BootID (app/opt/pack header) encryption key and IV. +BTKEY = bytes.fromhex("09ca5efd30c9aaef3804d0a7e3fa7120") +BTIV = bytes.fromhex("b155c22c2e7f0491fa7f0fdc217aff90") + +# The HMAC key used to "verify" the array of block CRCs. +SIGKEY = bytes.fromhex( + "e1bdcb2d5e9ed3b5de234364dfa4d126849edff769fc6c28fba5f43bc482bd7479d676afce8188e1d3a6852f4ebce45cde46bd15e8ee5fe84d197f945a54518f" +) + +# The header metadata contains the timestamp when the container is +# created, and the path to the original file, then just a bunch of random +# bytes. The private key used to decrypt the header metadata is unknown. +HEADER_META_PUBKEY = RSA.import_key("""-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsRMLnJuczNpfoqPpHQ3o +5XNkjKXO6P3ToV/45Az5dNaHVL7uEu9vPI7a2KYFQnNYgD3UUHFahfTcljzLOkcH +1aVrhm8gaB/5mygjUJWcN+kKyB7sASqhL22RC7NlxtDY15ozli/b0MagVoaBAV5D +MytUCa73GPRGY0x9v/wTvtmFclYCWjJ9c2QzrCrQ9eNTVyETwh5q6qKEARHGZgCX +rWmdCsa/+oS+3pLbUGFlHCSZtCvvWCJgmgurlTGAGzoxrieO6XDEg2AGiRprWWL2 +BGNh7gwgnSq6FWnKSf2Qe7xoFcTpV5QhNFBQjrq0KnBDRfz5EXJnMoKxNYL6reqR +uwIDAQAB +-----END PUBLIC KEY----- +""") +# ---- + +# ---- 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)), + "unk1" / Int8ul, + "type" / Int8ul, + "sequence_number" / Int8ul, + "derive_iv" / Int8ul, + "game_id" / Bytes(4), + "game_timestamp" / Timestamp, + "game_version" / IfThenElse(lambda ctx: ctx.type == 0x0201, Bytes(4), Version), + "block_count" / Int64ul, + "block_size" / Int64ul, + "header_block_count" / Int64ul, + "unk2" / 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 + +key = secrets.token_bytes(16) +iv = secrets.token_bytes(16) +encrypted_keypair = PKCS1_OAEP.new(HEADER_META_PUBKEY).encrypt(key + iv) +header_meta = struct.pack(" 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(" 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())