├── fuse_3ds ├── __init__.py ├── seeddb.py ├── romfsfuse.py ├── 3dsxfuse.py ├── ticket.py ├── crypto.py ├── aeskeydb.py ├── exefsfuse.py ├── cia.py ├── tmd.py ├── ncch.py ├── ncchfuse.py ├── ciafuse.py └── romfs.py ├── .gitignore ├── MANIFEST ├── setup.py ├── README └── genaeskeys.py /fuse_3ds/__init__.py: -------------------------------------------------------------------------------- 1 | all=["aeskeydb","cia","crypto","ncch","seeddb","ticket","tmd"] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | aeskeydb.bin 4 | *.cia 5 | seeddb.bin 6 | venv/ 7 | aeskeydb.yaml 8 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | README 3 | ciafuse.py 4 | exefsfuse.py 5 | ncchfuse.py 6 | romfsfuse.py 7 | setup.py 8 | fuse_3ds/__init__.py 9 | fuse_3ds/aeskeydb.py 10 | fuse_3ds/cia.py 11 | fuse_3ds/crypto.py 12 | fuse_3ds/ncch.py 13 | fuse_3ds/seeddb.py 14 | fuse_3ds/ticket.py 15 | fuse_3ds/tmd.py 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | setup(name="3ds-fuse", 4 | version="0.1.1", 5 | description="FUSE filesystems for different 3DS file types", 6 | author="Morten Delenk", 7 | author_email="morten@dark32.cf", 8 | packages=["fuse_3ds"], 9 | scripts=["fuse_3ds/ciafuse.py","fuse_3ds/exefsfuse.py","fuse_3ds/ncchfuse.py","fuse_3ds/romfsfuse.py","fuse_3ds/3dsxfuse.py"], 10 | install_requires=["fusepy","pycryptodome"]) 11 | 12 | -------------------------------------------------------------------------------- /fuse_3ds/seeddb.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from os import path 3 | def binfilegen(f,s): 4 | x=f.read(s) 5 | while len(x) == s: 6 | yield x 7 | x=f.read(s) 8 | def seedwalk(): 9 | f = open(path.join(path.expanduser("~"),"seeddb.bin"),"rb") 10 | f.read(16) 11 | for s in binfilegen(f,32): 12 | tid=struct.unpack(" ".format(name=argv[0])) 17 | exit(1) 18 | logging.basicConfig(level=logging.WARNING) 19 | fuse = FUSE(RomFS(argv[1], argv[2]),argv[2], foreground=False) 20 | -------------------------------------------------------------------------------- /fuse_3ds/3dsxfuse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | from errno import ENOENT, EIO 4 | from stat import S_IFDIR, S_IFLNK, S_IFREG 5 | from sys import argv, exit 6 | from time import time 7 | import subprocess 8 | from fuse import FUSE, FuseOSError, Operations, LoggingMixIn 9 | from fuse_3ds.romfs import * 10 | import struct 11 | class RomFS(RomFSBase): 12 | def __init__(self,fname,mount): 13 | f=open(fname,"rb") 14 | header=f.read(0x20) 15 | if header[:4] != b"3DSX": 16 | raise ValueError("This is not a 3dsx file!") 17 | size=struct.unpack(" ".format(name=argv[0])) 25 | exit(1) 26 | logging.basicConfig(level=logging.WARNING) 27 | fuse = FUSE(RomFS(argv[1], argv[2]),argv[2], foreground=False) 28 | -------------------------------------------------------------------------------- /fuse_3ds/ticket.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from . import aeskeydb 3 | class Ticket: 4 | def __init__(self, f): 5 | sigtype=struct.unpack(">I",f.read(4))[0]-0x10000 6 | skip=[0x23C,0x13C,0x7C,0x23C,0x13C,0x7C] 7 | f.read(skip[sigtype]) 8 | self.issuer=f.read(0x40) 9 | self.pubkey=f.read(0x3C) 10 | self.version,self.caCrlVersion,self.signerCrlVersion=struct.unpack("I",0x10004) 26 | header+=bytes(0x13C) 27 | header+=self.issuer 28 | header+=self.pubkey 29 | header+=struct.pack(" <3DS IP> 15 | mountpoint: 16 | / 17 | /decrypted.cia - Decrypted CIA file 18 | /tmd - TMD 19 | /ticket - Ticket 20 | /0.cxi - Content 0 21 | /1.cfa - Content 1 - Might not exist! 22 | . 23 | . 24 | . 25 | 26 | ./ncchfuse.py <3DS IP> 27 | mountpoint: 28 | / 29 | /decrypted.cxi/cfa - Decrypted NCCH 30 | /exefs.bin - ExeFS - only exists on CXIs 31 | /exheader.bin - Extended Header - optional 32 | /plainrgn.bin - Plain Region - optional 33 | /logorgn.bin - Logo Region - optional 34 | /romfs.bin - RomFS - optional. Should always exist in cfa's 35 | 36 | ./exefsfuse.py 37 | ./romfsfuse.py 38 | 39 | EXAMPLES: 40 | 41 | Mounting the romfs of the CIA of Mario Kart 7: 42 | ./ciafuse.py mk7.cia mount/ 192.168.2.104 43 | ./ncchfuse.py mount/0.cxi mount/ 192.168.2.104 44 | ./romfsfuse.py mount/romfs.bin mount/ 45 | 46 | To unmount this, you have to run "umount" three times 47 | 48 | ----------------------------------------------------- 49 | aeskeydb.bin for local decryption 50 | 51 | Thanks to boot9strap, you can dump boot9+boot11+otp by holding start+select+x during boot. You can dump the aeskeys in those roms by running the script "genaeskeys.py". This will append all new keys to aeskeydb.bin. Then you can store it in your home directory. 52 | 53 | Requirements: 54 | - 3DS running sighax 55 | - boot9.bin in current directory (The entire one) 56 | - otp.bin in current directory 57 | 58 | just run ./genaeskeys.py and your aeskeydb.bin will contain most AES keys. You still have to require eshop 0x3D normal key yourself. 59 | -------------------------------------------------------------------------------- /fuse_3ds/crypto.py: -------------------------------------------------------------------------------- 1 | #Code copied/ported from 3ds-crypto-client 2 | import sys 3 | import struct 4 | import socket 5 | import binascii 6 | import os 7 | 8 | def send_all(sock, data): 9 | r = len(data) 10 | while len(data) > 0: 11 | data = data[sock.send(data):] 12 | 13 | def recv_all(sock, size): 14 | data = b'' 15 | while len(data) < size: 16 | data += sock.recv(size - len(data)) 17 | return data 18 | def cryptoBytestring(ip, data, keyslot,algo, iv=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',keyY=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'): 19 | keyslot+=0x80 20 | meta = struct.pack('>128-v)) 39 | keyX=int.from_bytes(keyX,"big") 40 | keyY=int.from_bytes(keyY,"big") 41 | kn = rotate(asint128((rotate(keyX,2)^keyY)+0x1FF9E9AAC5FE0408024591DC5D52768A),87) 42 | return kn.to_bytes(16,"big") 43 | class NoBugCTR: 44 | def __init__(self, iv): 45 | self.ctr=int.from_bytes(iv,"big") 46 | def __call__(self, *kargs, **kwargs): 47 | iv=self.ctr.to_bytes(16,"big") 48 | self.ctr+=1 49 | if self.ctr == 2**128: 50 | self.ctr=0 51 | return iv 52 | def __iter__(self): 53 | return self 54 | def __next__(self): 55 | return self() 56 | def getCipher(keyslot, iv, CBC=False, keyY=None): 57 | key=getKey(keyslot)[0] 58 | if keyY != None: 59 | keyX=getKey(keyslot)[1] 60 | if keyX == None: 61 | raise ValueError("Unknown KeyX for crypto") 62 | key=scramble(keyX, keyY) 63 | if key == None: 64 | raise ValueError("Unknown Key!") 65 | if CBC: 66 | return AES.new(key, AES.MODE_CBC, iv) 67 | cipher=AES.new(key, AES.MODE_CTR, counter=Counter.new(128,allow_wraparound=True,initial_value=int.from_bytes(iv,"big"))) 68 | return cipher 69 | -------------------------------------------------------------------------------- /fuse_3ds/exefsfuse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | from errno import ENOENT, EIO 4 | from stat import S_IFDIR, S_IFLNK, S_IFREG 5 | from sys import argv, exit 6 | from time import time 7 | import subprocess 8 | from fuse import FUSE, FuseOSError, Operations, LoggingMixIn 9 | import struct 10 | 11 | class ExeFS(LoggingMixIn, Operations): 12 | def __init__(self,fname,mount): 13 | self.f=open(fname,"rb") 14 | self.files={} 15 | self.header=self.f.read(0x200) 16 | self.filelocs={} 17 | self.fd=1 18 | now=time() 19 | self.files["/"]=dict(st_mode=(S_IFDIR |0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2) 20 | for i in range(10): 21 | fname,off,size=struct.unpack("<8sII",self.header[i*0x10:(i+1)*0x10]) 22 | if not size: 23 | continue 24 | x="" 25 | for c in fname[:]: 26 | if c : 27 | x+=chr(c) 28 | fname=x 29 | self.files["/"+fname]=dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=size, st_blocks=(size+511)//512) 30 | self.filelocs["/"+fname]=off+512 31 | def chmod(self, path, mode): 32 | raise FuseOSError(EIO) 33 | def chown(self, path, uid, gid): 34 | raise FuseOSError(EIO) 35 | def create(self, path, mode): 36 | raise FuseOSError(EIO) 37 | def getattr(self,path,fh=None): 38 | if path not in self.files: 39 | raise FuseOSError(ENOENT) 40 | return self.files[path] 41 | def getxattr(self,path,name,position=0): 42 | return '' 43 | def listxattr(self,path): 44 | return [] 45 | def mkdir(self,path,mode): 46 | raise FuseOSError(EIO) 47 | def open(self,path,flags): 48 | self.fd+=1 49 | return self.fd 50 | def read(self,path,size,offset,fh): 51 | self.f.seek(self.filelocs[path]+offset) 52 | return self.f.read(size) 53 | def readdir(self, path, fh): 54 | return ['.','..'] + [x[1:] for x in self.files if x != '/'] 55 | def readlink(self, path): 56 | return self.data[path] 57 | def removexattr(self, path, name): 58 | raise FuseOSError(EIO) 59 | def rename(self, old, new): 60 | raise FuseOSError(EIO) 61 | def rmdir(self, path): 62 | raise FuseOSError(EIO) 63 | def setxattr(self, path, name, value, options, position=0): 64 | raise FuseOSError(EIO) 65 | def statfs(self,path): 66 | return dict(f_bsize=512, f_blocks=4096, f_bavail=0) 67 | def symlink(self, target, source): 68 | raise FuseOSError(EIO) 69 | def truncate(self,path,length,fh=None): 70 | raise FuseOSError(EIO) 71 | def unlink(self,path): 72 | raise FuseOSError(EIO) 73 | def utimens(self,path,times=None): 74 | raise FuseOSError(EIO) 75 | def write(self,path,data,offset,fh): 76 | raise FuseOSError(EIO) 77 | def flush(self,path,fh): 78 | pass 79 | def release(self,path,fh): 80 | pass 81 | 82 | if len(argv) != 3: 83 | print("usage: {name} ".format(name=argv[0])) 84 | exit(1) 85 | logging.basicConfig(level=logging.WARNING) 86 | fuse = FUSE(ExeFS(argv[1], argv[2]),argv[2], foreground=False) 87 | -------------------------------------------------------------------------------- /fuse_3ds/cia.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from . import ticket 3 | from . import tmd 4 | import hashlib 5 | import sys 6 | from Crypto.Cipher import AES 7 | def align(x,y): 8 | mask = ~(y-1) 9 | return (x+(y-1))&mask 10 | class CIA: 11 | def __init__(self, f): 12 | self.f=f 13 | self.headerSize,self.type,self.version,self.cachainSize,self.tikSize,self.tmdSize,self.metaSize,self.contentSize=struct.unpack("I",f.read(4))[0]-0x10000 6 | skip=[0x23C,0x13C,0x7C,0x23C,0x13C,0x7C] 7 | f.read(skip[sigtype]) 8 | loc=f.tell()+0xC4 9 | #Reading the TMD header 10 | self.issuer=f.read(0x40) 11 | self.version,self.caCrlVersion,self.signerCrlVersion,self.sysVersion=struct.unpack(">BBBxQ",f.read(12)) 12 | self.tid=struct.unpack(">Q",f.read(8))[0] 13 | self.type,self.gid,self.savesize,self.privatesavesize,self.SRLFlag=struct.unpack(">IHIIxxxxB",f.read(19)) 14 | f.read(0x31) 15 | self.accessRights,self.tversion,self.contentCount=struct.unpack(">IHH",f.read(8)) 16 | self.contents=[] 17 | self.bootContent=struct.unpack(">I",f.read(4))[0] 18 | sha=f.read(0x20) #SHA256 of content info record 19 | contentInfoRecord=f.read(64*0x24) 20 | self.contentInfoRecord=list(contentInfoRecord) 21 | contentChunkRecord=[f.read(0x30) for x in range(self.contentCount)] 22 | self.contentChunkRecord=list(contentChunkRecord) 23 | checksha=hashlib.sha256(contentInfoRecord).digest() 24 | if sha != checksha: 25 | print("WARNING: TMD content info record hash mismatch!") 26 | for c in range(64): 27 | record=contentInfoRecord[:0x24] 28 | contentInfoRecord=contentInfoRecord[0x24:] 29 | content={} 30 | content["indexOffset"],content["commandCount"]=struct.unpack(">HH",record[:4]) 31 | sha=record[4:] 32 | if c >= self.contentCount: 33 | continue 34 | checkstr=b"" 35 | for s in contentChunkRecord[c:c+content["commandCount"]]: 36 | checkstr+=s 37 | checksha=hashlib.sha256(checkstr).digest() 38 | if content["commandCount"] and checksha != sha: 39 | print("WARNING: TMD content chunk record hash mismatch!") 40 | self.contents.append(content) 41 | self.contentHashes=[] 42 | for no,c in enumerate(contentChunkRecord): 43 | self.contents[no]["cid"],self.contents[no]["index"],self.contents[no]["type"],self.contents[no]["size"]=struct.unpack(">IHHQ",c[:0x10]) 44 | self.contentHashes.append(c[0x10:]) 45 | def decrypt(self): 46 | contents=list(self.contents) 47 | contentChunkRecord=[] 48 | #modify content chunk records and rebuild them 49 | for no,c in enumerate(contents[:]): 50 | contents[no]=dict(c) 51 | for no,c in enumerate(contents): 52 | 53 | if c["type"] & 1: 54 | c["type"]&=~1 #unset encryption bit 55 | record=struct.pack(">IHHQ",c["cid"],c["index"],c["type"],c["size"]) 56 | record+=self.contentHashes[no] 57 | contentChunkRecord.append(record) 58 | contentInfoRecord=b'' 59 | #fix content info record hashes and rebuild them 60 | for no in range(64): 61 | sha=bytes(32) 62 | try: 63 | h=struct.pack(">HH",contents[no]["indexOffset"],contents[no]["commandCount"]) 64 | #Recalculate sha 65 | if contents[no]["commandCount"]: 66 | checkstr=b"" 67 | for s in contentChunkRecord[no:no+contents[no]["commandCount"]]: 68 | checkstr+=s 69 | sha=hashlib.sha256(checkstr).digest() 70 | except: 71 | h=struct.pack(">HH",0,0) 72 | contentInfoRecord+=h+sha 73 | 74 | header=struct.pack(">I",0x010004) 75 | header+=bytes(0x13C) 76 | header+=self.issuer 77 | header+=struct.pack(">BBBxQQIHIIxxxxB",self.version,self.caCrlVersion,self.signerCrlVersion,self.sysVersion,self.tid,self.type,self.gid,self.savesize,self.privatesavesize,self.SRLFlag) 78 | header+=bytes(0x31) 79 | header+=struct.pack(">IHHI",self.accessRights,self.tversion,self.contentCount,self.bootContent) 80 | header+=hashlib.sha256(contentInfoRecord).digest() #Fix this hash 81 | header+=contentInfoRecord 82 | for c in contentChunkRecord: 83 | header+=c 84 | return header 85 | -------------------------------------------------------------------------------- /genaeskeys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import struct 3 | def scramble(keyX,keyY): 4 | def asint128(a): 5 | if a < 0: 6 | raise ValueError("Must be positive") 7 | return a & ((2**128)-1) 8 | rotate=lambda c,v: (asint128(c<>128-v)) 9 | keyX=int.from_bytes(keyX,"big") 10 | keyY=int.from_bytes(keyY,"big") 11 | kn = rotate(asint128((rotate(keyX,2)^keyY)+0x1FF9E9AAC5FE0408024591DC5D52768A),87) 12 | return kn.to_bytes(16,"big") 13 | boot9=open("boot9.bin","rb") 14 | f=boot9 15 | if True: 16 | def getKey(no,dic): 17 | dic[no]=f.read(16) 18 | f.seek(-16,1) 19 | def getKeyloop(no,dic): 20 | for i in range(4): 21 | getKey(no+i,dic) 22 | f.read(16) 23 | def getKeyloop_increase(no,dic): 24 | for i in range(4): 25 | getKey(no+i,dic) 26 | f.read(16) 27 | f.seek(0xd860) #Beginning of keyarea 28 | _3fgendata=f.read(36) 29 | keyX={} 30 | keyY={} 31 | normals={} 32 | f.seek(0xd9d0) 33 | getKeyloop(0x2C,keyX) 34 | getKeyloop(0x30,keyX) 35 | getKeyloop(0x34,keyX) 36 | getKeyloop(0x38,keyX) 37 | getKeyloop_increase(0x3C,keyX) 38 | getKeyloop_increase(0x4,keyY) 39 | getKeyloop_increase(0x8,keyY) 40 | getKeyloop(0xC,normals) 41 | getKeyloop(0x10,normals) 42 | getKeyloop_increase(0x14,normals) 43 | getKeyloop(0x18,normals) 44 | getKeyloop(0x1C,normals) 45 | getKeyloop(0x20,normals) 46 | getKeyloop(0x24,normals) 47 | f.seek(-16,1) 48 | getKeyloop_increase(0x28,normals) 49 | getKeyloop(0x2C,normals) 50 | getKeyloop(0x30,normals) 51 | getKeyloop(0x34,normals) 52 | getKeyloop(0x38,normals) 53 | f.seek(-16,1) 54 | getKeyloop_increase(0x3C,normals) 55 | f.seek(0xD6E0) 56 | otpkey=f.read(16) 57 | otpiv=f.read(16) 58 | f.seek(0xD860) 59 | from Crypto.Cipher import AES 60 | with open("otp.bin","rb") as f: 61 | cipher=AES.new(otpkey, AES.MODE_CBC, otpiv) 62 | otp=cipher.decrypt(f.read()) 63 | import hashlib 64 | conunique=otp[:28]+_3fgendata 65 | conunique_hash=hashlib.sha256(conunique).digest() 66 | del normals[0x3F] 67 | keyX[0x3F]=conunique_hash[:16] 68 | keyY[0x3F]=conunique_hash[16:] 69 | def genkeys(size=0x40): 70 | boot9.read(36) 71 | aesiv=boot9.read(16) 72 | conunique_input=boot9.read(64) 73 | boot9.seek(-64,1) 74 | boot9.read(size) 75 | cipher=AES.new(scramble(keyX[0x3F],keyY[0x3F]),AES.MODE_CBC,aesiv) 76 | return cipher.encrypt(conunique_input) 77 | def getKey(dic,no,conunique,off): 78 | dic[no]=conunique[off:off+16] 79 | def getKeyloop(dic,no,conunique,off): 80 | for i in range(4): 81 | getKey(dic,no+i,conunique,off) 82 | def getKeyloop_increase(dic,no,conunique,off): 83 | for i in range(4): 84 | getKey(dic,no+i,conunique,off+16*i) 85 | conunique=genkeys() 86 | getKeyloop(keyX,4,conunique,0) 87 | getKeyloop(keyX,8,conunique,16) 88 | getKeyloop(keyX,0xC,conunique,32) 89 | getKey(keyX,0x10,conunique,48) 90 | 91 | conunique=genkeys(16) 92 | getKeyloop_increase(keyX,0x14,conunique,0) 93 | 94 | conunique=genkeys() 95 | 96 | getKeyloop(keyX,0x18,conunique,0) 97 | getKeyloop(keyX,0x1C,conunique,16) 98 | getKeyloop(keyX,0x20,conunique,32) 99 | getKey(keyX,0x24,conunique,48) 100 | 101 | conunique=genkeys(16) 102 | getKeyloop_increase(keyX,0x28,conunique,0) 103 | 104 | #Generate normal keys 105 | for kx in keyX.keys(): 106 | if kx in keyY: 107 | normals[kx]=scramble(keyX[kx],keyY[kx]) 108 | n=normals 109 | normals={} 110 | for kn in n.keys(): 111 | if ((kn in keyX) and (kn in keyY)) or ((kn not in keyX) and (kn not in keyY)): 112 | normals[kn]=n[kn] #only keep normal keys that are in keyX,keyY pairs, or are just normal keys. 113 | with open("aeskeydb.bin","ab") as f: 114 | for kx in keyX.keys(): 115 | f.write(struct.pack("= 1 and sectorno < 5: 60 | ctr=self.getCtr() 61 | if self.version == 1: 62 | ctr=self.addCtr(ctr,sectorno*32+512-32) 63 | else: 64 | ctr = ctr[:8] + b'\x01' + ctr[9:] 65 | ctr=self.addCtr(ctr,sectorno*32-32) 66 | 67 | #Sector is encrypted using keyslot 0x2C 68 | csectors=min(sectors,4) 69 | sectors-=csectors 70 | data = f.read(csectors*512) 71 | if self.flags[7]&0x04: #Except when it's decrypted 72 | decdata += data 73 | else: 74 | decdata += aeskeydb.getCipher(0x2C,ctr, keyY=self.keyY).decrypt(data) 75 | if not sectors: 76 | return decdata 77 | sectorno+=csectors 78 | ctr=self.getCtr() 79 | print(sectorno) 80 | if sectorno < self.exefsOff + self.exefsSize: 81 | print("EXE") 82 | #exefs 83 | if self.version == 1: 84 | ctr = self.addCtr(ctr,(sectorno-self.exefsOff)*32+self.exefsOff*512) 85 | else: 86 | ctr = ctr[:8] + b'\x02' + ctr[9:] 87 | ctr=self.addCtr(ctr,(sectorno-self.exefsOff)*32) 88 | overhang=sectors-self.exefsSize 89 | if overhang>0: 90 | return decdata + self.read(sectorno,self.exefsSize) + self.read(self.exefsOff+self.exefsSize,overhang) 91 | else: 92 | print("ROM") 93 | #romfs 94 | if self.version == 1: 95 | ctr = self.addCtr(ctr,(sectorno-self.romfsOff)*32+self.romfsOff*512) 96 | else: 97 | ctr = ctr[:8] + b'\x03' + ctr[9:] 98 | ctr=self.addCtr(ctr,(sectorno-self.romfsOff)*32) 99 | 100 | print(ctr) 101 | #Sectors are encrypted via multiple methods 102 | data=f.read(sectors*512) 103 | keyslot = 0x2C 104 | if self.flags[3] == 0x01: 105 | keyslot = 0x25 106 | elif self.flags[3] == 0x0A: 107 | keyslot = 0x18 108 | elif self.flags[3] == 0x0B: 109 | keyslot = 0x1B 110 | print(keyslot) 111 | decdata += aeskeydb.getCipher(keyslot,ctr,keyY=self.keyY).decrypt(data) 112 | return decdata 113 | 114 | def doExHeader(self): 115 | self.exheader=self.read(1,(2048//512)) 116 | if not self.exheadersize: 117 | return 118 | #Hash checking... 119 | if self.exheaderhash != hashlib.sha256(self.exheader[:self.exheadersize]).digest(): 120 | print("WARNING: ExHeader hash mismatch!") 121 | print(self.exheaderhash,hashlib.sha256(self.exheader[:self.exheadersize]).digest()) 122 | -------------------------------------------------------------------------------- /fuse_3ds/ncchfuse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from fuse_3ds import ncch 3 | 4 | import logging 5 | from errno import ENOENT, EIO 6 | from stat import S_IFDIR, S_IFLNK, S_IFREG 7 | from sys import argv, exit 8 | from time import time 9 | import subprocess 10 | from fuse import FUSE, FuseOSError, Operations, LoggingMixIn 11 | 12 | class NCCH(LoggingMixIn, Operations): 13 | 'Read only filesystem for NCCH files.' 14 | def __init__(self,fname,mount): 15 | self.files={} 16 | self.f = open(fname,"rb") 17 | self.ncch = ncch.NCCH(self.f) 18 | self.type="cxi" if self.ncch.exefsSize else "cfa" 19 | self.fd = 0 20 | now = time() 21 | self.files["/"] = dict(st_mode=(S_IFDIR | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2) 22 | self.files["/dec."+self.type] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=512*(self.ncch.romfsOff+self.ncch.romfsSize), st_blocks=self.ncch.romfsOff+self.ncch.romfsSize) 23 | if self.type == "cxi": 24 | self.files["/exefs.bin"] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.ncch.exefsSize*512, st_blocks=self.ncch.exefsSize) 25 | if self.ncch.exheadersize: 26 | self.files["/exheader.bin"] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.ncch.exheadersize, st_blocks=(self.ncch.exheadersize+511)//512) 27 | if self.ncch.plainregionSize: 28 | self.files["/plainrgn.bin"] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.ncch.plainregionSize*512, st_blocks=self.ncch.plainregionSize) 29 | if self.ncch.logoregionSize: 30 | self.files["/logorgn.bin"] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.ncch.logoregionSize*512, st_blocks=self.ncch.logoregionSize) 31 | if self.ncch.romfsSize: 32 | self.files["/romfs.bin"] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.ncch.romfsSize*512, st_blocks=self.ncch.romfsSize) 33 | 34 | 35 | def chmod(self,path,mode): 36 | raise FuseOSError(EIO) #IT'S RO 37 | 38 | def chown(self, path, uid, gid): 39 | raise FuseOSError(EIO) 40 | 41 | def create(self,path,mode): 42 | raise FuseOSError(EIO) 43 | 44 | def getattr(self,path,fh=None): 45 | if path not in self.files: 46 | raise FuseOSError(ENOENT) 47 | 48 | return self.files[path] 49 | 50 | def getxattr(self,path,name,position=0): 51 | return '' 52 | 53 | def listxattr(self, path): 54 | return [] 55 | 56 | def mkdir(self,path,mode): 57 | raise FuseOSError(EIO) 58 | 59 | def open(self,path,flags): 60 | self.fd += 1 61 | return self.fd 62 | def readDecNCCH(self,path,size,offset): 63 | header=self.ncch.header 64 | header=header[:0x188+7]+b'\x04'+header[0x189+7:] 65 | if offset == 0: 66 | endSize = size-512 67 | if endSize <= 0: 68 | return header[:size] 69 | return header+self.readDecNCCH(path, offset+512,endSize) 70 | data=self.ncch.read((offset//512),(size//512)+2) 71 | return data[:size] 72 | def read(self,path,size,offset,fh): 73 | if path[:5] == "/dec.": 74 | return self.readDecNCCH(path,size,offset) 75 | if path == "/exefs.bin": 76 | return self.readDecNCCH(path,size,offset+self.ncch.exefsOff*512) 77 | if path == "/exheader.bin": 78 | return self.readDecNCCH(path,size,offset+512) 79 | if path == "/plainrgn.bin": 80 | return self.readDecNCCH(path,size,offset+self.ncch.plainregionOff*512) 81 | if path == "/logorgn.bin": 82 | return self.readDecNCCH(path,size,offset+self.ncch.logoregionOff*512) 83 | if path == "/romfs.bin": 84 | return self.readDecNCCH(path,size,offset+self.ncch.romfsOff*512) 85 | 86 | raise FuseOSError(EIO) 87 | 88 | 89 | def readdir(self, path, fh): 90 | return ['.', '..'] + [x[1:] for x in self.files if x != '/'] 91 | 92 | def readlink(self, path): 93 | return self.data[path] 94 | 95 | def removexattr(self, path, name): 96 | raise FuseOSError(EIO) 97 | 98 | def rename(self, old, new): 99 | raise FuseOSError(EIO) 100 | 101 | def rmdir(self, path): 102 | raise FuseOSError(EIO) 103 | 104 | def setxattr(self,path,name,value,options,position=0): 105 | raise FuseOSError(EIO) 106 | 107 | def statfs(self,path): 108 | return dict(f_bsize=512, f_blocks=4096, f_bavail=0) 109 | 110 | def symlink(self,target,source): 111 | raise FuseOSError(EIO) 112 | 113 | def truncate(self,path,length,fh=None): 114 | raise FuseOSError(EIO) 115 | 116 | def unlink(self,path): 117 | raise FuseOSError(EIO) 118 | 119 | def utimens(self,path,times=None): 120 | raise FuseOSError(EIO) 121 | 122 | def write(self,path,data,offset,fh): 123 | raise FuseOSError(EIO) 124 | 125 | def flush(self,path,fh): 126 | pass 127 | def release(self,path,fh): 128 | pass 129 | 130 | if len(argv) != 3: 131 | print('usage: {name} '.format(name=argv[0])) 132 | exit(1) 133 | logging.basicConfig(level=logging.DEBUG) 134 | fuse = FUSE(NCCH(argv[1], argv[2]),argv[2],foreground=True) 135 | -------------------------------------------------------------------------------- /fuse_3ds/ciafuse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from fuse_3ds import cia 3 | 4 | import logging 5 | from errno import ENOENT, EIO 6 | from stat import S_IFDIR, S_IFLNK, S_IFREG 7 | from sys import argv, exit 8 | from time import time 9 | import subprocess 10 | from fuse import FUSE, FuseOSError, Operations, LoggingMixIn 11 | 12 | class CIA(LoggingMixIn, Operations): 13 | 'Read only filesystem for CIA files.' 14 | def __init__(self,fname,mount): 15 | self.files={} 16 | self.f = open(fname,"rb") 17 | self.cia = cia.CIA(self.f) 18 | self.fd = 0 19 | now = time() 20 | self.files["/"] = dict(st_mode=(S_IFDIR | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2) 21 | self.files["/dec.cia"] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.cia.size, st_blocks=(self.cia.size+511)//512) 22 | self.files["/ticket"] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.cia.tikSize, st_blocks=(self.cia.tikSize+511)//512) 23 | self.files["/tmd"] = dict(st_mode=(S_IFREG | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.cia.tmdSize, st_blocks=(self.cia.tmdSize+511)//512) 24 | for no in range(self.cia.tmd.contentCount): 25 | ending=".cfa" 26 | if no == 0: 27 | ending=".cxi" 28 | self.files["/"+str(no)+ending]=dict(st_mode=(S_IFREG | 0o555),st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.cia.tmd.contents[no]["size"], st_blocks=(self.cia.tmd.contents[no]["size"]+511)//512) 29 | 30 | 31 | def chmod(self,path,mode): 32 | raise FuseOSError(EIO) #IT'S RO 33 | 34 | def chown(self, path, uid, gid): 35 | raise FuseOSError(EIO) 36 | 37 | def create(self,path,mode): 38 | raise FuseOSError(EIO) 39 | 40 | def getattr(self,path,fh=None): 41 | if path not in self.files: 42 | raise FuseOSError(ENOENT) 43 | 44 | return self.files[path] 45 | 46 | def getxattr(self,path,name,position=0): 47 | return '' 48 | 49 | def listxattr(self, path): 50 | return [] 51 | 52 | def mkdir(self,path,mode): 53 | raise FuseOSError(EIO) 54 | 55 | def open(self,path,flags): 56 | self.fd += 1 57 | return self.fd 58 | def readDecCIA(self,path,size,offset): 59 | #Ok, read the decrypted CIA 60 | #If offset is before the actual contents, read that instead 61 | data=b'' 62 | s=False 63 | if offset < self.cia.contentOff: 64 | s=True 65 | data=self.cia.getDecHeader() 66 | data=data[offset:offset+size] 67 | if len(data) == size: 68 | return data 69 | offset-=self.cia.contentOff 70 | if offset < 0: 71 | offset = 0 72 | #If not, or if not enough data is read, start in the correct sector, and read more than asked 73 | data+=self.cia.read(offset//512,((size-len(data))//512)+2) 74 | #Remove data at the beginning 75 | if s: 76 | data=data[offset%512:] 77 | #remove trailing data 78 | data=data[:size] 79 | return data 80 | def readContent(self,path,size,offset,content): 81 | start=self.cia.startSec(content) 82 | data=self.cia.read((offset//512)+start,(size//512)+2) 83 | data=data[offset%512:] 84 | data=data[:size] 85 | return data 86 | def read(self,path,size,offset,fh): 87 | if path == "/dec.cia": 88 | return self.readDecCIA(path,size,offset) 89 | if path == "/ticket": 90 | data=self.cia.ticket.decrypt() 91 | return data[offset:offset+size] 92 | if path == "/tmd": 93 | data=self.cia.tmd.decrypt() 94 | return data[offset:offset+size] 95 | for no in range(self.cia.tmd.contentCount): 96 | ending=".cfa" 97 | if no == 0: 98 | ending=".cxi" 99 | if path == "/"+str(no)+ending: 100 | #Found it! 101 | return self.readContent(path,size,offset,no) 102 | 103 | raise FuseOSError(EIO) 104 | 105 | 106 | def readdir(self, path, fh): 107 | return ['.', '..'] + [x[1:] for x in self.files if x != '/'] 108 | 109 | def readlink(self, path): 110 | return self.data[path] 111 | 112 | def removexattr(self, path, name): 113 | raise FuseOSError(EIO) 114 | 115 | def rename(self, old, new): 116 | raise FuseOSError(EIO) 117 | 118 | def rmdir(self, path): 119 | raise FuseOSError(EIO) 120 | 121 | def setxattr(self,path,name,value,options,position=0): 122 | raise FuseOSError(EIO) 123 | 124 | def statfs(self,path): 125 | return dict(f_bsize=512, f_blocks=4096, f_bavail=0) 126 | 127 | def symlink(self,target,source): 128 | raise FuseOSError(EIO) 129 | 130 | def truncate(self,path,length,fh=None): 131 | raise FuseOSError(EIO) 132 | 133 | def unlink(self,path): 134 | raise FuseOSError(EIO) 135 | 136 | def utimens(self,path,times=None): 137 | raise FuseOSError(EIO) 138 | 139 | def write(self,path,data,offset,fh): 140 | raise FuseOSError(EIO) 141 | 142 | def flush(self,path,fh): 143 | pass 144 | def release(self,path,fh): 145 | pass 146 | 147 | if len(argv) != 3: 148 | print('usage: {name} '.format(name=argv[0])) 149 | exit(1) 150 | logging.basicConfig(level=logging.WARNING) 151 | fuse = FUSE(CIA(argv[1], argv[2]),argv[2],foreground=False) 152 | -------------------------------------------------------------------------------- /fuse_3ds/romfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import struct 3 | from errno import ENOENT, EIO 4 | from stat import S_IFDIR, S_IFLNK, S_IFREG 5 | from time import time 6 | from fuse import Operations, LoggingMixIn 7 | class romfsFile: 8 | def __repr__(self): 9 | if self.isdir: 10 | s = "Directory '{}' with subdirs [".format(self.name) 11 | for subdir in self.children: 12 | s+=subdir.name+", " 13 | s+="] and files [" 14 | for fil in self.files: 15 | s+=fil.name+", " 16 | s+="]." 17 | return s 18 | return "File '{}' of size {} at {}".format(self.name,hex(self.length),hex(self.off)) 19 | class RomFSBase(LoggingMixIn, Operations): 20 | def traverse(self): 21 | parent,sibling,child,ffile,namelen=struct.unpack("