Beispiel #1
0
    def __init__(self, ncch_fp: BinaryIO, g_stat: os.stat_result, decompress_code: bool = True, dev: bool = False,
                 seeddb: str = None):
        self.crypto = CryptoEngine(dev=dev)

        self.decompress_code = decompress_code
        self.seeddb = seeddb
        self.files: Dict[str, Dict] = {}

        # get status change, modify, and file access times
        self._g_stat = g_stat
        self.g_stat = {'st_ctime': int(g_stat.st_ctime), 'st_mtime': int(g_stat.st_mtime),
                       'st_atime': int(g_stat.st_atime)}

        ncch_header = ncch_fp.read(0x200)
        self.reader = NCCHReader.from_header(ncch_header)

        self.f = ncch_fp

        if not self.reader.flags.no_crypto:
            # I should figure out what happens if fixed-key crypto is
            #   used along with seed. even though this will never
            #   happen in practice, I would still like to see what
            #   happens if it happens.
            if self.reader.flags.fixed_crypto_key:
                normal_key = FIXED_SYSTEM_KEY if self.reader.program_id & (0x10 << 32) else 0x0
                self.crypto.set_normal_key(0x2C, normal_key.to_bytes(0x10, 'big'))
            else:
                if self.reader.flags.uses_seed:
                    self.reader.load_seed_from_seeddb()

                self.crypto.set_keyslot('y', 0x2C, readbe(self.reader.get_key_y(original=True)))
                self.crypto.set_keyslot('y', self.reader.extra_keyslot,
                                        readbe(self.reader.get_key_y()))
Beispiel #2
0
    def __init__(self,
                 sd_dir: str,
                 movable: str,
                 dev: bool = False,
                 readonly: bool = False):
        self.crypto = CryptoEngine(dev=dev)

        with open(movable, 'rb') as mv:
            mv.seek(0x110)
            key_y = mv.read(0x10)
        key_hash = sha256(key_y).digest()
        hash_parts = unpack('<IIII', key_hash[0:16])
        self.root_dir = f'{hash_parts[0]:08x}{hash_parts[1]:08x}{hash_parts[2]:08x}{hash_parts[3]:08x}'

        if not os.path.isdir(sd_dir + '/' + self.root_dir):
            exit(f'Failed to find {self.root_dir} in the SD dir.')

        self.fds: Dict[int, BinaryIO] = {}

        print('Root dir: ' + self.root_dir)

        self.crypto.set_keyslot('y', 0x34, readbe(key_y))
        print('Key:      ' + self.crypto.key_normal[0x34].hex())

        self.root = os.path.realpath(sd_dir + '/' + self.root_dir)
        self.root_len = len(self.root)

        self.readonly = readonly
Beispiel #3
0
    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
Beispiel #4
0
    def __init__(self, tmd_file: str = None, cdn_dir: str = None, dec_key: str = None, dev: bool = False,
                 seeddb: str = None, boot9: str = None):
        if tmd_file:
            self.cdn_dir = os.path.dirname(tmd_file)
        else:
            self.cdn_dir = cdn_dir
            tmd_file = os.path.join(cdn_dir, 'tmd')

        self.crypto = CryptoEngine(boot9=boot9, dev=dev)

        self.cdn_content_size = 0
        self.dev = dev
        self.seeddb = seeddb

        # get status change, modify, and file access times
        try:
            self.g_stat = get_time(tmd_file)
        except FileNotFoundError:
            exit('Could not find "tmd" in directory')

        self.tmd = TitleMetadataReader.from_file(tmd_file)

        # noinspection PyUnboundLocalVariable
        self.title_id = self.tmd.title_id

        if not os.path.isfile(self.rp('cetk')):
            if not dec_key:
                exit('cetk not found. Provide the ticket or decrypted titlekey with --dec-key.')

        if dec_key:
            try:
                titlekey = bytes.fromhex(dec_key)
                if len(titlekey) != 16:
                    exit('--dec-key input is not 32 hex characters.')
            except ValueError:
                exit('Failed to convert --dec-key input to bytes. Non-hex character likely found, or is not '
                     '32 hex characters.')
            # noinspection PyUnboundLocalVariable
            self.crypto.set_normal_key(Keyslot.DecryptedTitlekey, titlekey)
        else:
            with open(self.rp('cetk'), 'rb') as tik:
                # load ticket
                self.crypto.load_from_ticket(tik.read(0x350))

        # create virtual files
        self.files = {'/ticket.bin': {'size': 0x350, 'type': 'raw', 'real_filepath': self.rp('cetk')},
                      '/tmd.bin': {'size': 0xB04 + self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': 0,
                                   'type': 'raw', 'real_filepath': self.rp('tmd')},
                      '/tmdchunks.bin': {'size': self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': 0xB04,
                                         'type': 'raw', 'real_filepath': self.rp('tmd')}}

        self.dirs: Dict[str, NCCHContainerMount] = {}
Beispiel #5
0
    def __init__(self,
                 sd_dir: str,
                 movable: bytes,
                 dev: bool = False,
                 readonly: bool = False,
                 boot9: str = None):
        self.crypto = CryptoEngine(boot9=boot9, dev=dev)

        self.crypto.setup_sd_key(movable)
        self.root_dir = self.crypto.id0.hex()

        if not os.path.isdir(sd_dir + '/' + self.root_dir):
            exit(f'Failed to find {self.root_dir} in the SD dir.')

        print('Root dir: ' + self.root_dir)
        print('Key:      ' + self.crypto.keygen(Keyslot.SD).hex())

        self.root = os.path.realpath(sd_dir + '/' + self.root_dir)
        self.root_len = len(self.root)

        self.readonly = readonly
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
Beispiel #7
0
class NCCHContainerMount(LoggingMixIn, Operations):
    fd = 0
    romfs_fuse = None
    exefs_fuse = None

    def __init__(self,
                 ncch_fp: BinaryIO,
                 g_stat: os.stat_result,
                 decompress_code: bool = True,
                 dev: bool = False,
                 seeddb: str = None):
        self.crypto = CryptoEngine(dev=dev)

        self.decompress_code = decompress_code
        self.seeddb = seeddb
        self.files: Dict[str, Dict] = {}

        # get status change, modify, and file access times
        self._g_stat = g_stat
        self.g_stat = {
            'st_ctime': int(g_stat.st_ctime),
            'st_mtime': int(g_stat.st_mtime),
            'st_atime': int(g_stat.st_atime)
        }

        ncch_header = ncch_fp.read(0x200)
        self.reader = NCCHReader.from_header(ncch_header)

        self.f = ncch_fp

        if not self.reader.flags.no_crypto:
            # I should figure out what happens if fixed-key crypto is
            #   used along with seed. even though this will never
            #   happen in practice, I would still like to see what
            #   happens if it happens.
            if self.reader.flags.fixed_crypto_key:
                normal_key = FIXED_SYSTEM_KEY if self.reader.program_id & (
                    0x10 << 32) else 0x0
                self.crypto.set_normal_key(Keyslot.NCCH,
                                           normal_key.to_bytes(0x10, 'big'))
            else:
                if self.reader.flags.uses_seed:
                    self.reader.load_seed_from_seeddb()

                self.crypto.set_keyslot(
                    'y', Keyslot.NCCH,
                    readbe(self.reader.get_key_y(original=True)))
                self.crypto.set_keyslot('y', self.reader.extra_keyslot,
                                        readbe(self.reader.get_key_y()))

    def __del__(self, *args):
        try:
            self.f.close()
        except AttributeError:
            pass

    destroy = __del__

    def init(self, path, _setup_romfs=True):
        decrypted_filename = '/decrypted.' + (
            'cxi' if self.reader.flags.executable else 'cfa')

        self.files[decrypted_filename] = {
            'size': self.reader.content_size,
            'offset': 0,
            'enctype': 'fulldec'
        }
        self.files['/ncch.bin'] = {
            'size': 0x200,
            'offset': 0,
            'enctype': 'none'
        }

        if self.reader.check_for_extheader():
            self.files['/extheader.bin'] = {
                'size': 0x800,
                'offset': 0x200,
                'enctype': 'normal',
                'keyslot': Keyslot.NCCH,
                'iv': (self.reader.partition_id << 64 | (0x01 << 56))
            }

        plain_region = self.reader.plain_region
        if plain_region.offset:
            self.files['/plain.bin'] = {
                'size': plain_region.size,
                'offset': plain_region.offset,
                'enctype': 'none'
            }

        logo_region = self.reader.logo_region
        if logo_region.offset:
            self.files['/logo.bin'] = {
                'size': logo_region.size,
                'offset': logo_region.offset,
                'enctype': 'none'
            }

        exefs_region = self.reader.exefs_region
        if exefs_region.offset:
            exefs_type = 'exefs'
            if self.reader.extra_keyslot == Keyslot.NCCH:
                exefs_type = 'normal'
            self.files['/exefs.bin'] = {
                'size': exefs_region.size,
                'offset': exefs_region.offset,
                'enctype': exefs_type,
                'keyslot': Keyslot.NCCH,
                'keyslot_extra': self.reader.extra_keyslot,
                'iv': (self.reader.partition_id << 64 | (0x02 << 56)),
                'keyslot_normal_range': [(0, 0x200)]
            }

            # noinspection PyBroadException
            try:
                # get code compression bit
                decompress = False
                if self.decompress_code and self.reader.check_for_extheader():
                    exh_flag = self.read('/extheader.bin', 1, 0xD, 0)
                    decompress = exh_flag[0] & 1
                exefs_vfp = _c.VirtualFileWrapper(self, '/exefs.bin',
                                                  exefs_region.size)
                exefs_fuse = ExeFSMount(exefs_vfp,
                                        self._g_stat,
                                        decompress_code=decompress,
                                        strict=True)
                self.exefs_fuse = exefs_fuse
            except Exception as e:
                print(f'Failed to mount ExeFS: {type(e).__name__}: {e}')
                self.exefs_fuse = None
            else:
                if not self.reader.flags.no_crypto:
                    for n, ent in self.exefs_fuse.reader.entries.items():
                        if n in {'icon', 'banner'}:
                            self.files['/exefs.bin'][
                                'keyslot_normal_range'].append(
                                    (ent.offset + 0x200, ent.offset + 0x200 +
                                     roundup(ent.size, 0x200)))

        if not self.reader.flags.no_romfs:
            romfs_region = self.reader.romfs_region
            if romfs_region.offset:
                self.files['/romfs.bin'] = {
                    'size': romfs_region.size,
                    'offset': romfs_region.offset,
                    'enctype': 'normal',
                    'keyslot': self.reader.extra_keyslot,
                    'iv': (self.reader.partition_id << 64 | (0x03 << 56))
                }

        if _setup_romfs:
            self.setup_romfs()

        if self.exefs_fuse and '/code.bin' in self.exefs_fuse.files and self.exefs_fuse.decompress_code:
            print('ExeFS: Reading .code...')
            data = self.exefs_fuse.read(
                '/code.bin', self.exefs_fuse.files['/code.bin'].size, 0, 0)
            Thread(target=self.exefs_fuse.init, daemon=True,
                   args=(path, data)).start()

    def setup_romfs(self):
        if '/romfs.bin' in self.files:
            # noinspection PyBroadException
            try:
                romfs_vfp = _c.VirtualFileWrapper(
                    self, '/romfs.bin', self.reader.romfs_region.size)
                # noinspection PyTypeChecker
                romfs_fuse = RomFSMount(romfs_vfp, self._g_stat)
                romfs_fuse.init('/')
                self.romfs_fuse = romfs_fuse
            except Exception as e:
                print(f'Failed to mount RomFS: {type(e).__name__}: {e}')

    def flush(self, path, fh):
        return self.f.flush()

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.getattr(_c.remove_first_dir(path), fh)
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.getattr(_c.remove_first_dir(path), fh)
        uid, gid, pid = fuse_get_context()
        if path in {'/', '/romfs', '/exefs'}:
            st = {'st_mode': (S_IFDIR | 0o555), 'st_nlink': 2}
        elif path in self.files:
            st = {
                'st_mode': (S_IFREG | 0o444),
                'st_size': self.files[path]['size'],
                'st_nlink': 1
            }
        else:
            raise FuseOSError(ENOENT)
        return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        if path.startswith('/exefs'):
            yield from self.exefs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path.startswith('/romfs'):
            yield from self.romfs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path == '/':
            yield from ('.', '..')
            yield from (x[1:] for x in self.files)
            if self.exefs_fuse is not None:
                yield 'exefs'
            if self.romfs_fuse is not None:
                yield 'romfs'

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.read(_c.remove_first_dir(path), size,
                                        offset, fh)
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.read(_c.remove_first_dir(path), size,
                                        offset, fh)
        fi = self.files[path]
        real_offset = fi['offset'] + offset
        if fi['offset'] + offset > fi['offset'] + fi['size']:
            return b''
        if offset + size > fi['size']:
            size = fi['size'] - offset

        if fi['enctype'] == 'none' or self.reader.flags.no_crypto:
            # if no encryption, just read and return
            self.f.seek(real_offset)
            data = self.f.read(size)

        elif fi['enctype'] == 'normal':
            self.f.seek(real_offset)
            data = self.f.read(size)
            # thanks Stary2001
            before = offset % 16
            after = (offset + size) % 16
            data = (b'\0' * before) + data + (b'\0' * after)
            iv = fi['iv'] + (offset >> 4)
            data = self.crypto.create_ctr_cipher(
                fi['keyslot'], iv).decrypt(data)[before:size + before]

        elif fi['enctype'] == 'exefs':
            # thanks Stary2001
            before = offset % 0x200
            aligned_real_offset = real_offset - before
            aligned_offset = offset - before
            aligned_size = size + before
            self.f.seek(aligned_real_offset)

            def do_thing(al_offset: int, al_size: int, cut_start: int,
                         cut_end: int):
                end: int = al_offset + (ceil(al_size / 0x200) * 0x200)
                last_chunk_offset = end - 0x200
                # noinspection PyTypeChecker
                for chunk in range(al_offset, end, 0x200):
                    iv = fi['iv'] + (chunk >> 4)
                    keyslot = fi['keyslot_extra']
                    for r in fi['keyslot_normal_range']:
                        if r[0] <= self.f.tell() - fi['offset'] < r[1]:
                            keyslot = fi['keyslot']
                    out = self.crypto.create_ctr_cipher(keyslot, iv).decrypt(
                        self.f.read(0x200))
                    if chunk == al_offset:
                        out = out[cut_start:]
                    if chunk == last_chunk_offset and cut_end != 0x200:
                        out = out[:-cut_end]
                    yield out

            data = b''.join(
                do_thing(aligned_offset, aligned_size, before,
                         0x200 - ((size + before) % 0x200)))

        elif fi['enctype'] == 'fulldec':
            # this could be optimized much better
            before = offset % 0x200
            aligned_real_offset = real_offset - before
            aligned_offset = offset - before
            aligned_size = size + before
            self.f.seek(aligned_real_offset)

            def do_thing(al_offset: int, al_size: int, cut_start: int,
                         cut_end: int):
                end: int = al_offset + (ceil(al_size / 0x200) * 0x200)
                # dict is ordered by default in CPython since 3.6.0
                # and part of the language spec since 3.7.0
                to_read: Dict[str, List[int]] = {}

                if self.reader.check_for_extheader():
                    extheader_start = 0x200
                    extheader_end = 0xA00
                else:
                    extheader_start = extheader_end = 0

                logo = self.reader.logo_region
                logo_start = logo.offset
                logo_end = logo_start + logo.size

                plain = self.reader.plain_region
                plain_start = plain.offset
                plain_end = plain_start + plain.size

                exefs = self.reader.exefs_region
                exefs_start = exefs.offset
                exefs_end = exefs_start + exefs.size

                romfs = self.reader.romfs_region
                romfs_start = romfs.offset
                romfs_end = romfs_start + romfs.size

                for chunk_offset in range(al_offset, end, 0x200):
                    # RomFS check first, since it might be faster
                    if romfs_start <= chunk_offset < romfs_end:
                        name = '/romfs.bin'
                        curr_offset = romfs_start
                    # ExeFS check second, since it might be faster
                    elif exefs_start <= chunk_offset < exefs_end:
                        name = '/exefs.bin'
                        curr_offset = exefs_start
                    # NCCH check, always 0x0 to 0x200
                    elif 0 <= chunk_offset < 0x200:
                        name = '/ncch.bin'
                        curr_offset = 0
                    elif extheader_start <= chunk_offset < extheader_end:
                        name = '/extheader.bin'
                        curr_offset = extheader_start
                    elif logo_start <= chunk_offset < logo_end:
                        name = '/logo.bin'
                        curr_offset = logo_start
                    elif plain_start <= chunk_offset < plain_end:
                        name = '/plain.bin'
                        curr_offset = plain_start
                    else:
                        name = f'raw{chunk_offset}'
                        curr_offset = 0
                    if name not in to_read:
                        to_read[name] = [chunk_offset - curr_offset, 0]
                    to_read[name][1] += 0x200
                    last_name = name

                is_start = True
                for name, info in to_read.items():
                    try:
                        new_data = self.read(name, info[1], info[0], 0)
                        if name == '/ncch.bin':
                            # fix crypto flags
                            ncch_array = bytearray(new_data)
                            ncch_array[0x18B] = 0
                            ncch_array[0x18F] = 4
                            new_data = bytes(ncch_array)
                    except KeyError:
                        # for unknown files
                        self.f.seek(info[0])
                        new_data = self.f.read(info[1])
                    if is_start is True:
                        new_data = new_data[cut_start:]
                        is_start = False
                    # noinspection PyUnboundLocalVariable
                    if name == last_name and cut_end != 0x200:
                        new_data = new_data[:-cut_end]

                    yield new_data

            data = b''.join(
                do_thing(aligned_offset, aligned_size, before,
                         0x200 - ((size + before) % 0x200)))

        else:
            from pprint import pformat
            print(
                '--------------------------------------------------',
                'Warning: unknown file type (this should not happen!)',
                'Please file an issue or contact the developer with the details below.',
                '  https://github.com/ihaveamac/ninfs/issues',
                '--------------------------------------------------',
                f'{path!r}: {pformat(fi)!r}',
                sep='\n')

            data = b'g' * size

        return data

    @_c.ensure_lower_path
    def statfs(self, path):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.statfs(_c.remove_first_dir(path))
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.statfs(_c.remove_first_dir(path))
        else:
            return {
                'f_bsize': 4096,
                'f_blocks': self.reader.content_size // 4096,
                'f_bavail': 0,
                'f_bfree': 0,
                'f_files': len(self.files)
            }
Beispiel #8
0
class CDNContentsMount(LoggingMixIn, Operations):
    fd = 0

    # get the real path by returning self.cdn_dir + path
    def rp(self, path):
        return os.path.join(self.cdn_dir, path)

    def __init__(self, tmd_file: str = None, cdn_dir: str = None, dec_key: str = None, dev: bool = False,
                 seeddb: str = None, boot9: str = None):
        if tmd_file:
            self.cdn_dir = os.path.dirname(tmd_file)
        else:
            self.cdn_dir = cdn_dir
            tmd_file = os.path.join(cdn_dir, 'tmd')

        self.crypto = CryptoEngine(boot9=boot9, dev=dev)

        self.cdn_content_size = 0
        self.dev = dev
        self.seeddb = seeddb

        # get status change, modify, and file access times
        try:
            self.g_stat = get_time(tmd_file)
        except FileNotFoundError:
            exit('Could not find "tmd" in directory')

        self.tmd = TitleMetadataReader.from_file(tmd_file)

        # noinspection PyUnboundLocalVariable
        self.title_id = self.tmd.title_id

        if not os.path.isfile(self.rp('cetk')):
            if not dec_key:
                exit('cetk not found. Provide the ticket or decrypted titlekey with --dec-key.')

        if dec_key:
            try:
                titlekey = bytes.fromhex(dec_key)
                if len(titlekey) != 16:
                    exit('--dec-key input is not 32 hex characters.')
            except ValueError:
                exit('Failed to convert --dec-key input to bytes. Non-hex character likely found, or is not '
                     '32 hex characters.')
            # noinspection PyUnboundLocalVariable
            self.crypto.set_normal_key(Keyslot.DecryptedTitlekey, titlekey)
        else:
            with open(self.rp('cetk'), 'rb') as tik:
                # load ticket
                self.crypto.load_from_ticket(tik.read(0x350))

        # create virtual files
        self.files = {'/ticket.bin': {'size': 0x350, 'type': 'raw', 'real_filepath': self.rp('cetk')},
                      '/tmd.bin': {'size': 0xB04 + self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': 0,
                                   'type': 'raw', 'real_filepath': self.rp('tmd')},
                      '/tmdchunks.bin': {'size': self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': 0xB04,
                                         'type': 'raw', 'real_filepath': self.rp('tmd')}}

        self.dirs: Dict[str, NCCHContainerMount] = {}

    def init(self, path):
        # read contents to generate virtual files
        for chunk in self.tmd.chunk_records:
            if os.path.isfile(self.rp(chunk.id)):
                real_filename = chunk.id
            elif os.path.isfile(self.rp(chunk.id.upper())):
                real_filename = chunk.id.upper()
            else:
                print(f'Content {chunk.cindex:04}:{chunk.id} not found, will not be included.')
                continue
            f_stat = os.stat(self.rp(real_filename))
            if chunk.size != f_stat.st_size:
                print('Warning: TMD Content size and filesize of', chunk.id, 'are different.')
            self.cdn_content_size += chunk.size
            is_srl = chunk.cindex == 0 and self.title_id[3:5] == '48'
            file_ext = 'nds' if is_srl else 'ncch'
            filename = f'/{chunk.cindex:04x}.{chunk.id}.{file_ext}'
            self.files[filename] = {'size': chunk.size, 'index': chunk.cindex.to_bytes(2, 'big'),
                                    'type': 'enc' if chunk.type.encrypted else 'raw',
                                    'real_filepath': self.rp(real_filename)}

            dirname = f'/{chunk.cindex:04x}.{chunk.id}'
            # noinspection PyBroadException
            try:
                f_time = get_time(f_stat)
                content_vfp = _c.VirtualFileWrapper(self, filename, chunk.size)
                # boot9 is not passed here, as CryptoEngine has already set up the keys at the beginning.
                if is_srl:
                    content_fuse = SRLMount(content_vfp, g_stat=f_time)
                else:
                    content_reader = NCCHReader(content_vfp, dev=self.dev, seeddb=self.seeddb)
                    content_fuse = NCCHContainerMount(content_reader, g_stat=f_time)
                content_fuse.init(path)
                self.dirs[dirname] = content_fuse
            except Exception as e:
                print(f'Failed to mount {filename}: {type(e).__name__}: {e}')

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].getattr(_c.remove_first_dir(path), fh)
        uid, gid, pid = fuse_get_context()
        if path == '/' or path in self.dirs:
            st = {'st_mode': (S_IFDIR | 0o555), 'st_nlink': 2}
        elif path in self.files:
            st = {'st_mode': (S_IFREG | 0o444), 'st_size': self.files[path]['size'], 'st_nlink': 1}
        else:
            raise FuseOSError(ENOENT)
        return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            yield from self.dirs[first_dir].readdir(_c.remove_first_dir(path), fh)
        else:
            yield from ('.', '..')
            yield from (x[1:] for x in self.files)
            yield from (x[1:] for x in self.dirs)

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].read(_c.remove_first_dir(path), size, offset, fh)
        fi = self.files[path]
        real_size = size
        with open(fi['real_filepath'], 'rb') as f:
            if fi['type'] == 'raw':
                # if raw, just read and return
                f.seek(offset)
                data = f.read(size)

            elif fi['type'] == 'enc':
                # if encrypted, the block needs to be decrypted first
                # CBC requires a full block (0x10 in this case). and the previous
                #   block is used as the IV. so that's quite a bit to read if the
                #   application requires just a few bytes.
                # thanks Stary2001
                before = offset % 16
                if size % 16 != 0:
                    size = size + 16 - size % 16
                if offset - before == 0:
                    # use the initial value if reading from the first block
                    iv = fi['index'] + (b'\0' * 14)
                else:
                    # use the previous block if reading anywhere else
                    f.seek(offset - before - 0x10)
                    iv = f.read(0x10)
                # read to block size
                f.seek(offset - before)
                # adding 0x10 to the size fixes some kind of decryption bug
                data = self.crypto.create_cbc_cipher(Keyslot.DecryptedTitlekey,
                                                     iv).decrypt(f.read(size + 0x10))[before:real_size + before]

            else:
                from pprint import pformat
                print('--------------------------------------------------',
                      'Warning: unknown file type (this should not happen!)',
                      'Please file an issue or contact the developer with the details below.',
                      '  https://github.com/ihaveamac/ninfs/issues',
                      '--------------------------------------------------',
                      f'{path!r}: {pformat(fi)!r}', sep='\n')

                data = b'g' * size

            return data

    @_c.ensure_lower_path
    def statfs(self, path):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].read(_c.remove_first_dir(path))
        return {'f_bsize': 4096, 'f_blocks': self.cdn_content_size // 4096, 'f_bavail': 0, 'f_bfree': 0,
                'f_files': len(self.files)}
Beispiel #9
0
class CTRImportableArchiveMount(LoggingMixIn, Operations):
    fd = 0

    def __init__(self,
                 cia_fp: BinaryIO,
                 g_stat: os.stat_result,
                 dev: bool = False,
                 seeddb: bool = None):
        self.crypto = CryptoEngine(dev=dev)

        self.dev = dev
        self.seeddb = seeddb

        self._g_stat = g_stat
        # get status change, modify, and file access times
        self.g_stat = {
            'st_ctime': int(g_stat.st_ctime),
            'st_mtime': int(g_stat.st_mtime),
            'st_atime': int(g_stat.st_atime)
        }

        # open cia and get section sizes
        archive_header_size, cia_type, cia_version, cert_chain_size, \
            ticket_size, tmd_size, meta_size, content_size = unpack('<IHHIIIIQ', cia_fp.read(0x20))

        self.cia_size = new_offset(archive_header_size) + new_offset(cert_chain_size) + new_offset(ticket_size)\
            + new_offset(tmd_size) + new_offset(meta_size) + new_offset(content_size)

        # get offsets for sections of the CIA
        # each section is aligned to 64-byte blocks
        cert_chain_offset = new_offset(archive_header_size)
        ticket_offset = cert_chain_offset + new_offset(cert_chain_size)
        tmd_offset = ticket_offset + new_offset(ticket_size)
        self.content_offset = tmd_offset + new_offset(tmd_size)
        meta_offset = self.content_offset + new_offset(content_size)

        # load tmd
        cia_fp.seek(tmd_offset)
        self.tmd = TitleMetadataReader.load(cia_fp)
        self.title_id = self.tmd.title_id

        # load titlekey
        cia_fp.seek(ticket_offset)
        self.crypto.load_from_ticket(cia_fp.read(ticket_size))

        # create virtual files
        self.files = {
            '/header.bin': {
                'size': archive_header_size,
                'offset': 0,
                'type': 'raw'
            },
            '/cert.bin': {
                'size': cert_chain_size,
                'offset': cert_chain_offset,
                'type': 'raw'
            },
            '/ticket.bin': {
                'size': ticket_size,
                'offset': ticket_offset,
                'type': 'raw'
            },
            '/tmd.bin': {
                'size': tmd_size,
                'offset': tmd_offset,
                'type': 'raw'
            },
            '/tmdchunks.bin': {
                'size': self.tmd.content_count * CHUNK_RECORD_SIZE,
                'offset': tmd_offset + 0xB04,
                'type': 'raw'
            }
        }
        if meta_size:
            self.files['/meta.bin'] = {
                'size': meta_size,
                'offset': meta_offset,
                'type': 'raw'
            }
            # show icon.bin if meta size is the expected size
            # in practice this never changes, but better to be safe
            if meta_size == 0x3AC0:
                self.files['/icon.bin'] = {
                    'size': 0x36C0,
                    'offset': meta_offset + 0x400,
                    'type': 'raw'
                }

        self.dirs: Dict[str, NCCHContainerMount] = {}

        self.f = cia_fp

    def __del__(self, *args):
        try:
            self.f.close()
        except AttributeError:
            pass

    destroy = __del__

    def init(self, path):
        # read chunks to generate virtual files
        current_offset = self.content_offset
        for chunk in self.tmd.chunk_records:
            is_srl = chunk.cindex == 0 and self.title_id[3:5] == '48'
            file_ext = 'nds' if is_srl else 'ncch'
            filename = f'/{chunk.cindex:04x}.{chunk.id}.{file_ext}'
            self.files[filename] = {
                'size': chunk.size,
                'offset': current_offset,
                'index': chunk.cindex.to_bytes(2, 'big'),
                'type': 'enc' if chunk.type.encrypted else 'raw'
            }
            current_offset += new_offset(chunk.size)

            dirname = f'/{chunk.cindex:04x}.{chunk.id}'
            # noinspection PyBroadException
            try:
                content_vfp = _c.VirtualFileWrapper(self, filename, chunk.size)
                if is_srl:
                    content_fuse = SRLMount(content_vfp, g_stat=self._g_stat)
                else:
                    content_fuse = NCCHContainerMount(content_vfp,
                                                      dev=self.dev,
                                                      g_stat=self._g_stat,
                                                      seeddb=self.seeddb)
                content_fuse.init(path)
                self.dirs[dirname] = content_fuse
            except Exception as e:
                print(f'Failed to mount {filename}: {type(e).__name__}: {e}')

    def flush(self, path, fh):
        return self.f.flush()

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].getattr(_c.remove_first_dir(path), fh)
        uid, gid, pid = fuse_get_context()
        if path == '/' or path in self.dirs:
            st = {'st_mode': (S_IFDIR | 0o555), 'st_nlink': 2}
        elif path in self.files:
            st = {
                'st_mode': (S_IFREG | 0o444),
                'st_size': self.files[path]['size'],
                'st_nlink': 1
            }
        else:
            raise FuseOSError(ENOENT)
        return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            yield from self.dirs[first_dir].readdir(_c.remove_first_dir(path),
                                                    fh)
        else:
            yield from ('.', '..')
            yield from (x[1:] for x in self.files)
            yield from (x[1:] for x in self.dirs)

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].read(_c.remove_first_dir(path), size,
                                             offset, fh)
        fi = self.files[path]
        real_offset = fi['offset'] + offset
        if fi['offset'] + offset > fi['offset'] + fi['size']:
            return b''
        if offset + size > fi['size']:
            size = fi['size'] - offset
        real_size = size

        if fi['type'] == 'raw':
            # if raw, just read and return
            self.f.seek(real_offset)
            data = self.f.read(size)

        elif fi['type'] == 'enc':
            # if encrypted, the block needs to be decrypted first
            # CBC requires a full block (0x10 in this case). and the previous
            #   block is used as the IV. so that's quite a bit to read if the
            #   application requires just a few bytes.
            # thanks Stary2001
            before = offset % 16
            if size % 16 != 0:
                size = size + 16 - size % 16
            if offset - before == 0:
                # use the initial value if reading from the first block
                # noinspection PyTypeChecker
                iv = fi['index'] + (b'\0' * 14)
            else:
                # use the previous block if reading anywhere else
                self.f.seek(real_offset - before - 0x10)
                iv = self.f.read(0x10)
            # read to block size
            self.f.seek(real_offset - before)
            # adding 0x10 to the size fixes some kind of decryption bug
            data = self.crypto.create_cbc_cipher(
                Keyslot.DecryptedTitlekey, iv).decrypt(
                    self.f.read(size + 0x10))[before:real_size + before]

        else:
            from pprint import pformat
            print(
                '--------------------------------------------------',
                'Warning: unknown file type (this should not happen!)',
                'Please file an issue or contact the developer with the details below.',
                '  https://github.com/ihaveamac/ninfs/issues',
                '--------------------------------------------------',
                f'{path!r}: {pformat(fi)!r}',
                sep='\n')

            data = b'g' * size

        return data

    @_c.ensure_lower_path
    def statfs(self, path):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].statfs(_c.remove_first_dir(path))
        return {
            'f_bsize': 4096,
            'f_blocks': self.cia_size // 4096,
            'f_bavail': 0,
            'f_bfree': 0,
            'f_files': len(self.files)
        }
Beispiel #10
0
    def __init__(self,
                 cia_fp: BinaryIO,
                 g_stat: os.stat_result,
                 dev: bool = False,
                 seeddb: bool = None):
        self.crypto = CryptoEngine(dev=dev)

        self.dev = dev
        self.seeddb = seeddb

        self._g_stat = g_stat
        # get status change, modify, and file access times
        self.g_stat = {
            'st_ctime': int(g_stat.st_ctime),
            'st_mtime': int(g_stat.st_mtime),
            'st_atime': int(g_stat.st_atime)
        }

        # open cia and get section sizes
        archive_header_size, cia_type, cia_version, cert_chain_size, \
            ticket_size, tmd_size, meta_size, content_size = unpack('<IHHIIIIQ', cia_fp.read(0x20))

        self.cia_size = new_offset(archive_header_size) + new_offset(cert_chain_size) + new_offset(ticket_size)\
            + new_offset(tmd_size) + new_offset(meta_size) + new_offset(content_size)

        # get offsets for sections of the CIA
        # each section is aligned to 64-byte blocks
        cert_chain_offset = new_offset(archive_header_size)
        ticket_offset = cert_chain_offset + new_offset(cert_chain_size)
        tmd_offset = ticket_offset + new_offset(ticket_size)
        self.content_offset = tmd_offset + new_offset(tmd_size)
        meta_offset = self.content_offset + new_offset(content_size)

        # load tmd
        cia_fp.seek(tmd_offset)
        self.tmd = TitleMetadataReader.load(cia_fp)
        self.title_id = self.tmd.title_id

        # load titlekey
        cia_fp.seek(ticket_offset)
        self.crypto.load_from_ticket(cia_fp.read(ticket_size))

        # create virtual files
        self.files = {
            '/header.bin': {
                'size': archive_header_size,
                'offset': 0,
                'type': 'raw'
            },
            '/cert.bin': {
                'size': cert_chain_size,
                'offset': cert_chain_offset,
                'type': 'raw'
            },
            '/ticket.bin': {
                'size': ticket_size,
                'offset': ticket_offset,
                'type': 'raw'
            },
            '/tmd.bin': {
                'size': tmd_size,
                'offset': tmd_offset,
                'type': 'raw'
            },
            '/tmdchunks.bin': {
                'size': self.tmd.content_count * CHUNK_RECORD_SIZE,
                'offset': tmd_offset + 0xB04,
                'type': 'raw'
            }
        }
        if meta_size:
            self.files['/meta.bin'] = {
                'size': meta_size,
                'offset': meta_offset,
                'type': 'raw'
            }
            # show icon.bin if meta size is the expected size
            # in practice this never changes, but better to be safe
            if meta_size == 0x3AC0:
                self.files['/icon.bin'] = {
                    'size': 0x36C0,
                    'offset': meta_offset + 0x400,
                    'type': 'raw'
                }

        self.dirs: Dict[str, NCCHContainerMount] = {}

        self.f = cia_fp
Beispiel #11
0
parser = argparse.ArgumentParser(
    description='Generate Nintendo 3DS CMD files.')
parser.add_argument('-t', '--tmd', help='tmd file', required=True)
parser.add_argument('-m', '--movable', help='movable.sed file', required=True)
parser.add_argument('-o', '--otp', help='otp.bin file, for TWLNAND contents')
parser.add_argument('-b', '--boot9', help='boot9 file')
parser.add_argument('--output-id',
                    help='CMD content ID, default 00000001',
                    default='00000001')

a = parser.parse_args()

MISSING = b'\xff\xff\xff\xff'

crypto = CryptoEngine()
crypto.setup_sd_key_from_file(a.movable)
try:
    crypto.setup_keys_from_otp_file(a.otp)
except FileNotFoundError:
    pass
tmd = TitleMetadataReader.from_file(a.tmd)
dirname = os.path.dirname(a.tmd)
if tmd.title_id.startswith('0004008c'):
    content_dir = os.path.join(dirname, '00000000')
else:
    content_dir = dirname

# TODO: check Download Play
if tmd.title_id.startswith('00048'):
    keyslot = Keyslot.CMACNANDDB
Beispiel #12
0
    def __init__(self, nand_fp: BinaryIO, g_stat: dict, consoleid: str = None, cid: str = None,
                 readonly: bool = False):
        self.crypto = CryptoEngine(setup_b9_keys=False)
        self.readonly = readonly

        self.g_stat = g_stat

        self.files = {}

        self.f = nand_fp

        nand_size = nand_fp.seek(0, 2)
        if nand_size < 0xF000000:
            exit(f'NAND is too small (expected >= 0xF000000, got {nand_size:#X}')
        if nand_size & 0x40 == 0x40:
            self.files['/nocash_blk.bin'] = {'offset': nand_size - 0x40, 'size': 0x40, 'type': 'dec'}

        nand_fp.seek(0)

        try:
            consoleid = bytes.fromhex(consoleid)
        except (ValueError, TypeError):
            try:
                with open(consoleid, 'rb') as f:
                    consoleid = f.read(0x10)
            except (FileNotFoundError, TypeError):
                # read Console ID and CID from footer
                try:
                    nocash_blk: bytes = self.read('/nocash_blk.bin', 0x40, 0, 0)
                except KeyError:
                    if consoleid is None:
                        exit('Nocash block not found, and Console ID not provided.')
                    else:
                        exit('Failed to convert Console ID to bytes, or file did not exist.')
                else:
                    if len(nocash_blk) != 0x40:
                        exit('Failed to read 0x40 of footer (this should never happen)')
                    if nocash_blk[0:0x10] != b'DSi eMMC CID/CPU':
                        exit('Failed to find footer magic "DSi eMMC CID/CPU"')
                    if len(set(nocash_blk[0x10:0x40])) == 1:
                        exit('Nocash block is entirely empty. Maybe re-dump NAND with another exploit, or manually '
                             'get Console ID with some other method.')
                    cid = nocash_blk[0x10:0x20]
                    consoleid = nocash_blk[0x20:0x28][::-1]
                    print('Console ID and CID read from nocash block.')

        twl_consoleid_list = (readbe(consoleid[4:8]), readbe(consoleid[0:4]))

        key_x_list = [twl_consoleid_list[0],
                      twl_consoleid_list[0] ^ 0x24EE6906,
                      twl_consoleid_list[1] ^ 0xE65B601D,
                      twl_consoleid_list[1]]

        self.crypto.set_keyslot('x', Keyslot.TWLNAND, pack('<4I', *key_x_list))

        nand_fp.seek(0)
        header_enc = nand_fp.read(0x200)

        if cid:
            if not isinstance(cid, bytes):  # if cid was already read above
                try:
                    cid = bytes.fromhex(cid)
                except ValueError:
                    try:
                        with open(cid, 'rb') as f:
                            cid = f.read(0x10)
                    except FileNotFoundError:
                        exit('Failed to convert CID to bytes, or file did not exist.')
            self.ctr = readle(sha1(cid).digest()[0:16])

        else:
            # attempt to generate counter
            block_0x1c = readbe(header_enc[0x1C0:0x1D0])
            blk_xored = block_0x1c ^ 0x1804060FE03B77080000896F06000002
            ctr_offs = self.crypto.create_ecb_cipher(Keyslot.TWLNAND).decrypt(blk_xored.to_bytes(0x10, 'little'))
            self.ctr = int.from_bytes(ctr_offs, 'big') - 0x1C

            # try the counter
            block_0x1d = header_enc[0x1D0:0x1E0]
            out = self.crypto.create_ctr_cipher(Keyslot.TWLNAND, self.ctr + 0x1D).decrypt(block_0x1d)
            if out != b'\xce<\x06\x0f\xe0\xbeMx\x06\x00\xb3\x05\x01\x00\x00\x02':
                exit('Counter could not be automatically generated. Please provide the CID, '
                     'or ensure the provided Console ID is correct.')
            print('Counter automatically generated.')

        self.files['/stage2_infoblk1.bin'] = {'offset': 0x200, 'size': 0x200, 'type': 'dec'}
        self.files['/stage2_infoblk2.bin'] = {'offset': 0x400, 'size': 0x200, 'type': 'dec'}
        self.files['/stage2_infoblk3.bin'] = {'offset': 0x600, 'size': 0x200, 'type': 'dec'}
        self.files['/stage2_bootldr.bin'] = {'offset': 0x800, 'size': 0x4DC00, 'type': 'dec'}
        self.files['/stage2_footer.bin'] = {'offset': 0x4E400, 'size': 0x400, 'type': 'dec'}
        self.files['/diag_area.bin'] = {'offset': 0xFFA00, 'size': 0x400, 'type': 'dec'}

        header = self.crypto.create_ctr_cipher(Keyslot.TWLNAND, self.ctr).decrypt(header_enc)
        mbr = header[0x1BE:0x200]
        mbr_sig = mbr[0x40:0x42]
        if mbr_sig != b'\x55\xaa':
            exit(f'MBR signature not found (expected "55aa", got "{mbr_sig.hex()}"). '
                 f'Make sure the provided Console ID and CID are correct.')
        partitions = [[readle(mbr[i + 8:i + 12]) * 0x200,
                       readle(mbr[i + 12:i + 16]) * 0x200] for i in range(0, 0x40, 0x10)]

        for idx, part in enumerate(partitions):
            if part[0]:
                ptype = 'enc' if idx < 2 else 'dec'
                pname = ('twl_main', 'twl_photo', 'twl_unk1', 'twk_unk2')[idx]
                self.files[f'/{pname}.img'] = {'offset': part[0], 'size': part[1], 'type': ptype}
Beispiel #13
0
class CTRNandImageMount(LoggingMixIn, Operations):
    fd = 0

    _essentials_mounted = False

    def __init__(self,
                 nand_fp: BinaryIO,
                 g_stat: dict,
                 dev: bool = False,
                 readonly: bool = False,
                 otp: bytes = None,
                 cid: AnyStr = None,
                 boot9: str = None):
        self.crypto = CryptoEngine(boot9=boot9, dev=dev)

        self.g_stat = g_stat

        nand_fp.seek(0x100)  # screw the signature
        ncsd_header = nand_fp.read(0x100)
        if ncsd_header[0:4] != b'NCSD':
            exit(
                'NCSD magic not found, is this a real Nintendo 3DS NAND image?'
            )
        media_id = ncsd_header[0x8:0x10]
        if media_id != b'\0' * 8:
            exit(
                'Media ID not all-zero, is this a real Nintendo 3DS NAND image?'
            )

        # check for essential.exefs
        nand_fp.seek(0x200)
        try:
            exefs = ExeFSReader(nand_fp)
        except InvalidExeFSError:
            exefs = None

        otp_data = None
        if otp:
            try:
                with open(otp, 'rb') as f:
                    otp_data = f.read(0x200)
            except Exception:
                print(f'Failed to open and read given OTP ({otp}).\n')
                print_exc()
                exit(1)

        else:
            if exefs is None:
                exit(
                    'OTP not found, provide with --otp or embed essentials backup with GodMode9'
                )
            else:
                try:
                    with exefs.open('otp') as otp:
                        otp_data = otp.read(0x200)
                except ExeFSFileNotFoundError:
                    exit(
                        '"otp" not found in essentials backup, update with GodMode9 or provide with --otp'
                    )

        self.crypto.setup_keys_from_otp(otp_data)

        def generate_ctr():
            print(
                'Attempting to generate Counter for CTR/TWL areas. If errors occur, provide the CID manually.'
            )

            # -------------------------------------------------- #
            # attempt to generate CTR Counter
            nand_fp.seek(0xB9301D0)
            # these blocks are assumed to be entirely 00, so no need to xor anything
            ctrn_block_0x1d = nand_fp.read(0x10)
            ctrn_block_0x1e = nand_fp.read(0x10)
            for ks in (Keyslot.CTRNANDOld, Keyslot.CTRNANDNew):
                ctr_counter_offs = self.crypto.create_ecb_cipher(ks).decrypt(
                    ctrn_block_0x1d)
                ctr_counter = int.from_bytes(ctr_counter_offs,
                                             'big') - 0xB9301D

                # try the counter
                out = self.crypto.create_ctr_cipher(
                    ks, ctr_counter + 0xB9301E).decrypt(ctrn_block_0x1e)
                if out == b'\0' * 16:
                    print('Counter for CTR area automatically generated.')
                    self.ctr = ctr_counter
                    break
            else:
                print(
                    'Counter could not be generated for CTR area. Related virtual files will not appear.'
                )
                self.ctr = None

            # -------------------------------------------------- #
            # attempt to generate TWL Counter
            nand_fp.seek(0x1C0)
            twln_block_0x1c = readbe(nand_fp.read(0x10))
            twl_blk_xored = twln_block_0x1c ^ 0x18000601A03F97000000A97D04000004
            twl_counter_offs = self.crypto.create_ecb_cipher(0x03).decrypt(
                twl_blk_xored.to_bytes(0x10, 'little'))
            twl_counter = int.from_bytes(twl_counter_offs, 'big') - 0x1C

            # try the counter
            twln_block_0x1d = nand_fp.read(0x10)
            out = self.crypto.create_ctr_cipher(0x03, twl_counter +
                                                0x1D).decrypt(twln_block_0x1d)
            if out == b'\x8e@\x06\x01\xa0\xc3\x8d\x80\x04\x00\xb3\x05\x01\x00\x00\x00':
                print('Counter for TWL area automatically generated.')
                self.ctr_twl = twl_counter
            else:
                print(
                    'Counter could not be generated for TWL area. Related virtual files will not appear.'
                )
                self.ctr_twl = None

        cid_data = None
        if cid:
            try:
                with open(cid, 'rb') as f:
                    cid_data = f.read(0x200)
            except Exception:
                print(f'Failed to open and read given CID ({cid}).')
                print(
                    'If you want to attempt Counter generation, do not provide a CID path.\n'
                )
                print_exc()
                exit(1)

        else:
            if exefs is None:
                generate_ctr()
            else:
                try:
                    with exefs.open('nand_cid') as cid:
                        cid_data = cid.read(0x10)
                except ExeFSFileNotFoundError:
                    print(
                        '"nand_cid" not found in essentials backup, update with GodMode9 or provide with --cid'
                    )
                    generate_ctr()

        if cid_data:
            self.ctr = readbe(sha256(cid_data).digest()[0:16])
            self.ctr_twl = readle(sha1(cid_data).digest()[0:16])

        if not (self.ctr or self.ctr_twl):
            exit("Couldn't generate Counter for both CTR/TWL. "
                 "Make sure the OTP is correct, or provide the CID manually.")

        nand_fp.seek(0, 2)
        raw_nand_size = nand_fp.tell()

        self.real_nand_size = nand_size[readle(ncsd_header[4:8])]

        self.files = {
            '/nand_hdr.bin': {
                'size': 0x200,
                'offset': 0,
                'keyslot': 0xFF,
                'type': 'raw'
            },
            '/nand.bin': {
                'size': raw_nand_size,
                'offset': 0,
                'keyslot': 0xFF,
                'type': 'raw'
            },
            '/nand_minsize.bin': {
                'size': self.real_nand_size,
                'offset': 0,
                'keyslot': 0xFF,
                'type': 'raw'
            }
        }

        nand_fp.seek(0x12C00)
        keysect_enc = nand_fp.read(0x200)
        if len(set(keysect_enc)) != 1:
            keysect_dec = self.crypto.create_ecb_cipher(0x11).decrypt(
                keysect_enc)
            # i'm cheating here by putting the decrypted version in memory and
            #   not reading from the image every time. but it's not AES-CTR so
            #   f**k that.
            self.files['/sector0x96.bin'] = {
                'size': 0x200,
                'offset': 0x12C00,
                'keyslot': 0x11,
                'type': 'keysect',
                'content': keysect_dec
            }

        ncsd_part_fstype = ncsd_header[0x10:0x18]
        ncsd_part_crypttype = ncsd_header[0x18:0x20]
        ncsd_part_raw = ncsd_header[0x20:0x60]
        ncsd_partitions = [[
            readle(ncsd_part_raw[i:i + 4]) * 0x200,
            readle(ncsd_part_raw[i + 4:i + 8]) * 0x200
        ] for i in range(0, 0x40, 0x8)]

        # including padding for crypto
        if self.ctr_twl:
            twl_mbr = self.crypto.create_ctr_cipher(
                Keyslot.TWLNAND,
                self.ctr_twl + 0x1B).decrypt(ncsd_header[0xB0:0x100])[0xE:0x50]
            if twl_mbr[0x40:0x42] == b'\x55\xaa':
                twl_partitions = [[
                    readle(twl_mbr[i + 8:i + 12]) * 0x200,
                    readle(twl_mbr[i + 12:i + 16]) * 0x200
                ] for i in range(0, 0x40, 0x10)]
            else:
                twl_partitions = None

            self.files['/twlmbr.bin'] = {
                'size': 0x42,
                'offset': 0x1BE,
                'keyslot': Keyslot.TWLNAND,
                'type': 'twlmbr',
                'content': twl_mbr
            }
        else:
            twl_partitions = None

        # then actually parse the partitions to create files
        firm_idx = 0
        for idx, part in enumerate(ncsd_partitions):
            if ncsd_part_fstype[idx] == 0:
                continue
            print(
                f'ncsd idx:{idx} fstype:{ncsd_part_fstype[idx]} crypttype:{ncsd_part_crypttype[idx]} '
                f'offset:{part[0]:08x} size:{part[1]:08x} ',
                end='')
            if idx == 0:
                if self.ctr_twl:
                    self.files['/twl_full.img'] = {
                        'size': part[1],
                        'offset': part[0],
                        'keyslot': Keyslot.TWLNAND,
                        'type': 'enc'
                    }
                    print('/twl_full.img')
                    if twl_partitions:
                        twl_part_fstype = 0
                        for t_idx, t_part in enumerate(twl_partitions):
                            if t_part[0] != 0:
                                print(
                                    f'twl  idx:{t_idx}                      '
                                    f'offset:{t_part[0]:08x} size:{t_part[1]:08x} ',
                                    end='')
                                if twl_part_fstype == 0:
                                    self.files['/twln.img'] = {
                                        'size': t_part[1],
                                        'offset': t_part[0],
                                        'keyslot': Keyslot.TWLNAND,
                                        'type': 'enc'
                                    }
                                    print('/twln.img')
                                    twl_part_fstype += 1
                                elif twl_part_fstype == 1:
                                    self.files['/twlp.img'] = {
                                        'size': t_part[1],
                                        'offset': t_part[0],
                                        'keyslot': Keyslot.TWLNAND,
                                        'type': 'enc'
                                    }
                                    print('/twlp.img')
                                    twl_part_fstype += 1
                                else:
                                    self.files[
                                        f'/twl_unk{twl_part_fstype}.img'] = {
                                            'size': t_part[1],
                                            'offset': t_part[0],
                                            'keyslot': Keyslot.TWLNAND,
                                            'type': 'enc'
                                        }
                                    print(f'/twl_unk{twl_part_fstype}.img')
                                    twl_part_fstype += 1
                else:
                    print('<ctr_twl not set>')

            elif self.ctr:
                if ncsd_part_fstype[idx] == 3:
                    # boot9 hardcoded this keyslot, i'll do this properly later
                    self.files[f'/firm{firm_idx}.bin'] = {
                        'size': part[1],
                        'offset': part[0],
                        'keyslot': Keyslot.FIRM,
                        'type': 'enc'
                    }
                    print(f'/firm{firm_idx}.bin')
                    firm_idx += 1

                elif ncsd_part_fstype[
                        idx] == 1 and ncsd_part_crypttype[idx] >= 2:
                    ctrnand_keyslot = Keyslot.CTRNANDOld if ncsd_part_crypttype[
                        idx] == 2 else Keyslot.CTRNANDNew
                    self.files['/ctrnand_full.img'] = {
                        'size': part[1],
                        'offset': part[0],
                        'keyslot': ctrnand_keyslot,
                        'type': 'enc'
                    }
                    print('/ctrnand_full.img')
                    nand_fp.seek(part[0])
                    iv = self.ctr + (part[0] >> 4)
                    ctr_mbr = self.crypto.create_ctr_cipher(
                        ctrnand_keyslot,
                        iv).decrypt(nand_fp.read(0x200))[0x1BE:0x200]
                    if ctr_mbr[0x40:0x42] == b'\x55\xaa':
                        ctr_partitions = [[
                            readle(ctr_mbr[i + 8:i + 12]) * 0x200,
                            readle(ctr_mbr[i + 12:i + 16]) * 0x200
                        ] for i in range(0, 0x40, 0x10)]
                        ctr_part_fstype = 0
                        for c_idx, c_part in enumerate(ctr_partitions):
                            if c_part[0] != 0:
                                print(
                                    f'ctr  idx:{c_idx}                      offset:{part[0] + c_part[0]:08x} '
                                    f'size:{c_part[1]:08x} ',
                                    end='')
                                if ctr_part_fstype == 0:
                                    self.files['/ctrnand_fat.img'] = {
                                        'size': c_part[1],
                                        'offset': part[0] + c_part[0],
                                        'keyslot': ctrnand_keyslot,
                                        'type': 'enc'
                                    }
                                    print('/ctrnand_fat.img')
                                    ctr_part_fstype += 1
                                else:
                                    self.files[
                                        f'/ctr_unk{ctr_part_fstype}.img'] = {
                                            'size': c_part[1],
                                            'offset': part[0] + c_part[0],
                                            'keyslot': ctrnand_keyslot,
                                            'type': 'enc'
                                        }
                                    print(f'/ctr_unk{ctr_part_fstype}.img')
                                    ctr_part_fstype += 1

                elif ncsd_part_fstype[idx] == 4:
                    self.files['/agbsave.bin'] = {
                        'size': part[1],
                        'offset': part[0],
                        'keyslot': Keyslot.AGB,
                        'type': 'enc'
                    }
                    print('/agbsave.bin')

            else:
                print('<ctr not set>')

        self.readonly = readonly

        # GM9 bonus drive
        if raw_nand_size != self.real_nand_size:
            nand_fp.seek(self.real_nand_size)
            bonus_drive_header = nand_fp.read(0x200)
            if bonus_drive_header[0x1FE:0x200] == b'\x55\xAA':
                self.files['/bonus.img'] = {
                    'size': raw_nand_size - self.real_nand_size,
                    'offset': self.real_nand_size,
                    'keyslot': 0xFF,
                    'type': 'raw'
                }

        self.f = nand_fp

        if exefs is not None:
            exefs_size = sum(
                roundup(x.size, 0x200) for x in exefs.entries.values()) + 0x200
            self.files['/essential.exefs'] = {
                'size': exefs_size,
                'offset': 0x200,
                'keyslot': 0xFF,
                'type': 'raw'
            }
            try:
                self.exefs_fuse = ExeFSMount(exefs, g_stat=g_stat)
                self.exefs_fuse.init('/')
                self._essentials_mounted = True
            except Exception as e:
                print(
                    f'Failed to mount essential.exefs: {type(e).__name__}: {e}'
                )

    def __del__(self, *args):
        try:
            self.f.close()
        except AttributeError:
            pass

    destroy = __del__

    def flush(self, path, fh):
        return self.f.flush()

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        if path.startswith('/essential/'):
            return self.exefs_fuse.getattr(_c.remove_first_dir(path), fh)
        else:
            uid, gid, pid = fuse_get_context()
            if path in {'/', '/essential'}:
                st = {
                    'st_mode': (S_IFDIR | (0o555 if self.readonly else 0o777)),
                    'st_nlink': 2
                }
            elif path in self.files:
                st = {
                    'st_mode': (S_IFREG | (0o444 if self.readonly else 0o666)),
                    'st_size': self.files[path]['size'],
                    'st_nlink': 1
                }
            else:
                raise FuseOSError(ENOENT)
            return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        if path.startswith('/essential'):
            yield from self.exefs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path == '/':
            yield from ('.', '..')
            yield from (x[1:] for x in self.files)
            if self._essentials_mounted:
                yield 'essential'

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        if path.startswith('/essential/'):
            return self.exefs_fuse.read(_c.remove_first_dir(path), size,
                                        offset, fh)
        fi = self.files[path]
        real_offset = fi['offset'] + offset
        if fi['offset'] + offset > fi['offset'] + fi['size']:
            return b''
        if offset + size > fi['size']:
            size = fi['size'] - offset

        if fi['type'] == 'raw':
            self.f.seek(real_offset)
            data = self.f.read(size)

        elif fi['type'] == 'enc':
            self.f.seek(real_offset)
            data = self.f.read(size)
            # thanks Stary2001
            before = offset % 16
            after = (offset + size) % 16
            data = (b'\0' * before) + data + (b'\0' * after)
            iv = (self.ctr if fi['keyslot'] > Keyslot.TWLNAND else
                  self.ctr_twl) + (real_offset >> 4)
            data = self.crypto.create_ctr_cipher(
                fi['keyslot'], iv).decrypt(data)[before:len(data) - after]

        elif fi['type'] in {'keysect', 'twlmbr', 'info'}:
            # being lazy here since twlmbr starts at a weird offset. i'll do
            #   it properly some day. maybe.
            data = fi['content'][offset:offset + size]

        else:
            from pprint import pformat
            print(
                '--------------------------------------------------',
                'Warning: unknown file type (this should not happen!)',
                'Please file an issue or contact the developer with the details below.',
                '  https://github.com/ihaveamac/ninfs/issues',
                '--------------------------------------------------',
                f'{path!r}: {pformat(fi)!r}',
                sep='\n')

            data = b'g' * size

        return data

    @_c.ensure_lower_path
    def statfs(self, path):
        if path.startswith('/essential/'):
            return self.exefs_fuse.statfs(_c.remove_first_dir(path))
        return {
            'f_bsize': 4096,
            'f_blocks': self.real_nand_size // 4096,
            'f_bavail': 0,
            'f_bfree': 0,
            'f_files': len(self.files)
        }

    @_c.ensure_lower_path
    def write(self, path, data, offset, fh):
        if self.readonly:
            raise FuseOSError(EROFS)
        if path.startswith('/essential/'):
            raise FuseOSError(EPERM)
        fi = self.files[path]
        if fi['type'] == 'info':
            raise FuseOSError(EPERM)
        real_offset = fi['offset'] + offset
        real_len = len(data)
        if offset >= fi['size']:
            print('attempt to start writing past file')
            return real_len
        if real_offset + len(data) > fi['offset'] + fi['size']:
            data = data[:-((real_offset + len(data)) - fi['size'])]

        if fi['type'] == 'raw':
            self.f.seek(real_offset)
            self.f.write(data)

        elif fi['type'] == 'enc':
            twl = fi['keyslot'] < Keyslot.CTRNANDOld
            before = offset % 16
            if twl:
                after = 16 - ((offset + real_len) % 16)
                if after == 16:
                    after = 0
            else:
                after = 0  # not needed for ctr
            iv = (self.ctr_twl if twl else self.ctr) + (real_offset >> 4)
            out_data = self.crypto.create_ctr_cipher(
                fi['keyslot'],
                iv).encrypt((b'\0' * before) + data + (b'\0' * after))
            self.f.seek(real_offset)
            self.f.write(out_data[before:])

        elif fi['type'] == 'twlmbr':
            twlmbr = bytearray(fi['content'])
            twlmbr[offset:offset + len(data)] = data
            final = bytes(twlmbr)
            self.f.seek(fi['offset'])
            self.f.write(
                self.crypto.create_ctr_cipher(fi['keyslot'], self.ctr_twl +
                                              0x1B).encrypt((b'\0' * 0xE) +
                                                            final)[0xE:0x50])
            # noinspection PyTypeChecker
            fi['content'] = final

        elif fi['type'] == 'keysect':
            keysect = bytearray(fi['content'])
            keysect[offset:offset + len(data)] = data
            final = bytes(keysect)
            cipher_keysect = self.crypto.create_ecb_cipher(0x11)
            self.f.seek(fi['offset'])
            self.f.write(cipher_keysect.encrypt(final))
            # noinspection PyTypeChecker
            fi['content'] = final

        return real_len
Beispiel #14
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)
Beispiel #15
0
class TWLNandImageMount(LoggingMixIn, Operations):
    fd = 0

    def __init__(self,
                 nand_fp: BinaryIO,
                 g_stat: os.stat,
                 consoleid: str,
                 cid: str = None,
                 readonly: bool = False):
        self.crypto = CryptoEngine(setup_b9_keys=False)
        self.readonly = readonly

        self.g_stat = {
            'st_ctime': int(g_stat.st_ctime),
            'st_mtime': int(g_stat.st_mtime),
            'st_atime': int(g_stat.st_atime)
        }

        self.files = {}

        res = nand_fp.seek(0, 2)
        if res == 0xF000200:
            self.files['/nocash_blk.bin'] = {
                'offset': 0xF000000,
                'size': 0x200,
                'type': 'dec'
            }
        elif res != 0xF000000:
            exit(
                f'Unknown NAND size (expected 0xF000000 or 0xF000200, got {res:#09X}'
            )

        nand_fp.seek(0)

        try:
            consoleid = bytes.fromhex(consoleid)
        except (ValueError, TypeError):
            try:
                with open(consoleid, 'rb') as f:
                    consoleid = f.read(0x10)
            except (FileNotFoundError, TypeError):
                exit(
                    'Failed to convert Console ID to bytes, or file did not exist.'
                )

        twl_consoleid_list = (readbe(consoleid[4:8]), readbe(consoleid[0:4]))

        key_x_list = [
            twl_consoleid_list[0], twl_consoleid_list[0] ^ 0x24EE6906,
            twl_consoleid_list[1] ^ 0xE65B601D, twl_consoleid_list[1]
        ]

        self.crypto.set_keyslot('x', 0x03, pack('<4I', *key_x_list))

        header_enc = nand_fp.read(0x200)

        if cid:
            try:
                cid = bytes.fromhex(cid)
            except ValueError:
                try:
                    with open(cid, 'rb') as f:
                        cid = f.read(0x10)
                except FileNotFoundError:
                    exit(
                        'Failed to convert CID to bytes, or file did not exist.'
                    )
            self.ctr = readle(sha1(cid).digest()[0:16])

        else:
            # attempt to generate counter
            block_0x1c = readbe(header_enc[0x1C0:0x1D0])
            blk_xored = block_0x1c ^ 0x1804060FE03B77080000896F06000002
            ctr_offs = self.crypto.create_ecb_cipher(0x03).decrypt(
                blk_xored.to_bytes(0x10, 'little'))
            self.ctr = int.from_bytes(ctr_offs, 'big') - 0x1C

            # try the counter
            block_0x1d = header_enc[0x1D0:0x1E0]
            out = self.crypto.create_ctr_cipher(0x03, self.ctr +
                                                0x1D).decrypt(block_0x1d)
            if out != b'\xce<\x06\x0f\xe0\xbeMx\x06\x00\xb3\x05\x01\x00\x00\x02':
                exit(
                    'Counter could not be automatically generated. Please provide the CID, '
                    'or ensure the provided Console ID is correct..')
            print('Counter automatically generated.')

        self.files['/stage2_infoblk1.bin'] = {
            'offset': 0x200,
            'size': 0x200,
            'type': 'dec'
        }
        self.files['/stage2_infoblk2.bin'] = {
            'offset': 0x400,
            'size': 0x200,
            'type': 'dec'
        }
        self.files['/stage2_infoblk3.bin'] = {
            'offset': 0x600,
            'size': 0x200,
            'type': 'dec'
        }
        self.files['/stage2_bootldr.bin'] = {
            'offset': 0x800,
            'size': 0x4DC00,
            'type': 'dec'
        }
        self.files['/stage2_footer.bin'] = {
            'offset': 0x4E400,
            'size': 0x400,
            'type': 'dec'
        }
        self.files['/diag_area.bin'] = {
            'offset': 0xFFA00,
            'size': 0x400,
            'type': 'dec'
        }

        header = self.crypto.create_ctr_cipher(0x03,
                                               self.ctr).decrypt(header_enc)
        mbr = header[0x1BE:0x200]
        if mbr[0x40:0x42] != b'\x55\xaa':
            exit(
                'MBR signature not found. Make sure the provided Console ID and CID are correct.'
            )
        partitions = [[
            readle(mbr[i + 8:i + 12]) * 0x200,
            readle(mbr[i + 12:i + 16]) * 0x200
        ] for i in range(0, 0x40, 0x10)]

        for idx, part in enumerate(partitions):
            if part[0]:
                ptype = 'enc' if idx < 2 else 'dec'
                pname = ('twl_main', 'twl_photo', 'twl_unk1', 'twk_unk2')[idx]
                self.files[f'/{pname}.img'] = {
                    'offset': part[0],
                    'size': part[1],
                    'type': ptype
                }

        self.f = nand_fp

    def __del__(self, *args):
        try:
            self.f.close()
        except AttributeError:
            pass

    destroy = __del__

    def flush(self, path, fh):
        return self.f.flush()

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        uid, gid, pid = fuse_get_context()
        if path == '/':
            st = {
                'st_mode': (S_IFDIR | (0o555 if self.readonly else 0o777)),
                'st_nlink': 2
            }
        elif path in self.files:
            st = {
                'st_mode': (S_IFREG | (0o444 if self.readonly else 0o666)),
                'st_size': self.files[path]['size'],
                'st_nlink': 1
            }
        else:
            raise FuseOSError(ENOENT)
        return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        yield from ('.', '..')
        yield from (x[1:] for x in self.files)

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        fi = self.files[path]
        real_offset = fi['offset'] + offset
        if fi['offset'] + offset > fi['offset'] + fi['size']:
            return b''
        if offset + size > fi['size']:
            size = fi['size'] - offset

        self.f.seek(real_offset)
        data = self.f.read(size)
        if fi['type'] == 'enc':
            before = offset % 16
            after = (offset + size) % 16
            data = (b'\0' * before) + data + (b'\0' * after)
            iv = self.ctr + (real_offset >> 4)
            data = self.crypto.create_ctr_cipher(
                0x03, iv).decrypt(data)[before:len(data) - after]

        return data

    @_c.ensure_lower_path
    def statfs(self, path):
        return {
            'f_bsize': 4096,
            'f_blocks': 0xF000000 // 4096,
            'f_bavail': 0,
            'f_bfree': 0,
            'f_files': len(self.files)
        }

    @_c.ensure_lower_path
    def write(self, path, data, offset, fh):
        if self.readonly:
            raise FuseOSError(EROFS)

        fi = self.files[path]
        real_offset = fi['offset'] + offset
        real_len = len(data)
        if offset >= fi['size']:
            print('attempt to start writing past file')
            return real_len
        if real_offset + len(data) > fi['offset'] + fi['size']:
            data = data[:-((real_offset + len(data)) - fi['size'])]

        if fi['type'] == 'dec':
            self.f.seek(real_offset)
            self.f.write(data)

        else:
            before = offset % 16
            after = 16 - ((offset + real_len) % 16)
            if after == 16:
                after = 0
            iv = self.ctr + (real_offset >> 4)
            data = (b'\0' * before) + data + (b'\0' * after)
            out_data = self.crypto.create_ctr_cipher(
                0x03, iv).encrypt(data)[before:real_len - after]
            self.f.seek(real_offset)
            self.f.write(out_data)

        return real_len
Beispiel #16
0
 def path_to_iv(self, path):
     return CryptoEngine.sd_path_to_iv(path[self.root_len + 33:])
Beispiel #17
0
class SDFilesystemMount(LoggingMixIn, Operations):
    fd = 0

    @_c.ensure_lower_path
    def path_to_iv(self, path):
        return CryptoEngine.sd_path_to_iv(path[self.root_len + 33:])

    def __init__(self,
                 sd_dir: str,
                 movable: bytes,
                 dev: bool = False,
                 readonly: bool = False,
                 boot9: str = None):
        self.crypto = CryptoEngine(boot9=boot9, dev=dev)

        self.crypto.setup_sd_key(movable)
        self.root_dir = self.crypto.id0.hex()

        if not os.path.isdir(sd_dir + '/' + self.root_dir):
            exit(f'Failed to find {self.root_dir} in the SD dir.')

        print('Root dir: ' + self.root_dir)
        print('Key:      ' + self.crypto.keygen(Keyslot.SD).hex())

        self.root = os.path.realpath(sd_dir + '/' + self.root_dir)
        self.root_len = len(self.root)

        self.readonly = readonly

    # noinspection PyMethodOverriding
    def __call__(self, op, path, *args):
        return super().__call__(op, self.root + path, *args)

    def access(self, path, mode):
        if not os.access(path, mode):
            raise FuseOSError(EACCES)

    @_c.raise_on_readonly
    def chmod(self, path, mode):
        os.chmod(path, mode)

    @_c.raise_on_readonly
    def chown(self, path, *args, **kwargs):
        if not _c.windows:
            os.chown(path, *args, **kwargs)

    @_c.raise_on_readonly
    def create(self, path, mode, **kwargs):
        fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
        os.close(fd)
        self.fd += 1
        return self.fd

    def getattr(self, path, fh=None):
        st = os.lstat(path)
        uid, gid, _ = fuse_get_context()
        res = {
            key: getattr(st, key)
            for key in ('st_atime', 'st_ctime', 'st_mode', 'st_mtime',
                        'st_nlink', 'st_size', 'st_flags') if hasattr(st, key)
        }
        res['st_uid'] = st.st_uid if st.st_uid != 0 else uid
        res['st_gid'] = st.st_gid if st.st_gid != 0 else gid
        return res

    getxattr = None

    def link(self, target, source):
        return os.link(source, target)

    listxattr = None

    @_c.raise_on_readonly
    def mkdir(self, path, *args, **kwargs):
        os.mkdir(path, *args, **kwargs)

    @_c.raise_on_readonly
    def mknod(self, path, *args, **kwargs):
        if not _c.windows:
            os.mknod(path, *args, **kwargs)

    # open = os.open
    def open(self, path, flags):
        self.fd += 1
        return self.fd

    def read(self, path, size, offset, fh):
        # special check for special files
        if os.path.basename(path).startswith(
                '.') or 'nintendo dsiware' in path:
            with open(path, 'rb') as f:
                f.seek(offset)
                return f.read(size)

        before = offset % 16
        with open(path, 'rb') as f:
            f.seek(offset - before)
            data = f.read(size + before)
        iv = self.path_to_iv(path) + (offset >> 4)
        return self.crypto.create_ctr_cipher(Keyslot.SD,
                                             iv).decrypt(data)[before:]

    def readdir(self, path, fh):
        yield from ('.', '..')
        ld = os.listdir(path)
        if _c.windows:
            # I should figure out how to mark hidden files, if possible
            yield from (d for d in ld if not d.startswith('.'))
        else:
            yield from ld

    readlink = os.readlink

    @_c.raise_on_readonly
    def rename(self, old, new):
        # renaming's too difficult. just copy the file to the name you want if you really need it.
        raise FuseOSError(EPERM)

    @_c.raise_on_readonly
    def rmdir(self, path):
        os.rmdir(path)

    # noinspection PyPep8Naming
    def statfs(self, path):
        if _c.windows:
            lpSectorsPerCluster = c_ulonglong(0)
            lpBytesPerSector = c_ulonglong(0)
            lpNumberOfFreeClusters = c_ulonglong(0)
            lpTotalNumberOfClusters = c_ulonglong(0)
            ret = windll.kernel32.GetDiskFreeSpaceW(
                c_wchar_p(path), pointer(lpSectorsPerCluster),
                pointer(lpBytesPerSector), pointer(lpNumberOfFreeClusters),
                pointer(lpTotalNumberOfClusters))
            if not ret:
                raise WindowsError
            free_blocks = lpNumberOfFreeClusters.value * lpSectorsPerCluster.value
            result = {
                'f_bavail': free_blocks,
                'f_bfree': free_blocks,
                'f_bsize': lpBytesPerSector.value,
                'f_frsize': lpBytesPerSector.value,
                'f_blocks':
                lpTotalNumberOfClusters.value * lpSectorsPerCluster.value,
                'f_namemax': wintypes.MAX_PATH
            }
            return result
        else:
            stv = os.statvfs(path)
            # f_flag causes python interpreter crashes in some cases. i don't get it.
            return {
                key: getattr(stv, key)
                for key in ('f_bavail', 'f_bfree', 'f_blocks', 'f_bsize',
                            'f_favail', 'f_ffree', 'f_files', 'f_frsize',
                            'f_namemax')
            }

    def symlink(self, target, source):
        return os.symlink(source, target)

    def truncate(self, path, length, fh=None):
        with open(path, 'r+b') as f:
            f.truncate(length)

    @_c.raise_on_readonly
    def unlink(self, path, *args, **kwargs):
        os.unlink(path)

    @_c.raise_on_readonly
    def utimens(self, path, *args, **kwargs):
        os.utime(path, *args, **kwargs)

    @_c.raise_on_readonly
    def write(self, path, data, offset, fh):
        # special check for special files
        if os.path.basename(path).startswith(
                '.') or 'nintendo dsiware' in path.lower():
            with open(path, 'rb+') as f:
                f.seek(offset)
                return f.write(data)

        before = offset % 16
        iv = self.path_to_iv(path) + (offset >> 4)
        out_data = self.crypto.create_ctr_cipher(
            Keyslot.SD, iv).decrypt((b'\0' * before) + data)[before:]

        with open(path, 'rb+') as f:
            f.seek(offset)
            return f.write(out_data)
Beispiel #18
0
parser = ArgumentParser(description='Rebuilds 3DS Title Database.')
parser.add_argument('-b', '--boot9', help='boot9')
parser.add_argument('-m', '--movable', help='movable.sed', required=True)
parser.add_argument('-s',
                    '--sd',
                    help='SD card (containing "Nintendo 3DS")',
                    required=True)
parser.add_argument('-o',
                    '--output',
                    help='output directory for title info entries',
                    required=True)

args = parser.parse_args()

crypto = CryptoEngine(boot9=args.boot9)
crypto.setup_sd_key_from_file(args.movable)

out = Path(args.output)
out.mkdir(exist_ok=True)

id0 = Path(args.sd) / 'Nintendo 3DS' / crypto.id0.hex()

# Only continue if there is one id1 directory.
# If there isn't, the user needs to remove the unwanted ones.
id1_list = [x for x in id0.iterdir() if len(x.parts[-1]) == 32]
if len(id1_list) > 1:
    print('There are multiple id1 directories in', id0, file=sys.stderr)
    print('Please remove the rest.', file=sys.stderr)
    sys.exit(1)
elif len(id1_list) < 1:
Beispiel #19
0
class SDFilesystemMount(LoggingMixIn, Operations):
    @_c.ensure_lower_path
    def path_to_iv(self, path):
        path_hash = sha256(path[self.root_len + 33:].encode('utf-16le') +
                           b'\0\0').digest()
        hash_p1 = readbe(path_hash[0:16])
        hash_p2 = readbe(path_hash[16:32])
        return hash_p1 ^ hash_p2

    def __init__(self,
                 sd_dir: str,
                 movable: str,
                 dev: bool = False,
                 readonly: bool = False):
        self.crypto = CryptoEngine(dev=dev)

        with open(movable, 'rb') as mv:
            mv.seek(0x110)
            key_y = mv.read(0x10)
        key_hash = sha256(key_y).digest()
        hash_parts = unpack('<IIII', key_hash[0:16])
        self.root_dir = f'{hash_parts[0]:08x}{hash_parts[1]:08x}{hash_parts[2]:08x}{hash_parts[3]:08x}'

        if not os.path.isdir(sd_dir + '/' + self.root_dir):
            exit(f'Failed to find {self.root_dir} in the SD dir.')

        self.fds: Dict[int, BinaryIO] = {}

        print('Root dir: ' + self.root_dir)

        self.crypto.set_keyslot('y', 0x34, readbe(key_y))
        print('Key:      ' + self.crypto.key_normal[0x34].hex())

        self.root = os.path.realpath(sd_dir + '/' + self.root_dir)
        self.root_len = len(self.root)

        self.readonly = readonly

    # noinspection PyMethodOverriding
    def __call__(self, op, path, *args):
        return super().__call__(op, self.root + path, *args)

    def __del__(self, *args):
        # putting the keys in a tuple so the dict can be modified
        with suppress(AttributeError):
            for f in tuple(self.fds):
                with suppress(KeyError):
                    self.fds[f].close()
                    del self.fds[f]

    destroy = __del__

    def access(self, path, mode):
        if not os.access(path, mode):
            raise FuseOSError(EACCES)

    @_c.raise_on_readonly
    def chmod(self, path, mode):
        os.chmod(path, mode)

    @_c.raise_on_readonly
    def chown(self, path, *args, **kwargs):
        if not _c.windows:
            os.chown(path, *args, **kwargs)

    @_c.raise_on_readonly
    def create(self, path, mode, **kwargs):
        fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
        self.fds[fd] = os.fdopen(fd, 'wb')
        return fd

    def flush(self, path, fh):
        try:
            os.fsync(fh)
        except OSError as e:
            # I am not sure why this is happening on Windows. if anyone can give me a hint, please do.
            if e.errno != EBADF:  # "Bad file descriptor"
                raise
        return

    def fsync(self, path, datasync, fh):
        self.flush(path, fh)
        return

    def getattr(self, path, fh=None):
        st = os.lstat(path)
        uid, gid, _ = fuse_get_context()
        res = {
            key: getattr(st, key)
            for key in ('st_atime', 'st_ctime', 'st_mode', 'st_mtime',
                        'st_nlink', 'st_size', 'st_flags') if hasattr(st, key)
        }
        res['st_uid'] = st.st_uid if st.st_uid != 0 else uid
        res['st_gid'] = st.st_gid if st.st_gid != 0 else gid
        return res

    getxattr = None

    def link(self, target, source):
        return os.link(source, target)

    listxattr = None

    @_c.raise_on_readonly
    def mkdir(self, path, *args, **kwargs):
        os.mkdir(path, *args, **kwargs)

    @_c.raise_on_readonly
    def mknod(self, path, *args, **kwargs):
        if not _c.windows:
            os.mknod(path, *args, **kwargs)

    # open = os.open
    def open(self, path, flags):
        f = open(path, 'rb+', buffering=-1)
        self.fds[f.fileno()] = f
        return f.fileno()

    def read(self, path, size, offset, fh):
        f = self.fds[fh]
        # special check for special files
        if os.path.basename(path).startswith(
                '.') or 'nintendo dsiware' in path:
            f.seek(offset)
            return f.read(size)

        before = offset % 16
        f.seek(offset - before)
        data = f.read(size + before)
        iv = self.path_to_iv(path) + (offset >> 4)
        return self.crypto.create_ctr_cipher(0x34, iv).decrypt(data)[before:]

    def readdir(self, path, fh):
        yield from ('.', '..')
        ld = os.listdir(path)
        if _c.windows:
            # I should figure out how to mark hidden files, if possible
            yield from (d for d in ld if not d.startswith('.'))
        else:
            yield from ld

    readlink = os.readlink

    def release(self, path, fh):
        self.fds[fh].close()
        del self.fds[fh]

    @_c.raise_on_readonly
    def rename(self, old, new):
        # renaming's too difficult. just copy the file to the name you want if you really need it.
        raise FuseOSError(EPERM)

    @_c.raise_on_readonly
    def rmdir(self, path):
        os.rmdir(path)

    # noinspection PyPep8Naming
    def statfs(self, path):
        if _c.windows:
            lpSectorsPerCluster = c_ulonglong(0)
            lpBytesPerSector = c_ulonglong(0)
            lpNumberOfFreeClusters = c_ulonglong(0)
            lpTotalNumberOfClusters = c_ulonglong(0)
            ret = windll.kernel32.GetDiskFreeSpaceW(
                c_wchar_p(path), pointer(lpSectorsPerCluster),
                pointer(lpBytesPerSector), pointer(lpNumberOfFreeClusters),
                pointer(lpTotalNumberOfClusters))
            if not ret:
                raise WindowsError
            free_blocks = lpNumberOfFreeClusters.value * lpSectorsPerCluster.value
            result = {
                'f_bavail': free_blocks,
                'f_bfree': free_blocks,
                'f_bsize': lpBytesPerSector.value,
                'f_frsize': lpBytesPerSector.value,
                'f_blocks':
                lpTotalNumberOfClusters.value * lpSectorsPerCluster.value,
                'f_namemax': wintypes.MAX_PATH
            }
            return result
        else:
            stv = os.statvfs(path)
            # f_flag causes python interpreter crashes in some cases. i don't get it.
            return {
                key: getattr(stv, key)
                for key in ('f_bavail', 'f_bfree', 'f_blocks', 'f_bsize',
                            'f_favail', 'f_ffree', 'f_files', 'f_frsize',
                            'f_namemax')
            }

    def symlink(self, target, source):
        return os.symlink(source, target)

    def truncate(self, path, length, fh=None):
        if fh is None:
            with open(path, 'r+b') as f:
                f.truncate(length)
        else:
            f = self.fds[fh]
            f.truncate(length)

    @_c.raise_on_readonly
    def unlink(self, path, *args, **kwargs):
        os.unlink(path)

    @_c.raise_on_readonly
    def utimens(self, path, *args, **kwargs):
        os.utime(path, *args, **kwargs)

    @_c.raise_on_readonly
    def write(self, path, data, offset, fh):
        f = self.fds[fh]
        # special check for special files
        if os.path.basename(path).startswith(
                '.') or 'nintendo dsiware' in path.lower():
            f.seek(offset)
            return f.write(data)

        before = offset % 16
        iv = self.path_to_iv(path) + (offset >> 4)
        out_data = self.crypto.create_ctr_cipher(
            0x34, iv).decrypt((b'\0' * before) + data)[before:]
        f.seek(offset)
        return f.write(out_data)
Beispiel #20
0
    def __init__(self,
                 nand_fp: BinaryIO,
                 g_stat: os.stat,
                 consoleid: str,
                 cid: str = None,
                 readonly: bool = False):
        self.crypto = CryptoEngine(setup_b9_keys=False)
        self.readonly = readonly

        self.g_stat = {
            'st_ctime': int(g_stat.st_ctime),
            'st_mtime': int(g_stat.st_mtime),
            'st_atime': int(g_stat.st_atime)
        }

        self.files = {}

        res = nand_fp.seek(0, 2)
        if res == 0xF000200:
            self.files['/nocash_blk.bin'] = {
                'offset': 0xF000000,
                'size': 0x200,
                'type': 'dec'
            }
        elif res != 0xF000000:
            exit(
                f'Unknown NAND size (expected 0xF000000 or 0xF000200, got {res:#09X}'
            )

        nand_fp.seek(0)

        try:
            consoleid = bytes.fromhex(consoleid)
        except (ValueError, TypeError):
            try:
                with open(consoleid, 'rb') as f:
                    consoleid = f.read(0x10)
            except (FileNotFoundError, TypeError):
                exit(
                    'Failed to convert Console ID to bytes, or file did not exist.'
                )

        twl_consoleid_list = (readbe(consoleid[4:8]), readbe(consoleid[0:4]))

        key_x_list = [
            twl_consoleid_list[0], twl_consoleid_list[0] ^ 0x24EE6906,
            twl_consoleid_list[1] ^ 0xE65B601D, twl_consoleid_list[1]
        ]

        self.crypto.set_keyslot('x', 0x03, pack('<4I', *key_x_list))

        header_enc = nand_fp.read(0x200)

        if cid:
            try:
                cid = bytes.fromhex(cid)
            except ValueError:
                try:
                    with open(cid, 'rb') as f:
                        cid = f.read(0x10)
                except FileNotFoundError:
                    exit(
                        'Failed to convert CID to bytes, or file did not exist.'
                    )
            self.ctr = readle(sha1(cid).digest()[0:16])

        else:
            # attempt to generate counter
            block_0x1c = readbe(header_enc[0x1C0:0x1D0])
            blk_xored = block_0x1c ^ 0x1804060FE03B77080000896F06000002
            ctr_offs = self.crypto.create_ecb_cipher(0x03).decrypt(
                blk_xored.to_bytes(0x10, 'little'))
            self.ctr = int.from_bytes(ctr_offs, 'big') - 0x1C

            # try the counter
            block_0x1d = header_enc[0x1D0:0x1E0]
            out = self.crypto.create_ctr_cipher(0x03, self.ctr +
                                                0x1D).decrypt(block_0x1d)
            if out != b'\xce<\x06\x0f\xe0\xbeMx\x06\x00\xb3\x05\x01\x00\x00\x02':
                exit(
                    'Counter could not be automatically generated. Please provide the CID, '
                    'or ensure the provided Console ID is correct..')
            print('Counter automatically generated.')

        self.files['/stage2_infoblk1.bin'] = {
            'offset': 0x200,
            'size': 0x200,
            'type': 'dec'
        }
        self.files['/stage2_infoblk2.bin'] = {
            'offset': 0x400,
            'size': 0x200,
            'type': 'dec'
        }
        self.files['/stage2_infoblk3.bin'] = {
            'offset': 0x600,
            'size': 0x200,
            'type': 'dec'
        }
        self.files['/stage2_bootldr.bin'] = {
            'offset': 0x800,
            'size': 0x4DC00,
            'type': 'dec'
        }
        self.files['/stage2_footer.bin'] = {
            'offset': 0x4E400,
            'size': 0x400,
            'type': 'dec'
        }
        self.files['/diag_area.bin'] = {
            'offset': 0xFFA00,
            'size': 0x400,
            'type': 'dec'
        }

        header = self.crypto.create_ctr_cipher(0x03,
                                               self.ctr).decrypt(header_enc)
        mbr = header[0x1BE:0x200]
        if mbr[0x40:0x42] != b'\x55\xaa':
            exit(
                'MBR signature not found. Make sure the provided Console ID and CID are correct.'
            )
        partitions = [[
            readle(mbr[i + 8:i + 12]) * 0x200,
            readle(mbr[i + 12:i + 16]) * 0x200
        ] for i in range(0, 0x40, 0x10)]

        for idx, part in enumerate(partitions):
            if part[0]:
                ptype = 'enc' if idx < 2 else 'dec'
                pname = ('twl_main', 'twl_photo', 'twl_unk1', 'twk_unk2')[idx]
                self.files[f'/{pname}.img'] = {
                    'offset': part[0],
                    'size': part[1],
                    'type': ptype
                }

        self.f = nand_fp
from argparse import ArgumentParser
import gzip
from hashlib import sha256
from pathlib import Path
import sys

from pyctr.crypto import CryptoEngine, Keyslot

parser = ArgumentParser(description='Copy a clean version of title.db and import.db.')
parser.add_argument('-b', '--boot9', help='boot9')
parser.add_argument('-m', '--movable', help='movable.sed', required=True)
parser.add_argument('-s', '--sd', help='SD card (containing "Nintendo 3DS")')

args = parser.parse_args()

crypto = CryptoEngine(boot9=args.boot9)
crypto.setup_sd_key_from_file(args.movable)

id0 = Path(args.sd) / 'Nintendo 3DS' / crypto.id0.hex()

# Only continue if there is one id1 directory.
# If there isn't, the user needs to remove the unwanted ones.
id1_list = [x for x in id0.iterdir() if len(x.parts[-1]) == 32]
if len(id1_list) > 1:
    print('There are multiple id1 directories in', id0, file=sys.stderr)
    print('Please remove the rest.', file=sys.stderr)
    sys.exit(1)
elif len(id1_list) < 1:
    print('No id1 directory could be found in', id0, file=sys.stderr)
    sys.exit(2)
Beispiel #22
0
    def __init__(self,
                 nand_fp: BinaryIO,
                 g_stat: dict,
                 dev: bool = False,
                 readonly: bool = False,
                 otp: bytes = None,
                 cid: AnyStr = None,
                 boot9: str = None):
        self.crypto = CryptoEngine(boot9=boot9, dev=dev)

        self.g_stat = g_stat

        nand_fp.seek(0x100)  # screw the signature
        ncsd_header = nand_fp.read(0x100)
        if ncsd_header[0:4] != b'NCSD':
            exit(
                'NCSD magic not found, is this a real Nintendo 3DS NAND image?'
            )
        media_id = ncsd_header[0x8:0x10]
        if media_id != b'\0' * 8:
            exit(
                'Media ID not all-zero, is this a real Nintendo 3DS NAND image?'
            )

        # check for essential.exefs
        nand_fp.seek(0x200)
        try:
            exefs = ExeFSReader(nand_fp)
        except InvalidExeFSError:
            exefs = None

        otp_data = None
        if otp:
            try:
                with open(otp, 'rb') as f:
                    otp_data = f.read(0x200)
            except Exception:
                print(f'Failed to open and read given OTP ({otp}).\n')
                print_exc()
                exit(1)

        else:
            if exefs is None:
                exit(
                    'OTP not found, provide with --otp or embed essentials backup with GodMode9'
                )
            else:
                try:
                    with exefs.open('otp') as otp:
                        otp_data = otp.read(0x200)
                except ExeFSFileNotFoundError:
                    exit(
                        '"otp" not found in essentials backup, update with GodMode9 or provide with --otp'
                    )

        self.crypto.setup_keys_from_otp(otp_data)

        def generate_ctr():
            print(
                'Attempting to generate Counter for CTR/TWL areas. If errors occur, provide the CID manually.'
            )

            # -------------------------------------------------- #
            # attempt to generate CTR Counter
            nand_fp.seek(0xB9301D0)
            # these blocks are assumed to be entirely 00, so no need to xor anything
            ctrn_block_0x1d = nand_fp.read(0x10)
            ctrn_block_0x1e = nand_fp.read(0x10)
            for ks in (Keyslot.CTRNANDOld, Keyslot.CTRNANDNew):
                ctr_counter_offs = self.crypto.create_ecb_cipher(ks).decrypt(
                    ctrn_block_0x1d)
                ctr_counter = int.from_bytes(ctr_counter_offs,
                                             'big') - 0xB9301D

                # try the counter
                out = self.crypto.create_ctr_cipher(
                    ks, ctr_counter + 0xB9301E).decrypt(ctrn_block_0x1e)
                if out == b'\0' * 16:
                    print('Counter for CTR area automatically generated.')
                    self.ctr = ctr_counter
                    break
            else:
                print(
                    'Counter could not be generated for CTR area. Related virtual files will not appear.'
                )
                self.ctr = None

            # -------------------------------------------------- #
            # attempt to generate TWL Counter
            nand_fp.seek(0x1C0)
            twln_block_0x1c = readbe(nand_fp.read(0x10))
            twl_blk_xored = twln_block_0x1c ^ 0x18000601A03F97000000A97D04000004
            twl_counter_offs = self.crypto.create_ecb_cipher(0x03).decrypt(
                twl_blk_xored.to_bytes(0x10, 'little'))
            twl_counter = int.from_bytes(twl_counter_offs, 'big') - 0x1C

            # try the counter
            twln_block_0x1d = nand_fp.read(0x10)
            out = self.crypto.create_ctr_cipher(0x03, twl_counter +
                                                0x1D).decrypt(twln_block_0x1d)
            if out == b'\x8e@\x06\x01\xa0\xc3\x8d\x80\x04\x00\xb3\x05\x01\x00\x00\x00':
                print('Counter for TWL area automatically generated.')
                self.ctr_twl = twl_counter
            else:
                print(
                    'Counter could not be generated for TWL area. Related virtual files will not appear.'
                )
                self.ctr_twl = None

        cid_data = None
        if cid:
            try:
                with open(cid, 'rb') as f:
                    cid_data = f.read(0x200)
            except Exception:
                print(f'Failed to open and read given CID ({cid}).')
                print(
                    'If you want to attempt Counter generation, do not provide a CID path.\n'
                )
                print_exc()
                exit(1)

        else:
            if exefs is None:
                generate_ctr()
            else:
                try:
                    with exefs.open('nand_cid') as cid:
                        cid_data = cid.read(0x10)
                except ExeFSFileNotFoundError:
                    print(
                        '"nand_cid" not found in essentials backup, update with GodMode9 or provide with --cid'
                    )
                    generate_ctr()

        if cid_data:
            self.ctr = readbe(sha256(cid_data).digest()[0:16])
            self.ctr_twl = readle(sha1(cid_data).digest()[0:16])

        if not (self.ctr or self.ctr_twl):
            exit("Couldn't generate Counter for both CTR/TWL. "
                 "Make sure the OTP is correct, or provide the CID manually.")

        nand_fp.seek(0, 2)
        raw_nand_size = nand_fp.tell()

        self.real_nand_size = nand_size[readle(ncsd_header[4:8])]

        self.files = {
            '/nand_hdr.bin': {
                'size': 0x200,
                'offset': 0,
                'keyslot': 0xFF,
                'type': 'raw'
            },
            '/nand.bin': {
                'size': raw_nand_size,
                'offset': 0,
                'keyslot': 0xFF,
                'type': 'raw'
            },
            '/nand_minsize.bin': {
                'size': self.real_nand_size,
                'offset': 0,
                'keyslot': 0xFF,
                'type': 'raw'
            }
        }

        nand_fp.seek(0x12C00)
        keysect_enc = nand_fp.read(0x200)
        if len(set(keysect_enc)) != 1:
            keysect_dec = self.crypto.create_ecb_cipher(0x11).decrypt(
                keysect_enc)
            # i'm cheating here by putting the decrypted version in memory and
            #   not reading from the image every time. but it's not AES-CTR so
            #   f**k that.
            self.files['/sector0x96.bin'] = {
                'size': 0x200,
                'offset': 0x12C00,
                'keyslot': 0x11,
                'type': 'keysect',
                'content': keysect_dec
            }

        ncsd_part_fstype = ncsd_header[0x10:0x18]
        ncsd_part_crypttype = ncsd_header[0x18:0x20]
        ncsd_part_raw = ncsd_header[0x20:0x60]
        ncsd_partitions = [[
            readle(ncsd_part_raw[i:i + 4]) * 0x200,
            readle(ncsd_part_raw[i + 4:i + 8]) * 0x200
        ] for i in range(0, 0x40, 0x8)]

        # including padding for crypto
        if self.ctr_twl:
            twl_mbr = self.crypto.create_ctr_cipher(
                Keyslot.TWLNAND,
                self.ctr_twl + 0x1B).decrypt(ncsd_header[0xB0:0x100])[0xE:0x50]
            if twl_mbr[0x40:0x42] == b'\x55\xaa':
                twl_partitions = [[
                    readle(twl_mbr[i + 8:i + 12]) * 0x200,
                    readle(twl_mbr[i + 12:i + 16]) * 0x200
                ] for i in range(0, 0x40, 0x10)]
            else:
                twl_partitions = None

            self.files['/twlmbr.bin'] = {
                'size': 0x42,
                'offset': 0x1BE,
                'keyslot': Keyslot.TWLNAND,
                'type': 'twlmbr',
                'content': twl_mbr
            }
        else:
            twl_partitions = None

        # then actually parse the partitions to create files
        firm_idx = 0
        for idx, part in enumerate(ncsd_partitions):
            if ncsd_part_fstype[idx] == 0:
                continue
            print(
                f'ncsd idx:{idx} fstype:{ncsd_part_fstype[idx]} crypttype:{ncsd_part_crypttype[idx]} '
                f'offset:{part[0]:08x} size:{part[1]:08x} ',
                end='')
            if idx == 0:
                if self.ctr_twl:
                    self.files['/twl_full.img'] = {
                        'size': part[1],
                        'offset': part[0],
                        'keyslot': Keyslot.TWLNAND,
                        'type': 'enc'
                    }
                    print('/twl_full.img')
                    if twl_partitions:
                        twl_part_fstype = 0
                        for t_idx, t_part in enumerate(twl_partitions):
                            if t_part[0] != 0:
                                print(
                                    f'twl  idx:{t_idx}                      '
                                    f'offset:{t_part[0]:08x} size:{t_part[1]:08x} ',
                                    end='')
                                if twl_part_fstype == 0:
                                    self.files['/twln.img'] = {
                                        'size': t_part[1],
                                        'offset': t_part[0],
                                        'keyslot': Keyslot.TWLNAND,
                                        'type': 'enc'
                                    }
                                    print('/twln.img')
                                    twl_part_fstype += 1
                                elif twl_part_fstype == 1:
                                    self.files['/twlp.img'] = {
                                        'size': t_part[1],
                                        'offset': t_part[0],
                                        'keyslot': Keyslot.TWLNAND,
                                        'type': 'enc'
                                    }
                                    print('/twlp.img')
                                    twl_part_fstype += 1
                                else:
                                    self.files[
                                        f'/twl_unk{twl_part_fstype}.img'] = {
                                            'size': t_part[1],
                                            'offset': t_part[0],
                                            'keyslot': Keyslot.TWLNAND,
                                            'type': 'enc'
                                        }
                                    print(f'/twl_unk{twl_part_fstype}.img')
                                    twl_part_fstype += 1
                else:
                    print('<ctr_twl not set>')

            elif self.ctr:
                if ncsd_part_fstype[idx] == 3:
                    # boot9 hardcoded this keyslot, i'll do this properly later
                    self.files[f'/firm{firm_idx}.bin'] = {
                        'size': part[1],
                        'offset': part[0],
                        'keyslot': Keyslot.FIRM,
                        'type': 'enc'
                    }
                    print(f'/firm{firm_idx}.bin')
                    firm_idx += 1

                elif ncsd_part_fstype[
                        idx] == 1 and ncsd_part_crypttype[idx] >= 2:
                    ctrnand_keyslot = Keyslot.CTRNANDOld if ncsd_part_crypttype[
                        idx] == 2 else Keyslot.CTRNANDNew
                    self.files['/ctrnand_full.img'] = {
                        'size': part[1],
                        'offset': part[0],
                        'keyslot': ctrnand_keyslot,
                        'type': 'enc'
                    }
                    print('/ctrnand_full.img')
                    nand_fp.seek(part[0])
                    iv = self.ctr + (part[0] >> 4)
                    ctr_mbr = self.crypto.create_ctr_cipher(
                        ctrnand_keyslot,
                        iv).decrypt(nand_fp.read(0x200))[0x1BE:0x200]
                    if ctr_mbr[0x40:0x42] == b'\x55\xaa':
                        ctr_partitions = [[
                            readle(ctr_mbr[i + 8:i + 12]) * 0x200,
                            readle(ctr_mbr[i + 12:i + 16]) * 0x200
                        ] for i in range(0, 0x40, 0x10)]
                        ctr_part_fstype = 0
                        for c_idx, c_part in enumerate(ctr_partitions):
                            if c_part[0] != 0:
                                print(
                                    f'ctr  idx:{c_idx}                      offset:{part[0] + c_part[0]:08x} '
                                    f'size:{c_part[1]:08x} ',
                                    end='')
                                if ctr_part_fstype == 0:
                                    self.files['/ctrnand_fat.img'] = {
                                        'size': c_part[1],
                                        'offset': part[0] + c_part[0],
                                        'keyslot': ctrnand_keyslot,
                                        'type': 'enc'
                                    }
                                    print('/ctrnand_fat.img')
                                    ctr_part_fstype += 1
                                else:
                                    self.files[
                                        f'/ctr_unk{ctr_part_fstype}.img'] = {
                                            'size': c_part[1],
                                            'offset': part[0] + c_part[0],
                                            'keyslot': ctrnand_keyslot,
                                            'type': 'enc'
                                        }
                                    print(f'/ctr_unk{ctr_part_fstype}.img')
                                    ctr_part_fstype += 1

                elif ncsd_part_fstype[idx] == 4:
                    self.files['/agbsave.bin'] = {
                        'size': part[1],
                        'offset': part[0],
                        'keyslot': Keyslot.AGB,
                        'type': 'enc'
                    }
                    print('/agbsave.bin')

            else:
                print('<ctr not set>')

        self.readonly = readonly

        # GM9 bonus drive
        if raw_nand_size != self.real_nand_size:
            nand_fp.seek(self.real_nand_size)
            bonus_drive_header = nand_fp.read(0x200)
            if bonus_drive_header[0x1FE:0x200] == b'\x55\xAA':
                self.files['/bonus.img'] = {
                    'size': raw_nand_size - self.real_nand_size,
                    'offset': self.real_nand_size,
                    'keyslot': 0xFF,
                    'type': 'raw'
                }

        self.f = nand_fp

        if exefs is not None:
            exefs_size = sum(
                roundup(x.size, 0x200) for x in exefs.entries.values()) + 0x200
            self.files['/essential.exefs'] = {
                'size': exefs_size,
                'offset': 0x200,
                'keyslot': 0xFF,
                'type': 'raw'
            }
            try:
                self.exefs_fuse = ExeFSMount(exefs, g_stat=g_stat)
                self.exefs_fuse.init('/')
                self._essentials_mounted = True
            except Exception as e:
                print(
                    f'Failed to mount essential.exefs: {type(e).__name__}: {e}'
                )
Beispiel #23
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
Beispiel #24
0
def load_custom_boot9(path: str, dev: bool = False):
    """Load keys from a custom ARM9 bootROM path."""
    if path:
        from pyctr.crypto import CryptoEngine
        # doing this will set up the keys for all future CryptoEngine objects
        CryptoEngine(boot9=path, dev=dev)
Beispiel #25
0
class NCCHContainerMount(LoggingMixIn, Operations):
    fd = 0
    _exefs_mounted = False
    _romfs_mounted = False
    romfs_fuse = None
    exefs_fuse = None

    def __init__(self, ncch_fp: BinaryIO, g_stat: os.stat_result, decompress_code: bool = True, dev: bool = False,
                 seeddb: str = None):
        self.crypto = CryptoEngine(dev=dev)

        self.decompress_code = decompress_code
        self.seeddb = seeddb
        self.files: Dict[str, Dict] = {}

        # get status change, modify, and file access times
        self._g_stat = g_stat
        self.g_stat = {'st_ctime': int(g_stat.st_ctime), 'st_mtime': int(g_stat.st_mtime),
                       'st_atime': int(g_stat.st_atime)}

        ncch_header = ncch_fp.read(0x200)
        self.reader = NCCHReader.from_header(ncch_header)

        self.f = ncch_fp

        if not self.reader.flags.no_crypto:
            # I should figure out what happens if fixed-key crypto is
            #   used along with seed. even though this will never
            #   happen in practice, I would still like to see what
            #   happens if it happens.
            if self.reader.flags.fixed_crypto_key:
                normal_key = FIXED_SYSTEM_KEY if self.reader.program_id & (0x10 << 32) else 0x0
                self.crypto.set_normal_key(0x2C, normal_key.to_bytes(0x10, 'big'))
            else:
                if self.reader.flags.uses_seed:
                    self.reader.load_seed_from_seeddb()

                self.crypto.set_keyslot('y', 0x2C, readbe(self.reader.get_key_y(original=True)))
                self.crypto.set_keyslot('y', self.reader.extra_keyslot,
                                        readbe(self.reader.get_key_y()))

    def __del__(self, *args):
        try:
            self.f.close()
        except AttributeError:
            pass

    destroy = __del__

    def init(self, path):
        decrypted_filename = '/decrypted.' + ('cxi' if self.reader.flags.executable else 'cfa')

        self.files[decrypted_filename] = {'size': self.reader.content_size, 'offset': 0, 'enctype': 'fulldec'}
        self.files['/ncch.bin'] = {'size': 0x200, 'offset': 0, 'enctype': 'none'}

        if self.reader.check_for_extheader():
            self.files['/extheader.bin'] = {'size': 0x800, 'offset': 0x200, 'enctype': 'normal',
                                            'keyslot': 0x2C, 'iv': (self.reader.partition_id << 64 | (0x01 << 56))}

        plain_region = self.reader.plain_region
        if plain_region.offset:
            self.files['/plain.bin'] = {'size': plain_region.size, 'offset': plain_region.offset, 'enctype': 'none'}

        logo_region = self.reader.logo_region
        if logo_region.offset:
            self.files['/logo.bin'] = {'size': logo_region.size, 'offset': logo_region.offset, 'enctype': 'none'}

        exefs_region = self.reader.exefs_region
        if exefs_region.offset:
            self.files['/exefs.bin'] = {'size': exefs_region.size, 'offset': exefs_region.offset, 'enctype': 'exefs',
                                        'keyslot': 0x2C, 'keyslot_extra': self.reader.extra_keyslot,
                                        'iv': (self.reader.partition_id << 64 | (0x02 << 56)),
                                        'keyslot_normal_range': [(0, 0x200)]}

            # noinspection PyBroadException
            try:
                # get code compression bit
                decompress = False
                if self.decompress_code and self.reader.check_for_extheader():
                    exh_flag = self.read('/extheader.bin', 1, 0xD, 0)
                    decompress = exh_flag[0] & 1
                exefs_vfp = _c.VirtualFileWrapper(self, '/exefs.bin', exefs_region.size)
                exefs_fuse = ExeFSMount(exefs_vfp, self._g_stat, decompress_code=decompress, strict=True)
                exefs_fuse.init(path)
                self.exefs_fuse = exefs_fuse
            except Exception as e:
                print(f'Failed to mount ExeFS: {type(e).__name__}: {e}')
            else:
                if not self.reader.flags.no_crypto:
                    for n, ent in self.exefs_fuse.reader.entries.items():
                        if n in {'icon', 'banner'}:
                            self.files['/exefs.bin']['keyslot_normal_range'].append(
                                (ent.offset + 0x200, ent.offset + 0x200 + roundup(ent.size, 0x200)))

        if not self.reader.flags.no_romfs:
            romfs_region = self.reader.romfs_region
            if romfs_region.offset:
                self.files['/romfs.bin'] = {'size': romfs_region.size, 'offset': romfs_region.offset,
                                            'enctype': 'normal', 'keyslot': self.reader.extra_keyslot,
                                            'iv': (self.reader.partition_id << 64 | (0x03 << 56))}

                # noinspection PyBroadException
                try:
                    romfs_vfp = _c.VirtualFileWrapper(self, '/romfs.bin', romfs_region.size)
                    romfs_fuse = RomFSMount(romfs_vfp, self._g_stat)
                    romfs_fuse.init(path)
                    self.romfs_fuse = romfs_fuse
                except Exception as e:
                    print(f'Failed to mount RomFS: {type(e).__name__}: {e}')

    def flush(self, path, fh):
        return self.f.flush()

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.getattr(_c.remove_first_dir(path), fh)
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.getattr(_c.remove_first_dir(path), fh)
        uid, gid, pid = fuse_get_context()
        if path in {'/', '/romfs', '/exefs'}:
            st = {'st_mode': (S_IFDIR | 0o555), 'st_nlink': 2}
        elif path in self.files:
            st = {'st_mode': (S_IFREG | 0o444), 'st_size': self.files[path]['size'], 'st_nlink': 1}
        else:
            raise FuseOSError(ENOENT)
        return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        if path.startswith('/exefs'):
            yield from self.exefs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path.startswith('/romfs'):
            yield from self.romfs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path == '/':
            yield from ('.', '..')
            yield from (x[1:] for x in self.files)
            if self.exefs_fuse is not None:
                yield 'exefs'
            if self.romfs_fuse is not None:
                yield 'romfs'

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.read(_c.remove_first_dir(path), size, offset, fh)
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.read(_c.remove_first_dir(path), size, offset, fh)
        fi = self.files[path]
        real_offset = fi['offset'] + offset
        if fi['offset'] + offset > fi['offset'] + fi['size']:
            return b''
        if offset + size > fi['size']:
            size = fi['size'] - offset

        if fi['enctype'] == 'none' or self.reader.flags.no_crypto:
            # if no encryption, just read and return
            self.f.seek(real_offset)
            data = self.f.read(size)

        elif fi['enctype'] == 'normal':
            self.f.seek(real_offset)
            data = self.f.read(size)
            # thanks Stary2001
            before = offset % 16
            after = (offset + size) % 16
            data = (b'\0' * before) + data + (b'\0' * after)
            iv = fi['iv'] + (offset >> 4)
            data = self.crypto.create_ctr_cipher(fi['keyslot'], iv).decrypt(data)[before:size + before]

        elif fi['enctype'] == 'exefs':
            # thanks Stary2001
            before = offset % 0x200
            aligned_real_offset = real_offset - before
            aligned_offset = offset - before
            aligned_size = size + before
            self.f.seek(aligned_real_offset)
            data = b''
            # noinspection PyTypeChecker
            for chunk in range(ceil(aligned_size / 0x200)):
                iv = fi['iv'] + ((aligned_offset + (chunk * 0x200)) >> 4)
                keyslot = fi['keyslot_extra']
                for r in fi['keyslot_normal_range']:
                    if r[0] <= self.f.tell() - fi['offset'] < r[1]:
                        keyslot = fi['keyslot']
                data += self.crypto.create_ctr_cipher(keyslot, iv).decrypt(self.f.read(0x200))

            data = data[before:size + before]

        elif fi['enctype'] == 'fulldec':
            # this could be optimized much better
            before = offset % 0x200
            aligned_real_offset = real_offset - before
            aligned_offset = offset - before
            aligned_size = size + before
            self.f.seek(aligned_real_offset)
            data = b''
            files_to_read = OrderedDict()
            # noinspection PyTypeChecker
            for chunk in range(ceil(aligned_size / 0x200)):
                new_offset = (aligned_offset + (chunk * 0x200))
                added = False
                for fname, attrs in self.files.items():
                    if attrs['enctype'] == 'fulldec':
                        continue
                    if attrs['offset'] <= new_offset < attrs['offset'] + attrs['size']:
                        if fname not in files_to_read:
                            files_to_read[fname] = [new_offset - attrs['offset'], 0]
                        files_to_read[fname][1] += 0x200
                        added = True
                if not added:
                    files_to_read[f'raw{chunk}'] = [new_offset, 0x200]

            for fname, info in files_to_read.items():
                try:
                    new_data = self.read(fname, info[1], info[0], 0)
                    if fname == '/ncch.bin':
                        # fix crypto flags
                        ncch_array = bytearray(new_data)
                        ncch_array[0x18B] = 0
                        ncch_array[0x18F] = 4
                        new_data = bytes(ncch_array)
                except KeyError:
                    # for unknown files
                    self.f.seek(info[0])
                    new_data = self.f.read(info[1])

                data += new_data

            data = data[before:size + before]

        else:
            from pprint import pformat
            print('--------------------------------------------------',
                  'Warning: unknown file type (this should not happen!)',
                  'Please file an issue or contact the developer with the details below.',
                  '  https://github.com/ihaveamac/fuse-3ds/issues',
                  '--------------------------------------------------',
                  f'{path!r}: {pformat(fi)!r}', sep='\n')

            data = b'g' * size

        return data

    @_c.ensure_lower_path
    def statfs(self, path):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.statfs(_c.remove_first_dir(path))
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.statfs(_c.remove_first_dir(path))
        else:
            return {'f_bsize': 4096, 'f_blocks': self.reader.content_size // 4096, 'f_bavail': 0, 'f_bfree': 0,
                    'f_files': len(self.files)}