コード例 #1
0
class CustomInstall:

    cia: CIAReader

    def __init__(self, boot9, seeddb, movable, cias, sd, skip_contents=False):
        self.event = Events()
        self.log_lines = []  # Stores all info messages for user to view

        self.crypto = CryptoEngine(boot9=boot9)
        self.crypto.setup_sd_key_from_file(movable)
        self.seeddb = seeddb
        self.cias = cias
        self.sd = sd
        self.skip_contents = skip_contents
        self.movable = movable

    def copy_with_progress(self, src: BinaryIO, dst: BinaryIO, size: int,
                           path: str):
        left = size
        cipher = self.crypto.create_ctr_cipher(Keyslot.SD,
                                               self.crypto.sd_path_to_iv(path))
        while left > 0:
            to_read = min(READ_SIZE, left)
            data = cipher.encrypt(src.read(READ_SIZE))
            dst.write(data)
            left -= to_read
            total_read = size - left
            self.event.update_percentage((total_read / size) * 100,
                                         total_read / 1048576, size / 1048576)

    def start(self):
        crypto = self.crypto
        # TODO: Move a lot of these into their own methods
        self.log("Finding path to install to...")
        [sd_path, id1s] = self.get_sd_path()
        try:
            if len(id1s) > 1:
                raise SDPathError(
                    f'There are multiple id1 directories for id0 {crypto.id0.hex()}, '
                    f'please remove extra directories')
            elif len(id1s) == 0:
                raise SDPathError(
                    f'Could not find a suitable id1 directory for id0 {crypto.id0.hex()}'
                )
        except SDPathError:
            self.log("")

        cifinish_path = join(self.sd, 'cifinish.bin')
        sd_path = join(sd_path, id1s[0])
        title_info_entries = {}
        cifinish_data = load_cifinish(cifinish_path)

        # Now loop through all provided cia files

        for c in self.cias:
            self.log('Reading ' + c)

            cia = CIAReader(c, seeddb=self.seeddb)
            self.cia = cia

            tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16])

            try:
                self.log(
                    f'Installing {cia.contents[0].exefs.icon.get_app_title().short_desc}...'
                )
            except:
                self.log('Installing...')

            sizes = [1] * 5

            if cia.tmd.save_size:
                # one for the data directory, one for the 00000001.sav file
                sizes.extend((1, cia.tmd.save_size))

            for record in cia.content_info:
                sizes.append(record.size)

            # this calculates the size to put in the Title Info Entry
            title_size = sum(roundup(x, TITLE_ALIGN_SIZE) for x in sizes)

            # checks if this is dlc, which has some differences
            is_dlc = tid_parts[0] == '0004008c'

            # this checks if it has a manual (index 1) and is not DLC
            has_manual = (not is_dlc) and (1 in cia.contents)

            # this gets the extdata id from the extheader, stored in the storage info area
            try:
                with cia.contents[0].open_raw_section(
                        NCCHSection.ExtendedHeader) as e:
                    e.seek(0x200 + 0x30)
                    extdata_id = e.read(8)
            except KeyError:
                # not an executable title
                extdata_id = b'\0' * 8

            # cmd content id, starts with 1 for non-dlc contents
            cmd_id = len(cia.content_info) if is_dlc else 1
            cmd_filename = f'{cmd_id:08x}.cmd'

            # get the title root where all the contents will be
            title_root = join(sd_path, 'title', *tid_parts)
            content_root = join(title_root, 'content')
            # generate the path used for the IV
            title_root_cmd = f'/title/{"/".join(tid_parts)}'
            content_root_cmd = title_root_cmd + '/content'

            makedirs(join(content_root, 'cmd'), exist_ok=True)
            if cia.tmd.save_size:
                makedirs(join(title_root, 'data'), exist_ok=True)
            if is_dlc:
                # create the separate directories for every 256 contents
                for x in range(((len(cia.content_info) - 1) // 256) + 1):
                    makedirs(join(content_root, f'{x:08x}'))

            # maybe this will be changed in the future
            tmd_id = 0

            tmd_filename = f'{tmd_id:08x}.tmd'

            if not self.skip_contents:
                # write the tmd
                enc_path = content_root_cmd + '/' + tmd_filename
                self.log(f'Writing {enc_path}...')
                with cia.open_raw_section(CIASection.TitleMetadata) as s:
                    with open(join(content_root, tmd_filename), 'wb') as o:
                        self.copy_with_progress(
                            s, o, cia.sections[CIASection.TitleMetadata].size,
                            enc_path)

                # write each content
                for co in cia.content_info:
                    content_filename = co.id + '.app'
                    if is_dlc:
                        dir_index = format((co.cindex // 256), '08x')
                        enc_path = content_root_cmd + f'/{dir_index}/{content_filename}'
                        out_path = join(content_root, dir_index,
                                        content_filename)
                    else:
                        enc_path = content_root_cmd + '/' + content_filename
                        out_path = join(content_root, content_filename)
                    self.log(f'Writing {enc_path}...')
                    with cia.open_raw_section(co.cindex) as s, open(
                            out_path, 'wb') as o:
                        self.copy_with_progress(s, o, co.size, enc_path)

                # generate a blank save
                if cia.tmd.save_size:
                    enc_path = title_root_cmd + '/data/00000001.sav'
                    out_path = join(title_root, 'data', '00000001.sav')
                    cipher = crypto.create_ctr_cipher(
                        Keyslot.SD, crypto.sd_path_to_iv(enc_path))
                    # in a new save, the first 0x20 are all 00s. the rest can be random
                    data = cipher.encrypt(b'\0' * 0x20)
                    self.log(f'Generating blank save at {enc_path}...')
                    with open(out_path, 'wb') as o:
                        o.write(data)
                        o.write(b'\0' * (cia.tmd.save_size - 0x20))

                # generate and write cmd
                enc_path = content_root_cmd + '/cmd/' + cmd_filename
                out_path = join(content_root, 'cmd', cmd_filename)
                self.log(f'Generating {enc_path}')
                highest_index = 0
                content_ids = {}

                for record in cia.content_info:
                    highest_index = record.cindex
                    with cia.open_raw_section(record.cindex) as s:
                        s.seek(0x100)
                        cmac_data = s.read(0x100)

                    id_bytes = bytes.fromhex(record.id)[::-1]
                    cmac_data += record.cindex.to_bytes(4, 'little') + id_bytes

                    cmac_ncch = crypto.create_cmac_object(Keyslot.CMACSDNAND)
                    cmac_ncch.update(sha256(cmac_data).digest())
                    content_ids[record.cindex] = (id_bytes, cmac_ncch.digest())

                # add content IDs up to the last one
                ids_by_index = [CMD_MISSING] * (highest_index + 1)
                installed_ids = []
                cmacs = []
                for x in range(len(ids_by_index)):
                    try:
                        info = content_ids[x]
                    except KeyError:
                        # "MISSING CONTENT!"
                        # The 3DS does generate a cmac for missing contents, but I don't know how it works.
                        # It doesn't matter anyway, the title seems to be fully functional.
                        cmacs.append(
                            bytes.fromhex('4D495353494E4720434F4E54454E5421'))
                    else:
                        ids_by_index[x] = info[0]
                        cmacs.append(info[1])
                        installed_ids.append(info[0])
                installed_ids.sort(key=lambda x: int.from_bytes(x, 'little'))

                final = (cmd_id.to_bytes(4, 'little') +
                         len(ids_by_index).to_bytes(4, 'little') +
                         len(installed_ids).to_bytes(4, 'little') +
                         (1).to_bytes(4, 'little'))
                cmac_cmd_header = crypto.create_cmac_object(Keyslot.CMACSDNAND)
                cmac_cmd_header.update(final)
                final += cmac_cmd_header.digest()

                final += b''.join(ids_by_index)
                final += b''.join(installed_ids)
                final += b''.join(cmacs)

                cipher = crypto.create_ctr_cipher(
                    Keyslot.SD, crypto.sd_path_to_iv(enc_path))
                self.log(f'Writing {enc_path}')
                with open(out_path, 'wb') as o:
                    o.write(cipher.encrypt(final))

            # this starts building the title info entry
            title_info_entry_data = [
                # title size
                title_size.to_bytes(8, 'little'),
                # title type, seems to usually be 0x40
                0x40.to_bytes(4, 'little'),
                # title version
                int(cia.tmd.title_version).to_bytes(2, 'little'),
                # ncch version
                cia.contents[0].version.to_bytes(2, 'little'),
                # flags_0, only checking if there is a manual
                (1 if has_manual else 0).to_bytes(4, 'little'),
                # tmd content id, always starting with 0
                (0).to_bytes(4, 'little'),
                # cmd content id
                cmd_id.to_bytes(4, 'little'),
                # flags_1, only checking save data
                (1 if cia.tmd.save_size else 0).to_bytes(4, 'little'),
                # extdataid low
                extdata_id[0:4],
                # reserved
                b'\0' * 4,
                # flags_2, only using a common value
                0x100000000.to_bytes(8, 'little'),
                # product code
                cia.contents[0].product_code.encode('ascii').ljust(
                    0x10, b'\0'),
                # reserved
                b'\0' * 0x10,
                # unknown
                randint(0, 0xFFFFFFFF).to_bytes(4, 'little'),
                # reserved
                b'\0' * 0x2c
            ]

            title_info_entries[cia.tmd.title_id] = b''.join(
                title_info_entry_data)

            cifinish_data[int(cia.tmd.title_id, 16)] = {
                'seed': (cia.contents[0].seed
                         if cia.contents[0].flags.uses_seed else None)
            }

        save_cifinish(cifinish_path, cifinish_data)

        with TemporaryDirectory(suffix='-custom-install') as tempdir:
            # set up the common arguments for the two times we call save3ds_fuse
            save3ds_fuse_common_args = [
                join(script_dir, 'bin', platform,
                     'save3ds_fuse'), '-b', crypto.b9_path, '-m', self.movable,
                '--sd', self.sd, '--db', 'sdtitle', tempdir
            ]

            # extract the title database to add our own entry to
            self.log('Extracting Title Database...')
            subprocess.run(save3ds_fuse_common_args + ['-x'])

            for title_id, entry in title_info_entries.items():
                # write the title info entry to the temp directory
                with open(join(tempdir, title_id), 'wb') as o:
                    o.write(entry)

            # import the directory, now including our title
            self.log('Importing into Title Database...')
            subprocess.run(save3ds_fuse_common_args + ['-i'])

        self.log(
            'FINAL STEP:\nRun custom-install-finalize through homebrew launcher.'
        )
        self.log('This will install a ticket and seed if required.')

    def get_sd_path(self):
        sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
        id1s = []
        for d in scandir(sd_path):
            if d.is_dir() and len(d.name) == 32:
                try:
                    # check if the name can be converted to hex
                    # I'm not sure what the 3DS does if there is a folder that is not a 32-char hex string.
                    bytes.fromhex(d.name)
                except ValueError:
                    continue
                else:
                    id1s.append(d.name)
        return [sd_path, id1s]

    def log(self, message, mtype=0, errorname=None, end='\n'):
        """Logs an Message with a type. Format is similar to python errors

        There are 3 types of errors, indexed accordingly
        type 0 = Message
        type 1 = Warning
        type 2 = Error

        optionally, errorname can be a custom name as a string to identify errors easily
        """
        if errorname:
            errorname += ": "
        else:
            # No errorname provided
            errorname = ""
        types = [
            "",  # Type 0
            "Warning: ",  # Type 1
            "Error: "  # Type 2
        ]
        # Example: "Warning: UninformativeError: An error occured, try again.""
        msg_with_type = types[mtype] + errorname + str(message)
        self.log_lines.append(msg_with_type)
        self.event.on_log_msg(msg_with_type, end=end)
        return msg_with_type
コード例 #2
0
parser = ArgumentParser(
    description=
    'Manually install a CIA to the SD card for a Nintendo 3DS system.')
parser.add_argument('cia', help='CIA files', nargs='+')
parser.add_argument('-m', '--movable', help='movable.sed file', required=True)
parser.add_argument('-b', '--boot9', help='boot9 file')
parser.add_argument('--sd', help='path to SD root')
parser.add_argument('--skip-contents',
                    help="don't add contents, only add title info entry",
                    action='store_true')

args = parser.parse_args()

# set up the crypto engine to encrypt contents as they are written to the SD
crypto = CryptoEngine(boot9=args.boot9)
crypto.setup_sd_key_from_file(args.movable)

# try to find the path to the SD card contents
print('Finding path to install to...')
sd_path = join(args.sd, 'Nintendo 3DS', crypto.id0.hex())
id1s = []
for d in scandir(sd_path):
    if d.is_dir() and len(d.name) == 32:
        try:
            id1_tmp = bytes.fromhex(d.name)
        except ValueError:
            continue
        else:
            id1s.append(d.name)

# check the amount of id1 directories
コード例 #3
0
def main():
    print(len(sys.argv))
    if len(sys.argv) < 9:
        print("""Arguments missing. Required arguments are:
Nintendo 3DS folder
--inputFolder folderPath
Movable.sed path:
--movable movablePath
Output folder:
--outputFolder folderPath
Path to disa-extract.py:
--disa disaPath
""")
        return None

    inputFolder = None
    outputFolder = None
    movablePath = None
    disaPath = None

    i = 1
    while i < len(sys.argv):
        print(i, sys.argv[i])
        if sys.argv[i] == "--inputFolder" or sys.argv[i] == "-i":
            i += 1
            inputFolder = sys.argv[i]
        elif sys.argv[i] == "--outputFolder" or sys.argv[i] == "-o":
            i += 1
            outputFolder = sys.argv[i]
        elif sys.argv[i] == "--movable" or sys.argv[i] == "-m":
            i += 1
            movablePath = sys.argv[i]
        elif sys.argv[i] == "--disa" or sys.argv[i] == "-e":
            i += 1
            disaPath = sys.argv[i]
            releasesPath = sys.argv[i]
        else:
            print("Unable to parse {}".format(sys.argv[i]))
        i += 1

    if inputFolder is None:
        print("Input folder is not present")
    if outputFolder is None:
        print("Output folder is not present")
    if movablePath is None:
        print("Movable path is not present")
    if disaPath is None:
        print("Path to disa-extract.py is not present")

    if inputFolder is None or outputFolder is None or movablePath is None or disaPath is None:
        return None

    print("All paths parsed")
    if not (pathlib.Path(inputFolder).is_dir()):
        print(
            "Input folder directory does not exist. Inputted path: {}".format(
                inputFolder))
    if not (pathlib.Path(outputFolder).is_dir()):
        print(
            "Output folder directory does not exist. Inputted path: {}".format(
                outputFolder))
    if not (os.path.isfile(movablePath)):
        print("movable does not exist. Inputted path: {}".format(movablePath))
    if not (os.path.isfile(disaPath)):
        print("disa_extract.py does not exist. Inputted path: {}".format(
            disaPath))

    print(
        "Creating crypto object, if this creates an error then boot9.bin is not found"
    )
    crypto = CryptoEngine()

    print("Setting movable.sed for decryption")
    crypto.setup_sd_key_from_file(movablePath)

    print("Getting directories in Nintendo 3DS")
    #id0 = selectDirectory32char(inputFolder)
    id0 = crypto.id0
    id0 = id0.hex()

    print("Getting directories in {}".format(id0))
    id1 = selectDirectory32char(inputFolder + "\\" + id0)

    print("Starting dump and decrypt.")

    for titleHigh in os.listdir(inputFolder + "\\" + id0 + "\\" + id1 +
                                "\\title\\"):
        print("Title High: {}".format(titleHigh))
        for titleLow in os.listdir(inputFolder + "\\" + id0 + "\\" + id1 +
                                   "\\title\\" + titleHigh + "\\"):
            print("Title Low: {}".format(titleLow))
            appFile = selectAppFile(
                crypto, inputFolder + "\\" + id0 + "\\" + id1 + "\\title\\" +
                titleHigh + "\\" + titleLow + "\\" + "content" + "\\")
            return None