dir_index = format((c.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) print(f'Writing {enc_path}...') with cia.open_raw_section(c.cindex) as s, open(out_path, 'wb') as o: copy_with_progress(s, o, c.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) print(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) print(f'Generating {enc_path}') highest_index = 0 content_ids = {} for record in cia.content_info:
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
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
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) }
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
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)
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)
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)}