From c515afa2836fd6dc7ab0bd61a2f3eaa71300f1e6 Mon Sep 17 00:00:00 2001 From: seiya-git Date: Sat, 7 Oct 2023 13:14:21 +0300 Subject: [PATCH] new test base --- py-test/Fs/BaseFs.py | 175 +++++++ py-test/Fs/Bktr.py | 266 +++++++++++ py-test/Fs/Cnmt.py | 78 ++++ py-test/Fs/File.py | 495 ++++++++++++++++++++ py-test/Fs/Hfs0.py | 166 +++++++ py-test/Fs/Ivfc.py | 46 ++ py-test/Fs/Nacp.py | 609 +++++++++++++++++++++++++ py-test/Fs/Nca.py | 295 ++++++++++++ py-test/Fs/Nsp.py | 447 ++++++++++++++++++ py-test/Fs/Pfs0.py | 276 +++++++++++ py-test/Fs/Rom.py | 53 +++ py-test/Fs/Ticket.py | 224 +++++++++ py-test/Fs/Type.py | 30 ++ py-test/Fs/Xci.py | 322 +++++++++++++ py-test/Fs/__init__.py | 37 ++ py-test/lib/BlockDecompressorReader.py | 65 +++ py-test/lib/Header.py | 27 ++ py-test/lib/PathTools.py | 50 ++ py-test/nut/Hex.py | 40 ++ py-test/nut/Keys.py | 176 +++++++ py-test/nut/Print.py | 34 ++ py-test/nut/Titles.py | 78 ++++ py-test/nut/aes128.py | 428 +++++++++++++++++ py-test/verif.py | 82 ++++ py-test/verif_folder.py | 105 +++++ py/lib/NXKeys.py | 4 +- py/requirements.txt | 1 + 27 files changed, 4607 insertions(+), 2 deletions(-) create mode 100644 py-test/Fs/BaseFs.py create mode 100644 py-test/Fs/Bktr.py create mode 100644 py-test/Fs/Cnmt.py create mode 100644 py-test/Fs/File.py create mode 100644 py-test/Fs/Hfs0.py create mode 100644 py-test/Fs/Ivfc.py create mode 100644 py-test/Fs/Nacp.py create mode 100644 py-test/Fs/Nca.py create mode 100644 py-test/Fs/Nsp.py create mode 100644 py-test/Fs/Pfs0.py create mode 100644 py-test/Fs/Rom.py create mode 100644 py-test/Fs/Ticket.py create mode 100644 py-test/Fs/Type.py create mode 100644 py-test/Fs/Xci.py create mode 100644 py-test/Fs/__init__.py create mode 100644 py-test/lib/BlockDecompressorReader.py create mode 100644 py-test/lib/Header.py create mode 100644 py-test/lib/PathTools.py create mode 100644 py-test/nut/Hex.py create mode 100644 py-test/nut/Keys.py create mode 100644 py-test/nut/Print.py create mode 100644 py-test/nut/Titles.py create mode 100644 py-test/nut/aes128.py create mode 100644 py-test/verif.py create mode 100644 py-test/verif_folder.py diff --git a/py-test/Fs/BaseFs.py b/py-test/Fs/BaseFs.py new file mode 100644 index 00000000..19d91fbe --- /dev/null +++ b/py-test/Fs/BaseFs.py @@ -0,0 +1,175 @@ +# from nsz import Fs +from Fs.File import File +from Fs import Type +from nut import Print +from Fs.File import MemoryFile +from Fs.Cnmt import Cnmt +from Fs import Bktr +from binascii import hexlify as hx, unhexlify as uhx + +class EncryptedSection: + def __init__(self, offset, size, cryotoType, cryptoKey, cryptoCounter): + self.offset = offset + self.size = size + self.cryptoType = cryotoType + self.cryptoKey = cryptoKey + self.cryptoCounter = cryptoCounter + +class BaseFs(File): + def __init__(self, buffer, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + self.buffer = buffer + self.sectionStart = 0 + self.fsType = None + self.cryptoType = None + self.size = 0 + self.cryptoCounter = None + self.magic = None + self._headerSize = None + self.bktrRelocation = None + self.bktrSubsection = None + + self.files = [] + + if buffer: + self.buffer = buffer + try: + self.fsType = Type.Fs(buffer[0x3]) + except: + self.fsType = buffer[0x3] + + try: + self.cryptoType = Type.Crypto(buffer[0x4]) + except: + self.cryptoType = buffer[0x4] + + self.cryptoCounter = bytearray((b"\x00"*8) + buffer[0x140:0x148]) + self.cryptoCounter = self.cryptoCounter[::-1] + + cryptoType = self.cryptoType + cryptoCounter = self.cryptoCounter + + self.bktr1Buffer = buffer[0x100:0x120] + self.bktr2Buffer = buffer[0x120:0x140] + else: + self.bktr1Buffer = None + self.bktr2Buffer = None + + super(BaseFs, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + def __getitem__(self, key): + if isinstance(key, str): + for f in self.files: + if (hasattr(f, 'name') and f.name == key) or (hasattr(f, '_path') and f._path == key): + return f + elif isinstance(key, int): + return self.files[key] + + raise IOError('FS File Not Found') + + def getEncryptionSections(self): + sections = [] + + if self.hasBktr(): + sectionOffset = self.realOffset() + + for entry in self.bktrSubsection.getAllEntries(): + ctr = self.setBktrCounter(entry.ctr, 0) + sections.append(EncryptedSection(self.realOffset() + entry.virtualOffset, entry.size, self.cryptoType, self.cryptoKey, ctr)) + + if len(sections) == 0: + sections.append(EncryptedSection(sectionOffset, self.size, self.cryptoType, self.cryptoKey, self.cryptoCounter)) + else: + offset = sections[-1].offset + sections[-1].size + sections.append(EncryptedSection(offset, (sectionOffset + self.size) - offset, self.cryptoType, self.cryptoKey, self.cryptoCounter)) + + else: + sections.append(EncryptedSection(self.realOffset(), self.size, self.cryptoType, self.cryptoKey, self.cryptoCounter)) + return sections + + def realOffset(self): + return self.offset - self.sectionStart + + def hasBktr(self): + return (False if self.bktrSubsection is None else True) and self.bktrSubsection.isValid() + + + def open(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + r = super(BaseFs, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter) + + if self.bktr1Buffer: + try: + self.bktrRelocation = Bktr.Bktr1(MemoryFile(self.bktr1Buffer), 'rb', nca = self) + except BaseException as e: + Print.info('bktr reloc exception: ' + str(e)) + + if self.bktr2Buffer: + try: + self.bktrSubsection = Bktr.Bktr2(MemoryFile(self.bktr2Buffer), 'rb', nca = self) + except BaseException as e: + Print.info('bktr subsection exception: ' + str(e)) + + def bktrRead(self, size = None, direct = False): + self.cryptoOffset = 0 + self.ctr_val = 0 + ''' + if self.bktrRelocation: + entry = self.bktrRelocation.getRelocationEntry(self.tell()) + + if entry: + self.ctr_val = entry.ctr + #self.cryptoOffset = entry.virtualOffset + entry.physicalOffset + ''' + if self.bktrSubsection is not None: + entries = self.bktrSubsection.getEntries(self.tell(), size) + #print('offset = %x' % self.tell()) + for entry in entries: + #print('offset = %x' % self.tell()) + entry.printInfo() + #sys.exit(0) + #else: + # print('unknown offset = %x' % self.tell()) + + return super(BaseFs, self).read(size, direct) + + def read(self, size = None, direct = False): + ''' + if self.cryptoType == Type.Crypto.BKTR or self.bktrSubsection is not None: + return self.bktrRead(size, True) + else: + return super(BaseFs, self).read(size, direct) + ''' + return super(BaseFs, self).read(size, direct) + + def getCnmt(self): + for f in self: + if isinstance(f, Cnmt): + return f + raise("No Cnmt found!") + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info(tabs + 'magic = ' + str(self.magic)) + Print.info(tabs + 'fsType = ' + str(self.fsType)) + Print.info(tabs + 'cryptoType = ' + str(self.cryptoType)) + Print.info(tabs + 'size = ' + str(self.size)) + Print.info(tabs + 'headerSize = %s' % (str(self._headerSize))) + Print.info(tabs + 'offset = %s - (%s)' % (str(self.offset), str(self.sectionStart))) + if self.cryptoCounter: + Print.info(tabs + 'cryptoCounter = ' + str(hx(self.cryptoCounter))) + + if self.cryptoKey: + Print.info(tabs + 'cryptoKey = ' + str(hx(self.cryptoKey))) + + Print.info('\n%s\t%s\n' % (tabs, '*' * 64)) + Print.info('\n%s\tFiles:\n' % (tabs)) + + if(indent+1 < maxDepth): + for f in self: + f.printInfo(maxDepth, indent+1) + Print.info('\n%s\t%s\n' % (tabs, '*' * 64)) + + if self.bktrRelocation: + self.bktrRelocation.printInfo(maxDepth, indent+1) + + if self.bktrSubsection: + self.bktrSubsection.printInfo(maxDepth, indent+1) \ No newline at end of file diff --git a/py-test/Fs/Bktr.py b/py-test/Fs/Bktr.py new file mode 100644 index 00000000..6f1d218a --- /dev/null +++ b/py-test/Fs/Bktr.py @@ -0,0 +1,266 @@ +from nut import aes128 +from nut import Hex +from binascii import hexlify as hx, unhexlify as uhx +from struct import pack as pk, unpack as upk +from Fs.File import File, MemoryFile +from hashlib import sha256 +import os +import re +import pathlib +from nut import Keys, Print + +MEDIA_SIZE = 0x200 + + + +class Header(File): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, nca = None): + self.size = 0 + self.offset = 0 + self.nca = nca + self.bktr_offset = 0 + self.bktr_size = 0 + self.magic = None + self.version = None + self.enctryCount = 0 + self.reserved = None + self.buffer = None + super(Header, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Header, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + + self.bktr_offset = self.readInt64() + self.bktr_size = self.readInt64() + self.magic = self.read(0x4) + self.version = self.readInt32() + self.enctryCount = self.readInt32() + self.reserved = self.readInt32() + + def printInfo(self, maxDepth = 3, indent = 0): + if not self.bktr_size: + return + + tabs = '\t' * indent + Print.info('\n%sBKTR' % (tabs)) + Print.info('%soffset = %d' % (tabs, self.bktr_offset)) + Print.info('%ssize = %d' % (tabs, self.bktr_size)) + Print.info('%sentry count = %d' % (tabs, self.enctryCount)) + + Print.info('\n') + +class BktrRelocationEntry: + def __init__(self, f): + self.virtualOffset = f.readInt64() + self.physicalOffset = f.readInt64() + self.isPatch = f.readInt32() + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('%sRelocation Entry %s %x = %x' % (tabs, 'Patch' if self.isPatch else 'Base', self.physicalOffset, self.virtualOffset)) + +class BktrSubsectionEntry: + def __init__(self, f): + self.virtualOffset = f.readInt64() + self.size = 0 + self.padding = f.readInt32() + self.ctr = f.readInt32() + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('%sSubsection Entry %d, CTR = %x' % (tabs, self.virtualOffset, self.ctr)) + +class BktrBucket: + def __init__(self, f): + self.padding = f.readInt32() + self.entryCount = f.readInt32() + self.endOffset = f.readInt64() + self.entries = [] + + def getEntry(self, offset): + index = 0 + last = self.entries[index] + for entry in self.entries: + if entry.virtualOffset > offset: + break + + last = self.entries[index] + index += 1 + + return last + + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('\n%sBKTR Bucket' % tabs) + Print.info('%sentries: %d' % (tabs, self.entryCount)) + Print.info('%send offset: %d' % (tabs, self.endOffset)) + + for entry in self.entries: + entry.printInfo(maxDepth, indent + 1) + +class BktrSubsectionBucket(BktrBucket): + def __init__(self, f): + super(BktrSubsectionBucket, self).__init__(f) + + for i in range(self.entryCount): + self.entries.append(BktrSubsectionEntry(f)) + + +class BktrRelocationBucket(BktrBucket): + def __init__(self, f): + super(BktrRelocationBucket, self).__init__(f) + + if self.entryCount > 0xFFFF: + raise IOError('Too many entries') + + for i in range(self.entryCount): + self.entries.append(BktrRelocationEntry(f)) + + +class Bktr(Header): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, nca = None): + self.basePhysicalOffsets = [] + super(Bktr, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter, nca) + + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Bktr, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + + if self.bktr_size: + self.nca.seek(self.bktr_offset) + self.nca.readInt32() # padding + self.bucketCount = self.nca.readInt32() + self.totalPatchImageSize = self.nca.readInt64() + self.basePhysicalOffsets = [] + for i in range(int(0x3FF0 / 8)): + self.basePhysicalOffsets.append(self.nca.readInt64()) + + def isValid(self): + return True if self.bktr_size > 0 else False + + def getBucket(self, offset): + if len(self.buckets) == 0: + return None + + index = 0 + last = self.buckets[0] + + for virtualOffset in self.basePhysicalOffsets: + if index >= len(self.buckets): + break + + if offset > virtualOffset: + break + + last = self.buckets[index] + index += 1 + + return last + + def printInfo(self, maxDepth = 3, indent = 0): + super(Bktr, self).printInfo(maxDepth, indent) + tabs = '\t' * indent + Print.info('%sOffsets' % (tabs)) + + i = 0 + for off in self.basePhysicalOffsets: + i += 1 + if off == 0 and i != 1: + break + Print.info('%s %x' % (tabs, off)) + + + +class Bktr1(Bktr): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, nca = None): + self.buckets = [] + super(Bktr1, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter, nca) + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Bktr1, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + + self.buckets = [] + + #if self.bktr_size: + # for i in range(self.bucketCount): + # self.buckets.append(BktrRelocationBucket(self.nca)) + + def getRelocationEntry(self, offset): + if len(self.buckets) == 0: + return None + + bucket = self.buckets[0] + + index = 0 + for virtualOffset in self.basePhysicalOffsets: + + if virtualOffset > offset or index >= len(self.buckets): + break + + bucket = self.buckets[index] + index += 1 + + result = bucket.entries[0] + for entry in bucket.entries: + if offset > entry.virtualOffset: + break + result = entry + + return entry + + + def printInfo(self, maxDepth = 3, indent = 0): + super(Bktr1, self).printInfo(maxDepth, indent) + tabs = '\t' * indent + + for bucket in self.buckets: + bucket.printInfo(maxDepth, indent+1) + +class Bktr2(Bktr): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, nca = None): + self.buckets = [] + super(Bktr2, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter, nca) + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Bktr2, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + + self.buckets = [] + + if self.bktr_size: + for i in range(self.bucketCount): + self.buckets.append(BktrSubsectionBucket(self.nca)) + + def getEntries(self, offset, size): + entries = [] + + bucket = self.getBucket(offset) + if bucket is not None: + entries.append(bucket.getEntry(offset)) + + return entries + + def getAllEntries(self): + entries = [] + + for bucket in self.buckets: + last = None + for entry in bucket.entries: + if last is not None: + last.size = entry.virtualOffset - last.virtualOffset + last = entry + entries.append(entry) + + if len(entries) != 0: + entries[-1].size = bucket.endOffset - entries[-1].virtualOffset + + return entries + + def printInfo(self, maxDepth = 3, indent = 0): + super(Bktr2, self).printInfo(maxDepth, indent) + + for bucket in self.buckets: + bucket.printInfo(maxDepth, indent+1) + + \ No newline at end of file diff --git a/py-test/Fs/Cnmt.py b/py-test/Fs/Cnmt.py new file mode 100644 index 00000000..fe7fab9b --- /dev/null +++ b/py-test/Fs/Cnmt.py @@ -0,0 +1,78 @@ +from Fs.File import File +from binascii import hexlify as hx, unhexlify as uhx +from nut import Print, Keys + + +class MetaEntry: + def __init__(self, f): + self.titleId = hx(f.read(8)[::-1]).decode() + self.version = f.readInt32() + self.type = f.readInt8() + self.install = f.readInt8() + + f.readInt16() # junk + +class ContentEntry: + def __init__(self, f): + self.hash = f.read(32) + self.ncaId = hx(f.read(16)).decode() + self.size = f.readInt48() + self.type = f.readInt8() + + f.readInt8() # junk + + +class Cnmt(File): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Cnmt, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + self.titleId = None + self.version = None + self.titleType = None + self.headerOffset = None + self.contentEntryCount = None + self.metaEntryCount = None + self.contentEntries = [] + self.metaEntries = [] + + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Cnmt, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + + self.titleId = hx(self.read(8)[::-1]).decode() + self.version = self.readInt32() + self.titleType = self.readInt8() + + self.readInt8() # junk + + self.headerOffset = self.readInt16() + self.contentEntryCount = self.readInt16() + self.metaEntryCount = self.readInt16() + + self.contentEntries = [] + self.metaEntries = [] + + self.seek(0x20 + self.headerOffset) + for i in range(self.contentEntryCount): + self.contentEntries.append(ContentEntry(self)) + + for i in range(self.metaEntryCount): + self.metaEntries.append(MetaEntry(self)) + + + + + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('\n%sCnmt\n' % (tabs)) + Print.info('%stitleId = %s' % (tabs, self.titleId)) + Print.info('%sversion = %x' % (tabs, self.version)) + Print.info('%stitleType = %x' % (tabs, self.titleType)) + + for i in self.contentEntries: + Print.info('%s\tncaId: %s type = %x' % (tabs, i.ncaId, i.type)) + super(Cnmt, self).printInfo(maxDepth, indent) + + diff --git a/py-test/Fs/File.py b/py-test/Fs/File.py new file mode 100644 index 00000000..ed64e2b8 --- /dev/null +++ b/py-test/Fs/File.py @@ -0,0 +1,495 @@ +from enum import IntEnum +import Fs.Type +from nut import aes128,Print, Hex +from binascii import hexlify as hx, unhexlify as uhx +import hashlib +import os.path + +class BaseFile: + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + self.offset = 0x0 + self.size = None + self.f = None + self.crypto = None + self.cryptoKey = None + self.cryptoType = Fs.Type.Crypto.NONE + self.cryptoCounter = None + self.cryptoOffset = 0 + self.ctr_val = 0 + self.isPartition = False + self._children = [] + self._path = None + self._buffer = None + self._relativePos = 0x0 + self._bufferOffset = 0x0 + self._bufferSize = 0x1000 + self._bufferAlign = 0x1000 + self._bufferDirty = False + + if path and mode != None: + self.open(path, mode, cryptoType, cryptoKey, cryptoCounter) + + self.setupCrypto(cryptoType, cryptoKey, cryptoCounter) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def __del__(self): + self.close() + + def enableBufferedIO(self, size, align = 0): + self._bufferSize = size + self._bufferAlign = align + self._bufferOffset = None + self._relativePos = 0x0 + + def partition(self, offset = 0x0, size = None, n = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, autoOpen = True): + if not n: + n = File() + #Print.info('partition: ' + str(self) + ', ' + str(n)) + + n.offset = offset + + if not size: + size = self.size - n.offset - self.offset + + n.size = size + n.f = self + n.isPartition = True + + self._children.append(n) + + #Print.info('created partition for %s %x, size = %d' % (n.__class__.__name__, offset, size)) + if autoOpen == True: + n.open(None, None, cryptoType, cryptoKey, cryptoCounter) + + return n + + def removeChild(self, child): + a = [] + + for i in self._children: + if i != child: + a.append(i) + + self._children = a + + def read(self, size = None, direct = False): + if not size: + size = self.size + + return self.f.read(size) + + def readInt8(self, byteorder='little', signed = False): + return self.read(1)[0] + + def readInt16(self, byteorder='little', signed = False): + return int.from_bytes(self.read(2), byteorder=byteorder, signed=signed) + + def readInt32(self, byteorder='little', signed = False): + return int.from_bytes(self.read(4), byteorder=byteorder, signed=signed) + + def readInt48(self, byteorder='little', signed = False): + return int.from_bytes(self.read(6), byteorder=byteorder, signed=signed) + + def readInt64(self, byteorder='little', signed = False): + return int.from_bytes(self.read(8), byteorder=byteorder, signed=signed) + + def readInt128(self, byteorder='little', signed = False): + return int.from_bytes(self.read(16), byteorder=byteorder, signed=signed) + + def readInt(self, size, byteorder='little', signed = False): + return int.from_bytes(self.read(size), byteorder=byteorder, signed=signed) + + def write(self, value, size = None): + if size != None: + value = value + b'\0x00' * (size - len(value)) + #Print.info('writing to ' + hex(self.f.tell()) + ' ' + self.f.__class__.__name__) + #Hex.dump(value) + return self.f.write(value) + + def writeInt8(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(1, byteorder)) + + def writeInt16(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(2, byteorder)) + + def writeInt32(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(4, byteorder)) + + def writeInt64(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(8, byteorder)) + + def writeInt128(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(16, byteorder)) + + def writeInt(self, value, size, byteorder='little', signed = False): + return self.write(value.to_bytes(size, byteorder)) + + def seek(self, offset, from_what = 0): + if not self.isOpen(): + raise IOError('Trying to seek on closed file') + + if from_what == 0: + # seek from begining + self.f.seek(self.offset + offset) + #if self.cryptoType == Fs.Type.Crypto.CTR: + # self.crypto.set_ctr(self.setCounter(self.offset + self.tell())) + return + elif from_what == 1: + # seek from current position + self.f.seek(self.offset + offset) + + #if self.cryptoType == Fs.Type.Crypto.CTR: + # self.crypto.set_ctr(self.setCounter(self.offset + self.tell())) + return + elif from_what == 2: + # see from end + if offset > 0: + raise Exception('Invalid seek offset') + + self.f.seek(self.offset + offset + self.size) + + #if self.cryptoType == Fs.Type.Crypto.CTR: + # self.crypto.set_ctr(self.setCounter(self.offset + self.tell())) + return + + raise Exception('Invalid seek type') + + def rewind(self, offset = None): + if offset: + self.seek(-offset, 1) + else: + self.seek(0) + + def setupCrypto(self, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + if cryptoType != -1: + self.cryptoType = cryptoType + + if cryptoKey != -1: + self.cryptoKey = cryptoKey + + if cryptoCounter != -1: + self.cryptoCounter = cryptoCounter + + if self.cryptoType == Fs.Type.Crypto.CTR or self.cryptoType == Fs.Type.Crypto.BKTR: + if self.cryptoKey: + self.crypto = aes128.AESCTR(self.cryptoKey, nonce = self.cryptoCounter.copy()) + self.cryptoType = Fs.Type.Crypto.CTR + + self.enableBufferedIO(0x10, 0x10) + + elif self.cryptoType == Fs.Type.Crypto.XTS: + if self.cryptoKey: + self.crypto = aes128.AESXTS(self.cryptoKey) + self.cryptoType = Fs.Type.Crypto.XTS + + if self.size < 1 or self.size > 0xFFFFFF: + raise IOError('AESXTS Block too large or small') + + self.rewind() + self.enableBufferedIO(self.size, 0x10) + + elif self.cryptoType == Fs.Type.Crypto.BKTR: + self.cryptoType = Fs.Type.Crypto.BKTR + elif self.cryptoType == Fs.Type.Crypto.NCA0: + self.cryptoType = Fs.Type.Crypto.NCA0 + elif self.cryptoType == Fs.Type.Crypto.NONE: + self.cryptoType = Fs.Type.Crypto.NONE + + + def open(self, path, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + if path != None: + if self.isOpen(): + self.close() + + if isinstance(path, str): + self.f = open(path, mode) + self._path = path + + self.f.seek(0,2) + self.size = self.f.tell() + self.f.seek(0,0) + elif isinstance(path, BaseFile): + self.f = path + self.size = path.size + else: + raise IOError('Invalid file parameter') + + + self.setupCrypto(cryptoType, cryptoKey, cryptoCounter) + + def close(self): + if self.f: + self.flush() + for i in self._children: + i.close() + self._children = [] + + if not isinstance(self.f, BaseFile): + self.f.close() + else: + self.f.removeChild(self) + self.f = None + + def flush(self): + if self.f: + self.f.flush() + + def tell(self): + return self.f.tell() - self.offset + + def tellAbsolute(self): + if self.isPartition: + return self.f.tellAbsolute() + return self.f.tell() + + def eof(self): + return self.tell() >= self.size + + def isOpen(self): + return self.f != None + + def setCounter(self, ofs): + ctr = self.cryptoCounter.copy() + ofs >>= 4 + for j in range(8): + ctr[0x10-j-1] = ofs & 0xFF + ofs >>= 8 + return bytes(ctr) + + def setBktrCounter(self, ctr_val, ofs): + ctr = self.cryptoCounter.copy() + ofs >>= 4 + for j in range(8): + ctr[0x10-j-1] = ofs & 0xFF + ofs >>= 8 + + for j in range(4): + ctr[0x8-j-1] = ctr_val & 0xFF + ctr_val >>= 8 + + return bytes(ctr) + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + if self._path: + Print.info('%sFile Path: %s' % (tabs, self._path)) + Print.info('%sFile Size: %s' % (tabs, self.size)) + Print.info('%sFile Offset: %s' % (tabs, self.offset)) + + def sha256(self): + hash = hashlib.sha256() + + self.rewind() + + if self.size >= 10000: + while True: + buf = self.read(1 * 1024 * 1024, True) + if not buf: + break + hash.update(buf) + else: + hash.update(self.read(None, True)) + + return hash.hexdigest() + +class BufferedFile(BaseFile): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(BufferedFile, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + def read(self, size = None, direct = False): + if not size: + size = self.size + + if self._relativePos + size > self.size: + size = self.size - self._relativePos + + if size < 1: + return b'' + + if self._bufferOffset == None or self._buffer == None or self._relativePos < self._bufferOffset or (self._relativePos + size) > self._bufferOffset + len(self._buffer): + self.flushBuffer() + self._bufferOffset = (self._relativePos // self._bufferAlign) * self._bufferAlign + dataOffset = self._relativePos - self._bufferOffset + offsetModSize = (dataOffset + size) % self._bufferSize + garbageAtEnd = 0 if offsetModSize == 0 else self._bufferSize - offsetModSize + pageReadSize = dataOffset + size + garbageAtEnd + + if pageReadSize > self.size - self._bufferOffset: + pageReadSize = self.size - self._bufferOffset + + #Print.info('disk read %s\t\t: relativePos = %x, bufferOffset = %x, align = %x, size = %x, pageReadSize = %x, bufferSize = %x' % (self.__class__.__name__, self._relativePos, self._bufferOffset, self._bufferAlign, size, pageReadSize, self._bufferSize)) + super(BufferedFile, self).seek(self._bufferOffset) + self._buffer = super(BufferedFile, self).read(pageReadSize) + self.pageRefreshed() + if len(self._buffer) == 0: + raise IOError('read returned empty ' + hex(self.tellAbsolute())) + + offset = self._relativePos - self._bufferOffset + r = self._buffer[offset:offset+size] + self._relativePos += size + #Print.info(self._relativePos) + return r + + def write(self, value, size = None): + #if not size: + # size = len(value) + size = len(value) + + if self._bufferOffset == None or self._buffer == None or self._relativePos < self._bufferOffset or (self._relativePos + size) > self._bufferOffset + len(self._buffer): + self.flushBuffer() + + # read page into memory + pos = self.tell() + self.read(size) + self.seek(pos) + + offset = self._relativePos - self._bufferOffset + self._buffer = self._buffer[:offset] + (value) + self._buffer[offset+size:] + self._relativePos += size + self._bufferDirty = True + + return + + def flushBuffer(self): + if self.f != None and self._buffer != None and self._bufferDirty == True: + #Print.info('writing dirty page') + #Hex.dump(self._buffer) + super(BufferedFile, self).seek(self._bufferOffset) + super(BufferedFile, self).write(self.getPageFlushBuffer(self._buffer)) + self._bufferDirty = False + + def getPageFlushBuffer(self, buffer): + if self.crypto: + if self.cryptoType == Fs.Type.Crypto.CTR: + self.crypto.seek(self.offset + self._bufferOffset) + elif self.cryptoType == Fs.Type.Crypto.BKTR: + self.crypto.seek(self.offset + self._bufferOffset) + + return self.crypto.encrypt(buffer) + return buffer + + def flush(self): + if self.f: + self.flushBuffer() + super(BufferedFile, self).flush() + + def pageRefreshed(self): + pass + + def tell(self): + return self._relativePos + + def close(self): + self.flushBuffer() + if BufferedFile: + super(BufferedFile, self).close() + + def seek(self, offset, from_what = 0): + if not self.isOpen(): + raise IOError('Trying to seek on closed file') + + f = self.f + + + if from_what == 0: + # seek from begining + self._relativePos = offset + return + + elif from_what == 1: + # seek from current position + if self._buffer: + self._relativePos += offset + return + + r = f.seek(self.offset + offset) + return r + + elif from_what == 2: + # see from end + if offset > 0: + raise Exception('Invalid seek offset') + self._relativePos = self.size + offset + return + + raise Exception('Invalid seek type') + +class File(BufferedFile): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(File, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + def pageRefreshed(self): + if self.crypto: + if self.cryptoType == Fs.Type.Crypto.CTR or self.cryptoType == Fs.Type.Crypto.BKTR: + #Print.info('reading ctr from ' + hex(self._bufferOffset)) + self.crypto.seek(self.offset + self._bufferOffset) + else: + pass + #Print.info('reading from ' + hex(self._bufferOffset)) + self._buffer = self.crypto.decrypt(self._buffer) + return self._buffer + +class MemoryFile(File): + def __init__(self, buffer, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, offset = None): + super(MemoryFile, self).__init__() + self._bufferOffset = offset or self._bufferOffset + self.buffer = buffer + self.size = len(buffer) + self.setupCrypto(cryptoType = cryptoType, cryptoKey = cryptoKey, cryptoCounter = cryptoCounter) + + if self.crypto: + if self.cryptoType == Fs.Type.Crypto.CTR or self.cryptoType == Fs.Type.Crypto.BKTR: + self.crypto.seek(offset) + + self.buffer = self.crypto.decrypt(self.buffer) + + def read(self, size = None, direct = False): + if size == None: + size = self.size - self._relativePos + + return self.buffer[self._relativePos:self._relativePos+size] + + def write(self, value, size = None): + return + + def seek(self, offset, from_what = 0): + if from_what == 0: + self._relativePos = offset + return + elif from_what == 1: + self._relativePos = self.offset + offset + return + elif from_what == 2: + if offset > 0: + raise Exception('Invalid seek offset') + + self._relativePos = (self.offset + offset + self.size) + return + + def open(self, path, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + return + +class CryptoFile(BufferedFile): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(CryptoFile, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + def pageRefreshed(self): + self._buffer = self.crypto.decrypt(self._buffer) + return self._buffer + + def read2(self, size = None, direct = False): + return self.crypto.decrypt(super(CryptoFile, self).read(size)) + +class AesXtsFile(CryptoFile): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(AesXtsFile, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + +class AesCtrFile(CryptoFile): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(AesCtrFile, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + diff --git a/py-test/Fs/Hfs0.py b/py-test/Fs/Hfs0.py new file mode 100644 index 00000000..a6c59598 --- /dev/null +++ b/py-test/Fs/Hfs0.py @@ -0,0 +1,166 @@ +from nut import aes128 +from nut import Hex +from binascii import hexlify as hx, unhexlify as uhx +from struct import pack as pk, unpack as upk +from Fs.File import BaseFile +from Fs.File import File +from hashlib import sha256 +from Fs.Pfs0 import Pfs0 +from Fs.BaseFs import BaseFs +import os +import re +from pathlib import Path +from nut import Keys +from nut import Print +import Fs + +MEDIA_SIZE = 0x200 + +class Hfs0Stream(BaseFile): + def __init__(self, f, mode = 'wb'): + super(Hfs0Stream, self).__init__(f, mode) + self.headerSize = 0x8000 + self.files = [] + + self.actualSize = 0 + + self.seek(self.headerSize) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def write(self, value, size = None): + super(Hfs0Stream, self).write(value, len(value)) + if self.tell() > self.actualSize: + self.actualSize = self.tell() + + def add(self, name, size, pleaseNoPrint = None): + Print.info('[ADDING] {0} {1} bytes to NSP'.format(name, size), pleaseNoPrint) + self.files.append({'name': name, 'size': size, 'offset': self.f.tell()}) + return self.partition(self.f.tell(), size, n = BaseFile()) + + def get(self, name): + for i in self.files: + if i['name'] == name: + return i + return None + + def resize(self, name, size): + for i in self.files: + if i['name'] == name: + i['size'] = size + return True + return False + + def currentFileSize(self): + return self.f.tell() - self.files[-1]['offset'] + + def close(self): + if self.isOpen(): + self.seek(0) + self.write(self.getHeader()) + super(Hfs0Stream, self).close() + + def getHeader(self): + stringTable = '\x00'.join(file['name'] for file in self.files) + + headerSize = 0x10 + len(self.files) * 0x40 + len(stringTable) + + h = b'' + h += b'HFS0' + h += len(self.files).to_bytes(4, byteorder='little') + h += (len(stringTable)).to_bytes(4, byteorder='little') + h += b'\x00\x00\x00\x00' + + stringOffset = 0 + + for f in self.files: + sizeOfHashedRegion = 0x200 if 0x200 < f['size'] else f['size'] + + h += (f['offset'] - headerSize).to_bytes(8, byteorder='little') + h += f['size'].to_bytes(8, byteorder='little') + h += stringOffset.to_bytes(4, byteorder='little') + h += sizeOfHashedRegion.to_bytes(4, byteorder='little') + h += b'\x00' * 8 + h += b'\x00' * 0x20 # sha256 hash of region + + stringOffset += len(f['name']) + 1 + + h += stringTable.encode() + + return h + +class Hfs0(Pfs0): + def __init__(self, buffer, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Hfs0, self).__init__(buffer, path, mode, cryptoType, cryptoKey, cryptoCounter) + + def open(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + r = super(BaseFs, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + + self.magic = self.read(0x4); + if self.magic != b'HFS0': + raise IOError('Not a valid HFS0 partition %s @ %x' % (str(self.magic), self.tellAbsolute() - 4)) + + + fileCount = self.readInt32() + stringTableSize = self.readInt32() + self.readInt32() # junk data + + self.seek(0x10 + fileCount * 0x40) + stringTable = self.read(stringTableSize) + stringEndOffset = stringTableSize + + headerSize = 0x10 + 0x40 * fileCount + stringTableSize + self.files = [] + + for i in range(fileCount): + i = fileCount - i - 1 + self.seek(0x10 + i * 0x40) + + offset = self.readInt64() + size = self.readInt64() + nameOffset = self.readInt32() # just the offset + name = stringTable[nameOffset:stringEndOffset].decode('utf-8').rstrip(' \t\r\n\0') + stringEndOffset = nameOffset + + self.readInt32() # junk data + + f = Fs.factory(Path(name)) + + f._path = name + f.offset = offset + f.size = size + self.files.append(self.partition(offset + headerSize, f.size, f)) + + self.files.reverse() + + def unpack(self, path, extractregex="*"): + os.makedirs(str(path), exist_ok=True) + + for hfsf in self: + filePath_str = str(path.joinpath(hfsf._path)) + if not re.match(extractregex, filePath_str): + continue + f = open(filePath_str, 'wb') + hfsf.rewind() + i = 0 + + pageSize = 0x100000 + + while True: + buf = hfsf.read(pageSize) + if len(buf) == 0: + break + i += len(buf) + f.write(buf) + f.close() + Print.info(filePath_str) + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('\n%sHFS0\n' % (tabs)) + super(Pfs0, self).printInfo(maxDepth, indent) diff --git a/py-test/Fs/Ivfc.py b/py-test/Fs/Ivfc.py new file mode 100644 index 00000000..01d7599f --- /dev/null +++ b/py-test/Fs/Ivfc.py @@ -0,0 +1,46 @@ +from nut import aes128 +from nut import Hex +from binascii import hexlify as hx, unhexlify as uhx +from struct import pack as pk, unpack as upk +from Fs.File import File +from hashlib import sha256 +import os +import re +import pathlib +from nut import Keys +from nut import Print + +MEDIA_SIZE = 0x200 + + +class IvfcLevel: + def __init__(self, offset, size, blockSize, reserved): + self.offset = offset + self.size = size + self.blockSize = blockSize + self.reserved = reserved + +class Ivfc(File): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + self.magic = None + self.magicNumber = None + self.masterHashSize = None + self.numberLevels = None + self.levels = [] + self.hash = None + super(Ivfc, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Ivfc, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + self.magic = self.read(0x4) + self.magicNumber = self.readInt32() + self.masterHashSize = self.readInt32() + self.numberLevels = self.readInt32() + + for i in range(self.numberLevels-1): + self.levels.append(IvfcLevel(self.readInt64(), self.readInt64(), self.readInt32(), self.readInt32())) + + self.read(32) + self.hash = self.read(32) + \ No newline at end of file diff --git a/py-test/Fs/Nacp.py b/py-test/Fs/Nacp.py new file mode 100644 index 00000000..08e6ace7 --- /dev/null +++ b/py-test/Fs/Nacp.py @@ -0,0 +1,609 @@ +from Fs.File import File +from binascii import hexlify as hx, unhexlify as uhx +from enum import IntEnum +from nut import Print +from nut import Keys +#Some of this may have changed in 7.x.x+ + +class NacpLanguageType(IntEnum): + AmericanEnglish = 0 + BritishEnglish = 1 + Japanese = 2 + French = 3 + German = 4 + LatinAmericanSpanish = 5 + Spanish = 6 + Italian = 7 + Dutch = 8 + CanadianFrench = 9 + Portuguese = 10 + Russian = 11 + Korean = 12 + TraditionalChinese = 13 + SimplifiedChinese = 14 + +class NacpLanguage: + def __init__(self): + self.name = None + self.publisher = None + + +class OrganizationType(IntEnum): + CERO = 0 + GRACGCRB = 1 + GSRMR = 2 + ESRB = 3 + ClassInd = 4 + USK = 5 + PEGI = 6 + PEGIPortugal = 7 + PEGIBBFC = 8 + Russian = 9 + ACB = 10 + OFLC = 11 + +class RatingAge: + def __init__(self): + self.age = None + + +class Nacp(File): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Nacp, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + + self.languages = [] + + for i in range(15): + self.languages.append(NacpLanguage()) + + self.isbn = None + self.startupUserAccount = None + self.userAccountSwitchLock = None + self.addOnContentRegistrationType = None + self.attribute = None + self.parentalControl = None + self.screenshot = None + self.videoCapture = None + self.dataLossConfirmation = None + self.playLogPolicy = None + self.presenceGroupId = None + + self.ages = [] + + for i in range(12): + self.ages.append(RatingAge()) + + self.displayVersion = None + self.addOnContentBaseId = None + self.saveDataOwnerId = None + self.userAccountSaveDataSize = None + self.userAccountSaveDataJournalSize = None + self.deviceSaveDataSize = None + self.deviceSaveDataJournalSize = None + self.bcatDeliveryCacheStorageSize = None + self.applicationErrorCodeCategory = None + self.localCommunicationId = None + self.logoType = None + self.logoHandling = None + self.runtimeAddOnContentInstall = None + self.crashReport = None + self.hdcp = None + self.seedForPseudoDeviceId = None + self.bcatPassphrase = None + self.userAccountSaveDataSizeMax = None + self.userAccountSaveDataJournalSizeMax = None + self.deviceSaveDataSizeMax = None + self.deviceSaveDataJournalSizeMax = None + self.temporaryStorageSize = None + self.cacheStorageSize = None + self.cacheStorageJournalSize = None + self.cacheStorageDataAndJournalSizeMax = None + self.cacheStorageIndexMax = None + self.playLogQueryableApplicationId = None + self.playLogQueryCapability = None + self.repair = None + self.programIndex = None + self.requiredNetworkServiceLicenseOnLaunch = None + + + def getName(self, i): + self.seek(i * 0x300) + self.languages[i].name = self.read(0x200) + self.languages[i].name = self.languages[i].name.split(b'\0', 1)[0].decode('utf-8') + return self.languages[i].name + + + def getPublisher(self, i): + self.seek(i * 0x300 + 0x200) + self.languages[i].publisher = self.read(0x100) + self.languages[i].publisher = self.languages[i].publisher.split(b'\0', 1)[0].decode('utf-8') + return self.languages[i].publisher + + + def getIsbn(self): + self.seek(0x3000) + self.isbn = self.read(0x24).split(b'\0', 1)[0].decode('utf-8') + return self.isbn + + + def getStartupUserAccount(self): + self.seek(0x3025) + b = self.readInt8('little') + if b == 0: + self.startupUserAccount = 'None' + elif b == 1: + self.startupUserAccount = 'Required' + elif b == 2: + self.startupUserAccount = 'RequiredWithNetworkServiceAccountAvailable' + else: + self.startupUserAccount = 'Unknown' + return self.startupUserAccount + + + def getUserAccountSwitchLock(self): + self.seek(0x3026) + b = self.readInt8('little') + if b == 0: + self.userAccountSwitchLock = 'Disable' + elif b == 1: + self.userAccountSwitchLock = 'Enable' + else: + self.userAccountSwitchLock = 'Unknown' + return self.userAccountSwitchLock + + + def getAddOnContentRegistrationType(self): + self.seek(0x3027) + b = self.readInt8('little') + if b == 0: + self.addOnContentRegistrationType = 'AllOnLaunch' + elif b == 1: + self.addOnContentRegistrationType = 'OnDemand' + else: + self.addOnContentRegistrationType = 'Unknown' + return self.addOnContentRegistrationType + + + def getAttribute(self): + self.seek(0x3028) + b = self.readInt8('little') + if b == 0: + self.attribute = 'None' + elif b == 1: + self.attribute = 'Demo' + elif b == 2: + self.attribute = 'RetailInteractiveDisplay' + else: + self.attribute = 'Unknown' + return self.attribute + + + def getParentalControl(self): + self.seek(0x3030) + b = self.readInt8('little') + if b == 0: + self.parentalControl = 'None' + elif b == 1: + self.parentalControl = 'FreeCommunication' + else: + self.parentalControl = 'Unknown' + return self.parentalControl + + + def getScreenshot(self): + self.seek(0x3034) + b = self.readInt8('little') + if b == 0: + self.screenshot = 'Allow' + elif b == 1: + self.screenshot = 'Deny' + else: + self.screenshot = 'Unknown' + return self.screenshot + + + def getVideoCapture(self): + self.seek(0x3035) + b = self.readInt8('little') + if b == 0: + self.videoCapture = 'Disable' + elif b == 1: + self.videoCapture = 'Manual' + elif b == 2: + self.videoCapture = 'Enable' + else: + self.videoCapture = 'Unknown' + return self.videoCapture + + + def getDataLossConfirmation(self): + self.seek(0x3036) + b = self.readInt8('little') + if b == 0: + self.dataLossConfirmation = 'None' + elif b == 1: + self.dataLossConfirmation = 'Required' + else: + self.dataLossConfirmation = 'Unknown' + return self.dataLossConfirmation + + + def getPlayLogPolicy(self): + self.seek(0x3037) + b = self.readInt8('little') + if b == 0: + self.playLogPolicy = 'All' + elif b == 1: + self.playLogPolicy = 'LogOnly' + elif b == 2: + self.playLogPolicy = 'None' + else: + self.playLogPolicy = 'Unknown' + return self.playLogPolicy + + + def getPresenceGroupId(self): + self.seek(0x3038) + self.presenceGroupId = self.readInt64('little') + return self.presenceGroupId + + + def getRatingAge(self, i): + self.seek(i + 0x3040) + b = self.readInt8('little') + if b == 0: + self.ages[i].age = '0' + elif b == 3: + self.ages[i].age = '3' + elif b == 4: + self.ages[i].age = '4' + elif b == 6: + self.ages[i].age = '6' + elif b == 7: + self.ages[i].age = '7' + elif b == 8: + self.ages[i].age = '8' + elif b == 10: + self.ages[i].age = '10' + elif b == 12: + self.ages[i].age = '12' + elif b == 13: + self.ages[i].age = '13' + elif b == 14: + self.ages[i].age = '14' + elif b == 15: + self.ages[i].age = '15' + elif b == 16: + self.ages[i].age = '16' + elif b == 17: + self.ages[i].age = '17' + elif b == 18: + self.ages[i].age = '18' + else: + self.ages[i].age = 'Unknown' + return self.ages[i].age + + + def getDisplayVersion(self): + self.seek(0x3060) + self.displayVersion = self.read(0xF) + self.displayVersion = self.displayVersion.split(b'\0', 1)[0].decode('utf-8') + return self.displayVersion + + + def getAddOnContentBaseId(self): + self.seek(0x3070) + self.addOnContentBaseId = self.readInt64('little') + return self.addOnContentBaseId + + + def getSaveDataOwnerId(self): + self.seek(0x3078) + self.saveDataOwnerId = self.readInt64('little') + return self.saveDataOwnerId + + + def getUserAccountSaveDataSize(self): + self.seek(0x3080) + self.userAccountSaveDataSize = self.readInt64('little') + return self.userAccountSaveDataSize + + + def getUserAccountSaveDataJournalSize(self): + self.seek(0x3088) + self.userAccountSaveDataJournalSize = self.readInt64('little') + return self.userAccountSaveDataJournalSize + + + def getDeviceSaveDataSize(self): + self.seek(0x3090) + self.deviceSaveDataSize = self.readInt64('little') + return self.deviceSaveDataSize + + + def getDeviceSaveDataJournalSize(self): + self.seek(0x3098) + self.deviceSaveDataJournalSize = self.readInt64('little') + return self.deviceSaveDataJournalSize + + + def getBcatDeliveryCacheStorageSize(self): + self.seek(0x30A0) + self.bcatDeliveryCacheStorageSize = self.readInt64('little') + return self.bcatDeliveryCacheStorageSize + + + def getApplicationErrorCodeCategory(self): + self.seek(0x30A8) + self.applicationErrorCodeCategory = self.read(0x7).split(b'\0', 1)[0].decode('utf-8') + return self.applicationErrorCodeCategory + + + def getLocalCommunicationId(self): + self.seek(0x30B0) + self.localCommunicationId = self.readInt64('little') + return self.localCommunicationId + + + def getLogoType(self): + self.seek(0x30F0) + b = self.readInt8('little') + if b == 0: + self.logoType = 'LicensedByNintendo' + elif b == 2: + self.logoType = 'Nintendo' + else: + self.logoType = 'Unknown' + return self.logoType + + + def getLogoHandling(self): + self.seek(0x30F1) + b = self.readInt8('little') + if b == 0: + self.logoHandling = 'Auto' + elif b == 1: + self.logoHandling = 'Manual' + else: + self.logoHandling = 'Unknown' + return self.logoHandling + + + def getRuntimeAddOnContentInstall(self): + self.seek(0x30F2) + b = self.readInt8('little') + if b == 0: + self.runtimeAddOnContentInstall = 'Deny' + elif b == 1: + self.runtimeAddOnContentInstall = 'AllowAppend' + else: + self.runtimeAddOnContentInstall = 'Unknown' + return self.runtimeAddOnContentInstall + + + def getCrashReport(self): + self.seek(0x30F6) + b = self.readInt8('little') + if b == 0: + self.crashReport = 'Deny' + elif b == 1: + self.crashReport = 'Allow' + else: + self.crashReport = 'Unknown' + return self.crashReport + + + def getHdcp(self): + self.seek(0x30F7) + b = self.readInt8('little') + if b == 0: + self.hdcp = 'None' + elif b == 1: + self.hdcp = 'Required' + else: + self.hdcp = 'Unknown' + return self.hdcp + + + def getSeedForPseudoDeviceId(self): + self.seek(0x30F8) + self.seedForPseudoDeviceId = self.readInt64('little') + return self.seedForPseudoDeviceId + + + def getBcatPassphrase(self): + self.seek(0x3100) + self.bcatPassphrase = self.read(0x40).split(b'\0', 1)[0].decode('utf-8') + return self.bcatPassphrase + + + def getUserAccountSaveDataSizeMax(self): + self.seek(0x3148) + self.userAccountSaveDataSizeMax = self.readInt64('little') + return self.userAccountSaveDataSizeMax + + + def getUserAccountSaveDataJournalSizeMax(self): + self.seek(0x3150) + self.userAccountSaveDataJournalSizeMax = self.readInt64('little') + return self.userAccountSaveDataJournalSizeMax + + + def getDeviceSaveDataSizeMax(self): + self.seek(0x3158) + self.deviceSaveDataSizeMax = self.readInt64('little') + return self.deviceSaveDataSizeMax + + + def getDeviceSaveDataJournalSizeMax(self): + self.seek(0x3160) + self.deviceSaveDataJournalSizeMax = self.readInt64('little') + return self.deviceSaveDataJournalSizeMax + + + def getTemporaryStorageSize(self): + self.seek(0x3168) + self.temporaryStorageSize = self.readInt64('little') + return self.temporaryStorageSize + + + def getCacheStorageSize(self): + self.seek(0x3170) + self.cacheStorageSize = self.readInt64('little') + return self.cacheStorageSize + + + def getCacheStorageJournalSize(self): + self.seek(0x3178) + self.cacheStorageJournalSize = self.readInt64('little') + return self.cacheStorageJournalSize + + + def getCacheStorageDataAndJournalSizeMax(self): + self.seek(0x3180) + self.cacheStorageDataAndJournalSizeMax = self.readInt32('little') + return self.cacheStorageDataAndJournalSizeMax + + + def getCacheStorageIndexMax(self): + self.seek(0x3188) + self.cacheStorageIndexMax = self.readInt16('little') + return self.cacheStorageIndexMax + + + def getPlayLogQueryableApplicationId(self): + self.seek(0x3190) + self.playLogQueryableApplicationId = self.readInt64('little') + return self.playLogQueryableApplicationId + + + def getPlayLogQueryCapability(self): + self.seek(0x3210) + b = self.readInt8('little') + if b == 0: + self.playLogQueryCapability = 'None' + elif b == 1: + self.playLogQueryCapability = 'WhiteList' + elif b == 2: + self.playLogQueryCapability = 'All' + else: + self.playLogQueryCapability = 'Unknown' + return self.playLogQueryCapability + + + def getRepair(self): + self.seek(0x3211) + b = self.readInt8('little') + if b == 0: + self.repair = 'None' + elif b == 1: + self.repair = 'SuppressGameCardAccess' + else: + self.repair = 'Unknown' + return self.repair + + + def getProgramIndex(self): + self.seek(0x3212) + self.programIndex = self.readInt8('little') + return self.programIndex + + + def getRequiredNetworkServiceLicenseOnLaunch(self): + self.seek(0x3213) + b = self.readInt8('little') + if b == 0: + self.requiredNetworkServiceLicenseOnLaunch = 'None' + elif b == 1: + self.requiredNetworkServiceLicenseOnLaunch = 'Common' + else: + self.requiredNetworkServiceLicenseOnLaunch = 'Unknown' + return self.requiredNetworkServiceLicenseOnLaunch + + + def printInfo(self, indent = 0): + tabs = '\t' * indent + Print.info('\n%sNintendo Application Control Property (NACP)\n' % (tabs)) + super(Nacp, self).printInfo(indent) + + for i in range(15): + if self.getName(i) == '': + pass + else: + Print.info('Title:') + Print.info(' Language: ' + str(NacpLanguageType(i)).replace('NacpLanguageType.', '')) + Print.info(' Name: ' + self.getName(i)) + Print.info(' Publisher: ' + self.getPublisher(i)) + + if str(self.getIsbn()) == '': + pass + else: + Print.info('Isbn: ' + str(self.getIsbn())) + + Print.info('AddOnContentRegistrationType: ' + str(self.getAddOnContentRegistrationType())) + Print.info('StartupUserAccount: ' + str(self.getStartupUserAccount())) + Print.info('UserAccountSwitchLock: ' + str(self.getUserAccountSwitchLock())) + Print.info('Attribute: ' + str(self.getAttribute())) + Print.info('ParentalControl: ' + str(self.getParentalControl())) + Print.info('Screenshot: ' + str(self.getScreenshot())) + Print.info('VideoCapture: ' + str(self.getVideoCapture())) + Print.info('DataLossConfirmation: ' + str(self.getDataLossConfirmation())) + Print.info('PlayLogPolicy: ' + str(self.getPlayLogPolicy())) + Print.info('PresenceGroupId: ' + '0x' + hex(self.getPresenceGroupId()).replace('0x', '').zfill(16)) + + for i in range(12): + if str(self.getRatingAge(i)) == 'Unknown': + pass + else: + Print.info('Rating:') + Print.info(' Organization: ' + str(OrganizationType(i)).replace('OrganizationType.', '')) + Print.info(' Age: ' + str(self.getRatingAge(i))) + + Print.info('DisplayVersion: ' + str(self.getDisplayVersion())) + Print.info('AddOnContentBaseId: ' + '0x' + hex(self.getAddOnContentBaseId()).replace('0x', '').zfill(16)) + Print.info('SaveDataOwnerId: ' + '0x' + hex(self.getSaveDataOwnerId()).replace('0x', '').zfill(16)) + Print.info('UserAccountSaveDataSize: ' + '0x' + hex(self.getUserAccountSaveDataSize()).replace('0x', '').zfill(16)) + Print.info('UserAccountSaveDataJournalSize: ' + '0x' + hex(self.getUserAccountSaveDataJournalSize()).replace('0x', '').zfill(16)) + Print.info('DeviceSaveDataSize: ' + '0x' + hex(self.getDeviceSaveDataSize()).replace('0x', '').zfill(16)) + Print.info('DeviceSaveDataJournalSize: ' + '0x' + hex(self.getDeviceSaveDataJournalSize()).replace('0x', '').zfill(16)) + + if hex(self.getBcatDeliveryCacheStorageSize()).replace('0x', '').zfill(16) == '0000000000000000': + pass + else: + Print.info('BcatDeliveryCacheStorageSize: ' + '0x' + hex(self.getBcatDeliveryCacheStorageSize()).replace('0x', '').zfill(16)) + + if str(self.getApplicationErrorCodeCategory()) == '': + pass + else: + Print.info('ApplicationErrorCodeCategory: ' + str(self.getApplicationErrorCodeCategory())) + + Print.info('LocalCommunicationId: ' + '0x' + hex(self.getLocalCommunicationId()).replace('0x', '').zfill(16)) + Print.info('LogoType: ' + str(self.getLogoType())) + Print.info('LogoHandling: ' + str(self.getLogoHandling())) + Print.info('RuntimeAddOnContentInstall: ' + str(self.getRuntimeAddOnContentInstall())) + Print.info('CrashReport: ' + str(self.getCrashReport())) + Print.info('Hdcp: ' + str(self.getHdcp())) + Print.info('SeedForPseudoDeviceId: ' + '0x' + hex(self.getSeedForPseudoDeviceId()).replace('0x', '').zfill(16)) + + if str(self.getBcatPassphrase()) == '0000000000000000000000000000000000000000000000000000000000000000': + pass + elif str(self.getBcatPassphrase()) == '': + pass + else: + Print.info('BcatPassphrase: ' + str(self.getBcatPassphrase())) + + Print.info('UserAccountSaveDataSizeMax: ' + '0x' + hex(self.getUserAccountSaveDataSizeMax()).replace('0x', '').zfill(16)) + Print.info('UserAccountSaveDataJournalSizeMax: ' + '0x' + hex(self.getUserAccountSaveDataJournalSizeMax()).replace('0x', '').zfill(16)) + Print.info('DeviceSaveDataSizeMax: ' + '0x' + hex(self.getDeviceSaveDataSizeMax()).replace('0x', '').zfill(16)) + Print.info('DeviceSaveDataJournalSizeMax: ' + '0x' + hex(self.getDeviceSaveDataJournalSizeMax()).replace('0x', '').zfill(16)) + Print.info('TemporaryStorageSize: ' + '0x' + hex(self.getTemporaryStorageSize()).replace('0x', '').zfill(16)) + Print.info('CacheStorageSize: ' + '0x' + hex(self.getCacheStorageSize()).replace('0x', '').zfill(16)) + Print.info('CacheStorageJournalSize: ' + '0x' + hex(self.getCacheStorageJournalSize()).replace('0x', '').zfill(16)) + Print.info('CacheStorageDataAndJournalSizeMax: ' + '0x' + hex(self.getCacheStorageDataAndJournalSizeMax()).replace('0x', '').zfill(8)) + Print.info('CacheStorageIndexMax: ' + '0x' + hex(self.getCacheStorageIndexMax()).replace('0x', '').zfill(4)) + Print.info('PlayLogQueryableApplicationId: ' + '0x' + hex(self.getPlayLogQueryableApplicationId()).replace('0x', '').zfill(16)) + Print.info('PlayLogQueryCapability: ' + str(self.getPlayLogQueryCapability())) + Print.info('Repair: ' + str(self.getRepair())) + Print.info('ProgramIndex: ' + str(self.getProgramIndex())) + Print.info('RequiredNetworkServiceLicenseOnLaunch: ' + str(self.getRequiredNetworkServiceLicenseOnLaunch())) diff --git a/py-test/Fs/Nca.py b/py-test/Fs/Nca.py new file mode 100644 index 00000000..c6639bba --- /dev/null +++ b/py-test/Fs/Nca.py @@ -0,0 +1,295 @@ +from nut import aes128 +from nut import Hex +from binascii import hexlify as hx, unhexlify as uhx +from struct import pack as pk, unpack as upk +from hashlib import sha256 +import os +import re +import pathlib +from nut import Keys +from nut import Print +import Fs +import nut +from Fs.File import File +from Fs.Rom import Rom +from Fs.Pfs0 import Pfs0 +from Fs.BaseFs import BaseFs +from nut import Titles + +MEDIA_SIZE = 0x200 + + +class SectionTableEntry: + def __init__(self, d): + self.mediaOffset = int.from_bytes(d[0x0:0x4], byteorder='little', signed=False) + self.mediaEndOffset = int.from_bytes(d[0x4:0x8], byteorder='little', signed=False) + + self.offset = self.mediaOffset * MEDIA_SIZE + self.endOffset = self.mediaEndOffset * MEDIA_SIZE + + self.unknown1 = int.from_bytes(d[0x8:0xc], byteorder='little', signed=False) + self.unknown2 = int.from_bytes(d[0xc:0x10], byteorder='little', signed=False) + self.sha1 = None + + +def GetSectionFilesystem(buffer, cryptoKey): + fsType = buffer[0x3] + if fsType == Fs.Type.Fs.PFS0: + return Pfs0(buffer, cryptoKey = cryptoKey) + + if fsType == Fs.Type.Fs.ROMFS: + return Rom(buffer, cryptoKey = cryptoKey) + + return BaseFs(buffer, cryptoKey = cryptoKey) + +class NcaHeader(File): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + self.signature1 = None + self.signature2 = None + self.magic = None + self.isGameCard = None + self.contentType = None + self.cryptoType = None + self.keyIndex = None + self.size = None + self.titleId = None + self.contentIndex = None + self.sdkVersion = None + self.cryptoType2 = None + self.rightsId = None + self.titleKeyDec = None + self.masterKey = None + self.sectionTables = [] + self.keys = [] + + super(NcaHeader, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(NcaHeader, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + self.signature1 = self.read(0x100) + self.signature2 = self.read(0x100) + self.magic = self.read(0x4) + self.isGameCard = self.readInt8() + self.contentType = self.readInt8() + + try: + self.contentType = Fs.Type.Content(self.contentType) + except: + pass + + self.cryptoType = self.readInt8() + self.keyIndex = self.readInt8() + self.size = self.readInt64() + self.titleId = hx(self.read(8)[::-1]).decode('utf-8').upper() + self.contentIndex = self.readInt32() + self.sdkVersion = self.readInt32() + self.cryptoType2 = self.readInt8() + + self.read(0xF) # padding + + self.rightsId = hx(self.read(0x10)) + + if self.magic not in [b'NCA3', b'NCA2']: + raise Exception('Failed to decrypt NCA header: ' + str(self.magic)) + + self.sectionHashes = [] + + for i in range(4): + self.sectionTables.append(SectionTableEntry(self.read(0x10))) + + for i in range(4): + self.sectionHashes.append(self.sectionTables[i]) + + self.masterKey = (self.cryptoType if self.cryptoType > self.cryptoType2 else self.cryptoType2)-1 + + if self.masterKey < 0: + self.masterKey = 0 + + + self.encKeyBlock = self.getKeyBlock() + #for i in range(4): + # offset = i * 0x10 + # key = encKeyBlock[offset:offset+0x10] + # Print.info('enc %d: %s' % (i, hx(key))) + + + #crypto = aes128.AESECB(Keys.keyAreaKey(self.masterKey, 0)) + self.keyBlock = Keys.unwrapAesWrappedTitlekey(self.encKeyBlock, self.masterKey) + self.keys = [] + for i in range(4): + offset = i * 0x10 + key = self.keyBlock[offset:offset+0x10] + #Print.info('dec %d: %s' % (i, hx(key))) + self.keys.append(key) + + if self.hasTitleRights(): + titleRightsTitleId = self.rightsId.decode()[0:16].upper() + + if titleRightsTitleId in Titles.keys() and Titles.get(titleRightsTitleId).key: + self.titleKeyDec = Keys.decryptTitleKey(uhx(Titles.get(titleRightsTitleId).key), self.masterKey) + else: + Print.info('could not find title key %s!' % titleRightsTitleId) + else: + self.titleKeyDec = self.key() + + return True + + def realTitleId(self): + if not self.hasTitleRights(): + return self.titleId + + return self.getRightsIdStr()[0:16] + + def key(self): + return self.keys[2] + + def hasTitleRights(self): + return self.rightsId != (b'0' * 32) + + def getKeyBlock(self): + self.seek(0x300) + return self.read(0x40) + + def setKeyBlock(self, value): + if len(value) != 0x40: + raise IOError('invalid keyblock size') + + self.seek(0x300) + return self.write(value) + + def getCryptoType(self): + self.seek(0x206) + return self.readInt8() + + def setCryptoType(self, value): + self.seek(0x206) + self.writeInt8(value) + + def getCryptoType2(self): + self.seek(0x220) + return self.readInt8() + + def setCryptoType2(self, value): + self.seek(0x220) + self.writeInt8(value) + + def getRightsId(self): + self.seek(0x230) + return self.readInt128('big') + + def getRightsIdStr(self): + self.seek(0x230) + return hx(self.read(16)).decode() + + def setRightsId(self, value): + self.seek(0x230) + self.writeInt128(value, 'big') + + def getIsGameCard(self): + self.seek(0x204) + return self.readInt8() + + def setIsGameCard(self, value): + self.seek(0x204) + self.writeInt8(value) + + +class Nca(File): + def __init__(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + self.header = None + self.sectionFilesystems = [] + self.sections = [] + super(Nca, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + def __iter__(self): + return self.sectionFilesystems.__iter__() + + def __getitem__(self, key): + return self.sectionFilesystems[key] + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Nca, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + + self.header = NcaHeader() + self.partition(0x0, 0xC00, self.header, Fs.Type.Crypto.XTS, uhx(Keys.get('header_key'))) + #Print.info('partition complete, seeking') + self.header.seek(0x400) + #Print.info('reading') + #Hex.dump(self.header.read(0x200)) + #sys.exit() + + for i in range(4): + hdr = self.header.read(0x200) + section = BaseFs(hdr, cryptoKey = self.header.titleKeyDec) + fs = GetSectionFilesystem(hdr, cryptoKey = -1) + #Print.info('fs type = ' + hex(fs.fsType)) + #Print.info('fs crypto = ' + hex(fs.cryptoType)) + #Print.info('st end offset = ' + str(self.header.sectionTables[i].endOffset - self.header.sectionTables[i].offset)) + #Print.info('fs offset = ' + hex(self.header.sectionTables[i].offset)) + #Print.info('fs section start = ' + hex(fs.sectionStart)) + #Print.info('titleKey = ' + hex(self.header.titleKeyDec)) + + self.partition(self.header.sectionTables[i].offset, self.header.sectionTables[i].endOffset - self.header.sectionTables[i].offset, section, cryptoKey = self.header.titleKeyDec) + + try: + section.partition(fs.sectionStart, section.size - fs.sectionStart, fs) + except BaseException as e: + pass + #Print.info(e) + #raise + + if fs.fsType: + self.sectionFilesystems.append(fs) + self.sections.append(section) + + fs.open(None, 'rb') + + + self.titleKeyDec = None + + def masterKey(self): + return max(self.header.cryptoType, self.header.cryptoType2) + + def buildId(self): + if self.header.contentType != Fs.Type.Content.PROGRAM: + return None + + try: + f = self[0]['main'] + f.seek(0x40) + return hx(f.read(0x20)).decode('utf8').upper() + except IOError as e: + pass + except: + raise + return None + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('\n%sNCA Archive\n' % (tabs)) + super(Nca, self).printInfo(maxDepth, indent) + + Print.info(tabs + 'magic = ' + str(self.header.magic)) + Print.info(tabs + 'titleId = ' + str(self.header.titleId)) + Print.info(tabs + 'rightsId = ' + str(self.header.rightsId)) + Print.info(tabs + 'isGameCard = ' + hex(self.header.isGameCard)) + Print.info(tabs + 'contentType = ' + str(self.header.contentType)) + Print.info(tabs + 'cryptoType = ' + str(self.cryptoType)) + Print.info(tabs + 'Size: ' + str(self.header.size)) + Print.info(tabs + 'crypto master key: ' + str(self.header.cryptoType)) + Print.info(tabs + 'crypto master key2: ' + str(self.header.cryptoType2)) + Print.info(tabs + 'key Index: ' + str(self.header.keyIndex)) + #Print.info(tabs + 'key Block: ' + str(self.header.getKeyBlock())) + for key in self.header.keys: + if key: + Print.info(tabs + 'key Block: ' + str(hx(key))) + + if(indent+1 < maxDepth): + Print.info('\n%sPartitions:' % (tabs)) + + for s in self: + s.printInfo(maxDepth, indent+1) + + if self.header.contentType == Fs.Type.Content.PROGRAM: + Print.info(tabs + 'build Id: ' + str(self.buildId())) diff --git a/py-test/Fs/Nsp.py b/py-test/Fs/Nsp.py new file mode 100644 index 00000000..e8ba37e0 --- /dev/null +++ b/py-test/Fs/Nsp.py @@ -0,0 +1,447 @@ +from nut import aes128 +from nut import Hex +from binascii import hexlify as hx, unhexlify as uhx +from struct import pack as pk, unpack as upk +from Fs.File import File +from hashlib import sha256 +import Fs +import os +import re +import pathlib +from nut import Keys +from nut import Print +from Fs.Pfs0 import Pfs0 +from Fs.Ticket import Ticket +from Fs.Nca import Nca +import enlighten +import shutil +from nut import Titles +from nut.Titles import Title +from lib.PathTools import * + +MEDIA_SIZE = 0x200 + +class Nsp(Pfs0): + def __init__(self, path = None, mode = 'rb'): + self.path = None + self.titleId = None + self.hasValidTicket = None + self.timestamp = None + self.version = None + self.fileSize = None + self.fileModified = None + self.extractedNcaMeta = False + + super(Nsp, self).__init__(None, path, mode) + + if path: + self.setPath(path) + #if files: + # self.pack(files) + + if self.titleId and self.isUnlockable(): + Print.info('unlockable title found ' + self.path) + # self.unlock() + + def getFileSize(self): + if self.fileSize == None: + self.fileSize = os.path.getsize(self.path) + return self.fileSize + + def getFileModified(self): + if self.fileModified == None: + self.fileModified = os.path.getmtime(self.path) + return self.fileModified + + def loadCsv(self, line, map = ['id', 'path', 'version', 'timestamp', 'hasValidTicket', 'extractedNcaMeta']): + split = line.split('|') + for i, value in enumerate(split): + if i >= len(map): + Print.info('invalid map index: ' + str(i) + ', ' + str(len(map))) + continue + + i = str(map[i]) + methodName = 'set' + i[0].capitalize() + i[1:] + method = getattr(self, methodName, lambda x: None) + method(value.strip()) + + def serialize(self, map = ['id', 'path', 'version', 'timestamp', 'hasValidTicket', 'extractedNcaMeta']): + r = [] + for i in map: + + methodName = 'get' + i[0].capitalize() + i[1:] + method = getattr(self, methodName, lambda: methodName) + r.append(str(method())) + return '|'.join(r) + + def __lt__(self, other): + return str(self.path) < str(other.path) + + def __iter__(self): + return self.files.__iter__() + + def title(self): + if not self.titleId: + raise IOError('NSP no titleId set') + + if self.titleId in Titles.keys(): + return Titles.get(self.titleId) + + t = Title.Title() + t.setId(self.titleId) + Titles.data()[self.titleId] = t + return t + + def unpack(self, path, extractregex="*"): + os.makedirs(str(path), exist_ok=True) + + for nspf in self: + filePath_str = str(path.joinpath(nspf._path)) + if not re.match(extractregex, filePath_str): + continue + f = open(filePath_str, 'wb') + nspf.rewind() + i = 0 + + pageSize = 0x100000 + + while True: + buf = nspf.read(pageSize) + if len(buf) == 0: + break + i += len(buf) + f.write(buf) + f.close() + Print.info(filePath_str) + + def setHasValidTicket(self, value): + if hasattr(self.title(), 'isUpdate') and self.title().isUpdate: + self.hasValidTicket = True + return + + try: + self.hasValidTicket = (True if value and int(value) != 0 else False) or self.title().isUpdate + except: + pass + + #extractedNcaMeta + + def getExtractedNcaMeta(self): + if hasattr(self, 'extractedNcaMeta') and self.extractedNcaMeta == True: + return 1 + return 0 + + def setExtractedNcaMeta(self, val): + if val and (val != 0 or val == True): + self.extractedNcaMeta = True + else: + self.extractedNcaMeta = False + + def getHasValidTicket(self): + if self.title().isUpdate: + return 1 + return (1 if self.hasValidTicket and self.hasValidTicket == True else 0) + + def setId(self, id): + if re.match('[A-F0-9]{16}', id, re.I): + self.titleId = id + + def getId(self): + return self.titleId or ('0' * 16) + + def setTimestamp(self, timestamp): + try: + self.timestamp = int(str(timestamp), 10) + except: + pass + + def getTimestamp(self): + return str(self.timestamp or '') + + def setVersion(self, version): + if version and len(version) > 0: + self.version = version + + def getVersion(self): + return self.version or '' + + def setPath(self, path): + self.path = path + self.version = '0' + + z = re.match('.*\[([a-zA-Z0-9]{16})\].*', path, re.I) + if z: + self.titleId = z.groups()[0].upper() + else: + Print.info('could not get title id from filename, name needs to contain [titleId] : ' + path) + self.titleId = None + + z = re.match('.*\[v([0-9]+)\].*', path, re.I) + + if z: + self.version = z.groups()[0] + + if path.endswith('.nsp'): + if self.hasValidTicket is None: + self.setHasValidTicket(True) + elif path.endswith('.nsx'): + if self.hasValidTicket is None: + self.setHasValidTicket(False) + else: + # print('unknown extension ' + str(path)) + return + + def getPath(self): + return self.path or '' + + def open(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Nsp, self).open(path or self.path, mode, cryptoType, cryptoKey, cryptoCounter) + + return True + + def cleanFilename(self, s): + if s is None: + return '' + #s = re.sub('\s+\Demo\s*', ' ', s, re.I) + s = re.sub('\s*\[DLC\]\s*', '', s, re.I) + s = re.sub(r'[\/\\\:\*\?\"\<\>\|\.\s™©®()\~]+', ' ', s) + return s.strip() + + def dict(self): + return {"titleId": self.titleId, "hasValidTicket": self.hasValidTicket, 'extractedNcaMeta': self.getExtractedNcaMeta(), 'version': self.version, 'timestamp': self.timestamp, 'path': self.path } + + def ticket(self): + for f in (f for f in self if type(f) == Ticket): + return f + raise IOError('no ticket in NSP') + + def cnmt(self): + for f in (f for f in self if f._path.endswith('.cnmt.nca')): + return f + raise IOError('no cnmt in NSP') + + def xml(self): + for f in (f for f in self if f._path.endswith('.xml')): + return f + raise IOError('no XML in NSP') + + def hasDeltas(self): + return b'DeltaFragment' in self.xml().read() + + def application(self): + for f in (f for f in self if f._path.endswith('.nca') and not f._path.endswith('.cnmt.nca')): + return f + raise IOError('no application in NSP') + + def isUnlockable(self): + return (not self.hasValidTicket) and self.titleId and Titles.contains(self.titleId) and Titles.get(self.titleId).key + + def unlock(self): + #if not self.isOpen(): + # self.open('r+b') + + if not Titles.contains(self.titleId): + raise IOError('No title key found in database!') + + self.ticket().setTitleKeyBlock(int(Titles.get(self.titleId).key, 16)) + Print.info('setting title key to ' + Titles.get(self.titleId).key) + self.ticket().flush() + self.close() + self.hasValidTicket = True + self.move() + + def setMasterKeyRev(self, newMasterKeyRev): + if not Titles.contains(self.titleId): + raise IOError('No title key found in database! ' + self.titleId) + + ticket = self.ticket() + masterKeyRev = ticket.getMasterKeyRevision() + titleKey = ticket.getTitleKeyBlock() + newTitleKey = Keys.changeTitleKeyMasterKey(titleKey.to_bytes(16, byteorder='big'), Keys.getMasterKeyIndex(masterKeyRev), Keys.getMasterKeyIndex(newMasterKeyRev)) + rightsId = ticket.getRightsId() + + if rightsId != 0: + raise IOError('please remove titlerights first') + + if (newMasterKeyRev == None and rightsId == 0) or masterKeyRev == newMasterKeyRev: + Print.info('Nothing to do') + return + + Print.info('rightsId =\t' + hex(rightsId)) + Print.info('titleKey =\t' + str(hx(titleKey.to_bytes(16, byteorder='big')))) + Print.info('newTitleKey =\t' + str(hx(newTitleKey))) + Print.info('masterKeyRev =\t' + hex(masterKeyRev)) + + + + for nca in self: + if type(nca) == Nca: + if nca.header.getCryptoType2() != masterKeyRev: + pass + raise IOError('Mismatched masterKeyRevs!') + + ticket.setMasterKeyRevision(newMasterKeyRev) + ticket.setRightsId((ticket.getRightsId() & 0xFFFFFFFFFFFFFFFF0000000000000000) + newMasterKeyRev) + ticket.setTitleKeyBlock(int.from_bytes(newTitleKey, 'big')) + + for nca in self: + if type(nca) == Nca: + if nca.header.getCryptoType2() != newMasterKeyRev: + Print.info('writing masterKeyRev for %s, %d -> %s' % (str(nca._path), nca.header.getCryptoType2(), str(newMasterKeyRev))) + + encKeyBlock = nca.header.getKeyBlock() + + if sum(encKeyBlock) != 0: + key = Keys.keyAreaKey(Keys.getMasterKeyIndex(masterKeyRev), nca.header.keyIndex) + Print.info('decrypting with %s (%d, %d)' % (str(hx(key)), Keys.getMasterKeyIndex(masterKeyRev), nca.header.keyIndex)) + crypto = aes128.AESECB(key) + decKeyBlock = crypto.decrypt(encKeyBlock) + + key = Keys.keyAreaKey(Keys.getMasterKeyIndex(newMasterKeyRev), nca.header.keyIndex) + Print.info('encrypting with %s (%d, %d)' % (str(hx(key)), Keys.getMasterKeyIndex(newMasterKeyRev), nca.header.keyIndex)) + crypto = aes128.AESECB(key) + + reEncKeyBlock = crypto.encrypt(decKeyBlock) + nca.header.setKeyBlock(reEncKeyBlock) + + + if newMasterKeyRev >= 3: + nca.header.setCryptoType(2) + nca.header.setCryptoType2(newMasterKeyRev) + else: + nca.header.setCryptoType(newMasterKeyRev) + nca.header.setCryptoType2(0) + + + def removeTitleRights(self): + if not Titles.contains(self.titleId): + raise IOError('No title key found in database! ' + self.titleId) + + ticket = self.ticket() + masterKeyRev = ticket.getMasterKeyRevision() + titleKeyDec = Keys.decryptTitleKey(ticket.getTitleKeyBlock().to_bytes(16, byteorder='big'), Keys.getMasterKeyIndex(masterKeyRev)) + rightsId = ticket.getRightsId() + + Print.info('rightsId =\t' + hex(rightsId)) + Print.info('titleKeyDec =\t' + str(hx(titleKeyDec))) + Print.info('masterKeyRev =\t' + hex(masterKeyRev)) + + + + for nca in self: + if type(nca) == Nca: + if nca.header.getCryptoType2() != masterKeyRev: + pass + raise IOError('Mismatched masterKeyRevs!') + + + ticket.setRightsId(0) + + for nca in self: + if type(nca) == Nca: + if nca.header.getRightsId() == 0: + continue + + kek = Keys.keyAreaKey(Keys.getMasterKeyIndex(masterKeyRev), nca.header.keyIndex) + Print.info('writing masterKeyRev for %s, %d' % (str(nca._path), masterKeyRev)) + Print.info('kek =\t' + hx(kek).decode()) + crypto = aes128.AESECB(kek) + + encKeyBlock = crypto.encrypt(titleKeyDec * 4) + nca.header.setRightsId(0) + nca.header.setKeyBlock(encKeyBlock) + Hex.dump(encKeyBlock) + + def setGameCard(self, isGameCard = False): + if isGameCard: + targetValue = 1 + else: + targetValue = 0 + + for nca in self: + if type(nca) == Nca: + if nca.header.getIsGameCard() == targetValue: + continue + + Print.info('writing isGameCard for %s, %d' % (str(nca._path), targetValue)) + nca.header.setIsGameCard(targetValue) + + + def pack(self, files): + if not self.path: + return False + + Print.info('\tRepacking to NSP...') + + hd = self.generateHeader(files) + + totalSize = len(hd) + sum(os.path.getsize(file) for file in files) + if os.path.exists(self.path) and os.path.getsize(self.path) == totalSize: + Print.info('\t\tRepack %s is already complete!' % self.path) + return + + t = enlighten.Counter(total=totalSize, unit='B', desc=os.path.basename(self.path), leave=False) + + Print.info('\t\tWriting header...') + outf = open(self.path, 'wb') + outf.write(hd) + t.update(len(hd)) + + done = 0 + for f_str in files: + for filePath in expandFiles(Path(f_str)): + Print.info('\t\tAppending %s...' % os.path.basename(filePath)) + with open(filePath, 'rb') as inf: + while True: + buf = inf.read(4096) + if not buf: + break + outf.write(buf) + t.update(len(buf)) + t.close() + + Print.info('\t\tRepacked to %s!' % outf.name) + outf.close() + + def generateHeader(self, files): + filesNb = len(files) + stringTable = '\x00'.join(os.path.basename(file) for file in files) + headerSize = 0x10 + (filesNb)*0x18 + len(stringTable) + + fileSizes = [os.path.getsize(file) for file in files] + fileOffsets = [sum(fileSizes[:n]) for n in range(filesNb)] + + fileNamesLengths = [len(os.path.basename(file))+1 for file in files] # +1 for the \x00 + stringTableOffsets = [sum(fileNamesLengths[:n]) for n in range(filesNb)] + + header = b'' + header += b'PFS0' + header += pk(' self.actualSize: + self.actualSize = self.tell() + + def add(self, name, size, pleaseNoPrint = None): + Print.info('[ADDING] {0} {1} bytes to NSP'.format(name, size), pleaseNoPrint) + self.files.append({'name': name, 'size': size, 'offset': self.f.tell()}) + return self.partition(self.f.tell(), size, n = BaseFile()) + + def get(self, name): + for i in self.files: + if i['name'] == name: + return i + return None + + def resize(self, name, size): + for i in self.files: + if i['name'] == name: + i['size'] = size + return True + return False + + def close(self): + if self.isOpen(): + self.seek(0) + self.write(self.getHeader()) + super(Pfs0Stream, self).close() + + def getHeaderSize(self): + stringTable = '\x00'.join(file['name'] for file in self.files) + headerSize = 0x10 + len(self.files) * 0x18 + self.stringTableSize + return headerSize + + def getStringTableSize(self): + stringTable = '\x00'.join(file['name'] for file in self.files) + stringTableLen = len(stringTable) + if self._stringTableSize == None: + self._stringTableSize = stringTableLen + if stringTableLen > self._stringTableSize: + self._stringTableSize = stringTableLen + return self._stringTableSize + + def getFirstFileOffset(self): + return self.files[0].offset + + def getHeader(self): + stringTable = '\x00'.join(file['name'] for file in self.files) + headerSize = 0x10 + len(self.files) * 0x18 + self.getStringTableSize() + + h = b'' + h += b'PFS0' + h += len(self.files).to_bytes(4, byteorder='little') + h += (self.getStringTableSize()).to_bytes(4, byteorder='little') + h += b'\x00\x00\x00\x00' + + stringOffset = 0 + + for f in self.files: + h += (f['offset'] - headerSize).to_bytes(8, byteorder='little') + h += f['size'].to_bytes(8, byteorder='little') + h += stringOffset.to_bytes(4, byteorder='little') + h += b'\x00\x00\x00\x00' + + stringOffset += len(f['name']) + 1 + + h += stringTable.encode() + + return h + + +class Pfs0VerifyStream(): + def __init__(self, headerSize, stringTableSize, mode = 'wb'): + self.files = [] + self.binhash = sha256() + self.pos = headerSize + self._stringTableSize = stringTableSize + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + pass + + def write(self, value, size = None): + self.binhash.update(value) + self.pos += len(value) + + def tell(self): + return self.pos + + def add(self, name, size, pleaseNoPrint = None): + Print.info('[ADDING] {0} {1} bytes to NSP'.format(name, size), pleaseNoPrint) + self.files.append({'name': name, 'size': size, 'offset': self.pos}) + return self + + def resize(self, name, size): + for i in self.files: + if i['name'] == name: + i['size'] = size + return True + return False + + def close(self): + pass + + def getStringTableSize(self): + stringTable = '\x00'.join(file['name'] for file in self.files) + stringTableLen = len(stringTable) + if self._stringTableSize == None: + self._stringTableSize = stringTableLen + if stringTableLen > self._stringTableSize: + self._stringTableSize = stringTableLen + return self._stringTableSize + + def getHash(self): + hexHash = self.binhash.hexdigest() + return hexHash + + def getHeaderHash(self): + stringTable = '\x00'.join(file['name'] for file in self.files) + headerSize = 0x10 + len(self.files) * 0x18 + self.getStringTableSize() + + h = b'' + h += b'PFS0' + h += len(self.files).to_bytes(4, byteorder='little') + h += (self.getStringTableSize()).to_bytes(4, byteorder='little') + h += b'\x00\x00\x00\x00' + + stringOffset = 0 + + for f in self.files: + h += (f['offset'] - headerSize).to_bytes(8, byteorder='little') + h += f['size'].to_bytes(8, byteorder='little') + h += stringOffset.to_bytes(4, byteorder='little') + h += b'\x00\x00\x00\x00' + + stringOffset += len(f['name']) + 1 + + h += stringTable.encode() + + headerBinhash = sha256() + headerBinhash.update(h) + headerHexHash = headerBinhash.hexdigest() + return headerHexHash + + +class Pfs0(BaseFs): + def __init__(self, buffer, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Pfs0, self).__init__(buffer, path, mode, cryptoType, cryptoKey, cryptoCounter) + + if buffer: + self.size = int.from_bytes(buffer[0x48:0x50], byteorder='little', signed=False) + self.sectionStart = int.from_bytes(buffer[0x40:0x48], byteorder='little', signed=False) + #self.offset += sectionStart + #self.size -= sectionStart + + def getHeaderSize(self): + return self._headerSize; + + def getStringTableSize(self): + return self._stringTableSize; + + def getFirstFileOffset(self): + return self.files[0].offset + + def open(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + r = super(Pfs0, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + #self.setupCrypto() + #Print.info('cryptoType = ' + hex(self.cryptoType)) + #Print.info('titleKey = ' + (self.cryptoKey.hex())) + #Print.info('cryptoCounter = ' + (self.cryptoCounter.hex())) + + self.magic = self.read(4) + if self.magic != b'PFS0': + raise IOError('Not a valid PFS0 partition ' + str(self.magic)) + + + fileCount = self.readInt32() + self._stringTableSize = self.readInt32() + self.readInt32() # junk data + + self.seek(0x10 + fileCount * 0x18) + stringTable = self.read(self._stringTableSize) + stringEndOffset = self._stringTableSize + + self._headerSize = 0x10 + 0x18 * fileCount + self._stringTableSize + self.files = [] + + for i in range(fileCount): + i = fileCount - i - 1 + self.seek(0x10 + i * 0x18) + + offset = self.readInt64() + size = self.readInt64() + nameOffset = self.readInt32() # just the offset + name = stringTable[nameOffset:stringEndOffset].decode('utf-8').rstrip(' \t\r\n\0') + stringEndOffset = nameOffset + + self.readInt32() # junk data + + f = Fs.factory(Path(name)) + + f._path = name + f.offset = offset + f.size = size + + self.files.append(self.partition(offset + self._headerSize, f.size, f, autoOpen = False)) + + ticket = None + + + try: + ticket = self.ticket() + ticket.open(None, None) + #key = format(ticket.getTitleKeyBlock(), 'X').zfill(32) + + if ticket.titleKey() != ('0' * 32): + Titles.get(ticket.titleId()).key = ticket.titleKey() + except: + pass + + for i in range(fileCount): + if self.files[i] != ticket: + try: + self.files[i].open(None, None) + except: + pass + + self.files.reverse() + + + def getCnmt(self): + return super(Pfs0, self).getCnmt() + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('\n%sPFS0\n' % (tabs)) + super(Pfs0, self).printInfo(maxDepth, indent) diff --git a/py-test/Fs/Rom.py b/py-test/Fs/Rom.py new file mode 100644 index 00000000..70bb8086 --- /dev/null +++ b/py-test/Fs/Rom.py @@ -0,0 +1,53 @@ +from binascii import hexlify as hx, unhexlify as uhx +from struct import pack as pk, unpack as upk +from Fs.File import File +from Fs.File import MemoryFile +import os +import re +import pathlib +from nut import Keys +from nut import Print +from Fs.BaseFs import BaseFs +from Fs.Ivfc import Ivfc +from nut import Hex + +MEDIA_SIZE = 0x200 + +class Rom(BaseFs): + def __init__(self, buffer, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Rom, self).__init__(buffer, path, mode, cryptoType, cryptoKey, cryptoCounter) + if buffer: + self.ivfc = Ivfc(MemoryFile(buffer[0x8:]), 'rb') + self.magic = buffer[0x8:0xC] + + #Hex.dump(buffer) + #self.sectionStart = self.ivfc.levels[5].offset + else: + self.ivfc = None + + def open(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + r = super(Rom, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter) + + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('\n%sRom' % (tabs)) + if self.ivfc: + Print.info('%sMagic = %s' % (tabs, self.ivfc.magic)) + Print.info('%sLevels = %d' % (tabs, self.ivfc.numberLevels)) + Print.info('%sHash = %s' % (tabs, hx(self.ivfc.hash).decode())) + if self.ivfc.numberLevels < 16: + for i,level in enumerate(self.ivfc.levels): + Print.info('%sLevel%d offset = %d' % (tabs, i, level.offset)) + Print.info('%sLevel%d size = %d' % (tabs, i, level.size)) + Print.info('%sLevel%d blockSize = %d' % (tabs, i, level.blockSize)) + + ''' + self.seek(0) + level1 = self.read(0x4000) + Print.info('%ssha = %s' % (tabs, sha256(level1).hexdigest())) + Hex.dump(level1) + ''' + super(Rom, self).printInfo(maxDepth, indent) + + diff --git a/py-test/Fs/Ticket.py b/py-test/Fs/Ticket.py new file mode 100644 index 00000000..d8624d02 --- /dev/null +++ b/py-test/Fs/Ticket.py @@ -0,0 +1,224 @@ +from Fs.File import File +import Fs +from binascii import hexlify as hx, unhexlify as uhx +from nut import Print +from nut import Keys + + +class Ticket(File): + def __init__(self, path = None, mode = None, cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Ticket, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter) + + self.signatureType = None + self.signature = None + self.signaturePadding = None + + self.issuer = None + self.titleKeyBlock = None + self.keyType = None + self.ticketId = None + self.deviceId = None + self.rightsId = None + self.accountId = None + + self.signatureSizes = {} + self.signatureSizes[Fs.Type.TicketSignature.RSA_4096_SHA1] = 0x200 + self.signatureSizes[Fs.Type.TicketSignature.RSA_2048_SHA1] = 0x100 + self.signatureSizes[Fs.Type.TicketSignature.ECDSA_SHA1] = 0x3C + self.signatureSizes[Fs.Type.TicketSignature.RSA_4096_SHA256] = 0x200 + self.signatureSizes[Fs.Type.TicketSignature.RSA_2048_SHA256] = 0x100 + self.signatureSizes[Fs.Type.TicketSignature.ECDSA_SHA256] = 0x3C + + def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(Ticket, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + self.signatureType = self.readInt32() + try: + self.signatureType = Fs.Type.TicketSignature(self.signatureType) + except: + raise IOError('Invalid ticket format') + + self.signaturePadding = 0x40 - ((self.signatureSizes[self.signatureType] + 4) % 0x40) + + self.seek(0x4 + self.signatureSizes[self.signatureType] + self.signaturePadding) + + self.issuer = self.read(0x40) + self.titleKeyBlock = self.read(0x100) + self.readInt8() # unknown + self.keyType = self.readInt8() + self.read(0xE) # unknown + self.ticketId = hx(self.read(0x8)).decode('utf-8') + self.deviceId = hx(self.read(0x8)).decode('utf-8') + self.rightsId = hx(self.read(0x10)).decode('utf-8') + self.accountId = hx(self.read(0x4)).decode('utf-8') + + def seekStart(self, offset): + self.seek(0x4 + self.signatureSizes[self.signatureType] + self.signaturePadding + offset) + + def getSignatureType(self): + self.seek(0x0) + self.signatureType = self.readInt32() + return self.signatureType + + def setSignatureType(self, value): + self.seek(0x0) + self.signatureType = value + self.writeInt32(value) + return self.signatureType + + + def getSignature(self): + self.seek(0x4) + self.signature = self.read(self.signatureSizes[self.getSignatureType()]) + return self.signature + + def setSignature(self, value): + self.seek(0x4) + self.signature = value + self.write(value, self.signatureSizes[self.getSignatureType()]) + return self.signature + + + def getSignaturePadding(self): + self.signaturePadding = 0x40 - ((self.signatureSizes[self.signatureType] + 4) % 0x40) + return self.signaturePadding + + + def getIssuer(self): + self.seekStart(0x0) + self.issuer = self.read(0x40) + return self.issuer + + def setIssuer(self, value): + self.seekStart(0x0) + self.issuer = value + self.write(value, 0x40) + return self.issuer + + + def getTitleKeyBlock(self): + self.seekStart(0x40) + #self.titleKeyBlock = self.readInt(0x100, 'big') + self.titleKeyBlock = self.readInt(0x10, 'big') + return self.titleKeyBlock + + def getTitleKey(self): + self.seekStart(0x40) + return self.read(0x10) + + def setTitleKeyBlock(self, value): + self.seekStart(0x40) + self.titleKeyBlock = value + #self.writeInt(value, 0x100, 'big') + self.writeInt(value, 0x10, 'big') + return self.titleKeyBlock + + + def getKeyType(self): + self.seekStart(0x141) + self.keyType = self.readInt8() + return self.keyType + + def setKeyType(self, value): + self.seekStart(0x141) + self.keyType = value + self.writeInt8(value) + return self.keyType + + + def getMasterKeyRevision(self): + self.seekStart(0x145) + rev = self.readInt8() + if rev == 0: + rev = self.readInt8() + return rev + + def setMasterKeyRevision(self, value): + self.seekStart(0x145) + self.writeInt8(value) + return value + + + def getTicketId(self): + self.seekStart(0x150) + self.ticketId = self.readInt64('big') + return self.ticketId + + def setTicketId(self, value): + self.seekStart(0x150) + self.ticketId = value + self.writeInt64(value, 'big') + return self.ticketId + + + def getDeviceId(self): + self.seekStart(0x158) + self.deviceId = self.readInt64('big') + return self.deviceId + + def setDeviceId(self, value): + self.seekStart(0x158) + self.deviceId = value + self.writeInt64(value, 'big') + return self.deviceId + + + def getRightsId(self): + self.seekStart(0x160) + self.rightsId = self.readInt128('big') + return self.rightsId + + def setRightsId(self, value): + self.seekStart(0x160) + self.rightsId = value + self.writeInt128(value, 'big') + return self.rightsId + + + def getAccountId(self): + self.seekStart(0x170) + self.accountId = self.readInt32('big') + return self.accountId + + def setAccountId(self, value): + self.seekStart(0x170) + self.accountId = value + self.writeInt32(value, 'big') + return self.accountId + + def titleId(self): + rightsId = format(self.getRightsId(), 'X').zfill(32) + return rightsId[0:16] + + def titleKey(self): + return format(self.getTitleKeyBlock(), 'X').zfill(32) + + + + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + + rightsId = format(self.getRightsId(), 'X').zfill(32) + titleId = rightsId[0:16] + titleKey = format(self.getTitleKeyBlock(), 'X').zfill(32) + masterKeyRevision = self.getMasterKeyRevision() + + Print.info('\n%sTicket\n' % (tabs)) + super(Ticket, self).printInfo(maxDepth, indent) + Print.info(tabs + 'signatureType = ' + str(self.signatureType)) + Print.info(tabs + 'keyType = ' + str(self.keyType)) + Print.info(tabs + 'masterKeyRev = ' + str(masterKeyRevision) + " (master_key_{0:02x})".format(masterKeyRevision - 1)) + Print.info(tabs + 'ticketId = ' + str(self.ticketId)) + Print.info(tabs + 'deviceId = ' + str(self.deviceId)) + Print.info(tabs + 'rightsId = ' + rightsId) + Print.info(tabs + 'accountId = ' + str(self.accountId)) + Print.info(tabs + 'titleId = ' + titleId) + Print.info(tabs + 'titleKey = ' + titleKey) + try: + Print.info(tabs + 'titleKeyDec = ' + str(hx(Keys.decryptTitleKey((self.getTitleKey()), masterKeyRevision - 1)))) + except: + Print.info(tabs + 'titleKeyDec = An error occurred while obtaining titleKeyDec') + + + diff --git a/py-test/Fs/Type.py b/py-test/Fs/Type.py new file mode 100644 index 00000000..e1fff99c --- /dev/null +++ b/py-test/Fs/Type.py @@ -0,0 +1,30 @@ +from enum import IntEnum + +class Content(IntEnum): + PROGRAM = 0x0 + META = 0x1 + CONTROL = 0x2 + MANUAL = 0x3 # HtmlDocument, LegalInformation + DATA = 0x4 # DeltaFragment + PUBLICDATA = 0x5 + +class Fs(IntEnum): + NONE = 0x0 + PFS0 = 0x2 + ROMFS = 0x3 + +class Crypto(IntEnum): + ERR = 0 + NONE = 1 + XTS = 2 + CTR = 3 + BKTR = 4 + NCA0 = 0x3041434E + +class TicketSignature(IntEnum): + RSA_4096_SHA1 = 0x010000 + RSA_2048_SHA1 = 0x010001 + ECDSA_SHA1 = 0x010002 + RSA_4096_SHA256 = 0x010003 + RSA_2048_SHA256 = 0x010004 + ECDSA_SHA256 = 0x010005 diff --git a/py-test/Fs/Xci.py b/py-test/Fs/Xci.py new file mode 100644 index 00000000..2503fd80 --- /dev/null +++ b/py-test/Fs/Xci.py @@ -0,0 +1,322 @@ +from binascii import hexlify as hx, unhexlify as uhx +from Fs.File import File +from Fs.File import BaseFile +from Fs.Hfs0 import Hfs0 +from Fs.Hfs0 import Hfs0Stream +import os +import re +from nut import Print + + +MEDIA_SIZE = 0x200 + +class XciStream(BaseFile): + def __init__(self, path = None, mode = 'wb', originalXciPath = None): + os.makedirs(os.path.dirname(path), exist_ok = True) + super(XciStream, self).__init__(path, mode) + self.path = path + self.f = open(path, 'wb+') + self.start = 0 + + self.files = [] + + self.signature = b'\x00' * 0x100 + self.magic = b'\x00' * 4 + self.secureOffset = 0 + self.backupOffset = 0 + self.titleKekIndex = 0 + self.gamecardSize = 0 + self.gamecardHeaderVersion = 0 + self.gamecardFlags = 0 + self.packageId = 0 + self.validDataEndOffset = 0 + self.gamecardInfo = b'\x00' * 0x10 + + self.hfs0Offset = 0 + self.hfs0HeaderSize = 0 + self.hfs0HeaderHash = b'\x00' * 0x20 + self.hfs0InitialDataHash = b'\x00' * 0x20 + self.secureMode = 0 + + self.titleKeyFlag = 0 + self.keyFlag = 0 + self.normalAreaEndOffset = 0 + + #self.gamecardInfo = GamecardInfo(self.partition(self.tell(), 0x70)) + #self.gamecardCert = GamecardCertificate(self.partition(0x7000, 0x200)) + + with open(originalXciPath, 'rb') as xf: + self.headerBuffer = xf.read(0x200) # gross hack just to get this working + + self.f.seek(0xF000) + self.hfs0 = Hfs0Stream(self.partition(0xF000, n = BaseFile())) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def add(self, name, size, pleaseNoPrint = None): + Print.info('[ADDING] {0} {1} bytes to NSP'.format(name, size), pleaseNoPrint) + self.files.append({'name': name, 'size': size, 'offset': self.f.tell()}) + t = {'name': name, 'size': size, 'offset': self.f.tell()} + return self.f + + def currentFileSize(self): + return self.f.tell() - self.files[-1]['offset'] + + def get(self, name): + for i in self.files: + if i['name'] == name: + return i + return None + + def resize(self, name, size): + for i in self.files: + if i['name'] == name: + i['size'] = size + return True + return False + + def close(self): + if self.isOpen(): + if self.hfs0: + hfs0Size = self.hfs0.actualSize + self.hfs0.close() + self.hfs0 = None + else: + hfs0Size = 0 + + self.seek(0) + self.writeHeader() + + super(XciStream, self).close() + + def write(self, value, size = None): + if size != None: + value = value + '\0x00' * (size - len(value)) + return self.f.write(value) + + def writeInt8(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(1, byteorder)) + + def writeInt16(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(2, byteorder)) + + def writeInt32(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(4, byteorder)) + + def writeInt64(self, value, byteorder='little', signed = False): + return self.write(value.to_bytes(8, byteorder)) + + def writeHeader(self): + self.write(self.headerBuffer) # gross hack to get this working + return + self.write(self.signature) + self.write(self.magic) + self.writeInt32(self.secureOffset) + self.writeInt32(self.backupOffset) + self.writeInt8(self.titleKekIndex) + self.writeInt8(self.gamecardSize) + self.writeInt8(self.gamecardHeaderVersion) + self.writeInt8(self.gamecardFlags) + self.writeInt64(self.packageId) + self.writeInt64(self.validDataEndOffset) + self.write(self.gamecardInfo) + + self.writeInt64(self.hfs0Offset) + self.writeInt64(self.hfs0HeaderSize) + self.write(self.hfs0HeaderHash) + self.write(self.hfs0InitialDataHash) + self.writeInt32(self.secureMode) + + self.writeInt32(self.titleKeyFlag) + self.writeInt32(self.keyFlag) + self.writeInt32(self.normalAreaEndOffset) + + #self.gamecardInfo = GamecardInfo(self.partition(self.tell(), 0x70)) + #self.gamecardCert = GamecardCertificate(self.partition(0x7000, 0x200)) + + +class GamecardInfo(File): + def __init__(self, file = None): + super(GamecardInfo, self).__init__() + + self.firmwareVersion = 0 + self.accessControlFlags = 0 + self.readWaitTime = 0 + self.readWaitTime2 = 0 + self.writeWaitTime = 0 + self.writeWaitTime2 = 0 + self.firmwareMode = 0 + self.cupVersion = 0 + self.empty1 = 0 + self.updatePartitionHash = 0 + self.cupId = 0 + self.empty2 = b'\x00' * 0x38 + + if file: + self.open(file) + + def open(self, file, mode='rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(GamecardInfo, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + self.firmwareVersion = self.readInt64() + self.accessControlFlags = self.readInt32() + self.readWaitTime = self.readInt32() + self.readWaitTime2 = self.readInt32() + self.writeWaitTime = self.readInt32() + self.writeWaitTime2 = self.readInt32() + self.firmwareMode = self.readInt32() + self.cupVersion = self.readInt32() + self.empty1 = self.readInt32() + self.updatePartitionHash = self.readInt64() + self.cupId = self.readInt64() + self.empty2 = self.read(0x38) + + def write(self): + self.rewind() + self.writeInt64(self.firmwareVersion) + self.writeInt32(self.accessControlFlags) + self.writeInt32(self.readWaitTime) + self.writeInt32(self.readWaitTime2) + self.writeInt32(self.writeWaitTime) + self.writeInt32(self.writeWaitTime2) + self.writeInt32(self.firmwareMode) + self.writeInt32(self.cupVersion) + self.writeInt32(self.empty1) + self.writeInt64(self.updatePartitionHash) + self.writeInt64(self.cupId) + self.read(self.empty2) + +class GamecardCertificate(File): + def __init__(self, file = None): + super(GamecardCertificate, self).__init__() + self.signature = b'\x00' * 0x100 + self.magic = b'\x00' * 0x4 + self.unknown1 = b'\x00' * 0x10 + self.unknown2 = b'\x00' * 0xA + self.data = b'\x00' * 0xD6 + + if file: + self.open(file) + + def open(self, file, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + super(GamecardCertificate, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter) + self.rewind() + self.signature = self.read(0x100) + self.magic = self.read(0x4) + self.unknown1 = self.read(0x10) + self.unknown2 = self.read(0xA) + self.data = self.read(0xD6) + + def write(self): + self.write(self.signature) + self.write(self.magic) + self.write(self.unknown1) + self.write(self.unknown2) + self.write(self.data) + +class Xci(File): + def __init__(self, file = None): + super(Xci, self).__init__() + self.header = None + self.signature = None + self.magic = None + self.secureOffset = None + self.backupOffset = None + self.titleKekIndex = None + self.gamecardSize = None + self.gamecardHeaderVersion = None + self.gamecardFlags = None + self.packageId = None + self.validDataEndOffset = None + self.gamecardInfo = None + + self.hfs0Offset = None + self.hfs0HeaderSize = None + self.hfs0HeaderHash = None + self.hfs0InitialDataHash = None + self.secureMode = None + + self.titleKeyFlag = None + self.keyFlag = None + self.normalAreaEndOffset = None + + self.gamecardInfo = None + self.gamecardCert = None + self.hfs0 = None + + if file: + self.open(file) + + def readHeader(self): + + self.signature = self.read(0x100) + self.magic = self.read(0x4) + self.secureOffset = self.readInt32() + self.backupOffset = self.readInt32() + self.titleKekIndex = self.readInt8() + self.gamecardSize = self.readInt8() + self.gamecardHeaderVersion = self.readInt8() + self.gamecardFlags = self.readInt8() + self.packageId = self.readInt64() + self.validDataEndOffset = self.readInt64() + self.gamecardInfo = self.read(0x10) + + self.hfs0Offset = self.readInt64() + self.hfs0HeaderSize = self.readInt64() + self.hfs0HeaderHash = self.read(0x20) + self.hfs0InitialDataHash = self.read(0x20) + self.secureMode = self.readInt32() + + self.titleKeyFlag = self.readInt32() + self.keyFlag = self.readInt32() + self.normalAreaEndOffset = self.readInt32() + + self.gamecardInfo = GamecardInfo(self.partition(self.tell(), 0x70)) + self.gamecardCert = GamecardCertificate(self.partition(0x7000, 0x200)) + + + def open(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + r = super(Xci, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter) + self.readHeader() + self.seek(0xF000) + self.hfs0 = Hfs0(None, cryptoKey = None) + self.partition(0xf000, None, self.hfs0, cryptoKey = None) + + def unpack(self, path, extractregex="*"): + os.makedirs(str(path), exist_ok=True) + + for nspF in self.hfs0: + filePath_str = str(path.joinpath(nspF._path)) + if not re.match(extractregex, filePath_str): + continue + f = open(filePath_str, 'wb') + nspF.rewind() + i = 0 + + pageSize = 0x10000 + + while True: + buf = nspF.read(pageSize) + if len(buf) == 0: + break + i += len(buf) + f.write(buf) + f.close() + Print.info(filePath_str) + + def printInfo(self, maxDepth = 3, indent = 0): + tabs = '\t' * indent + Print.info('\n%sXCI Archive\n' % (tabs)) + super(Xci, self).printInfo(maxDepth, indent) + + Print.info(tabs + 'magic = ' + str(self.magic)) + Print.info(tabs + 'titleKekIndex = ' + str(self.titleKekIndex)) + + Print.info(tabs + 'gamecardCert = ' + str(hx(self.gamecardCert.magic + self.gamecardCert.unknown1 + self.gamecardCert.unknown2 + self.gamecardCert.data))) + + self.hfs0.printInfo(maxDepth, indent) + diff --git a/py-test/Fs/__init__.py b/py-test/Fs/__init__.py new file mode 100644 index 00000000..5033aef1 --- /dev/null +++ b/py-test/Fs/__init__.py @@ -0,0 +1,37 @@ +import Fs.Nsp +import Fs.Xci +import Fs.Nca +import Fs.Nacp +import Fs.Ticket +import Fs.Cnmt +import Fs.File + +def factory(name): + if name.suffix == '.xci': + f = Fs.Xci.Xci() + elif name.suffix == '.xcz': + f = Fs.Xci.Xci() + elif name.suffix == '.nsp': + f = Fs.Nsp.Nsp() + elif name.suffix == '.nsz': + f = Fs.Nsp.Nsp() + elif name.suffix == '.nspz': + f = Fs.Nsp.Nsp() + elif name.suffix == '.nsx': + f = Fs.Nsp.Nsp() + elif name.suffix == '.nca': + f = Fs.Nca.Nca() + elif name.suffix == '.ncz': + f = Fs.File.File() + elif name.suffix == '.nacp': + f = Fs.Nacp.Nacp() + elif name.suffix == '.tik': + f = Fs.Ticket.Ticket() + elif name.suffix == '.cnmt': + f = Fs.Cnmt.Cnmt() + elif str(name) in set(['normal', 'logo', 'update', 'secure']): + f = Fs.Hfs0.Hfs0(None) + else: + f = Fs.File.File() + + return f \ No newline at end of file diff --git a/py-test/lib/BlockDecompressorReader.py b/py-test/lib/BlockDecompressorReader.py new file mode 100644 index 00000000..3265e29a --- /dev/null +++ b/py-test/lib/BlockDecompressorReader.py @@ -0,0 +1,65 @@ +from zstandard import ZstdDecompressor + +class BlockDecompressorReader: + #Position in decompressed data + Position = 0 + BlockHeader = None + CurrentBlock = b"" + CurrentBlockId = -1 + + def __init__(self, nspf, BlockHeader): + self.BlockHeader = BlockHeader + initialOffset = nspf.tell() + self.nspf = nspf + if BlockHeader.blockSizeExponent < 14 or BlockHeader.blockSizeExponent > 32: + raise ValueError("Corrupted NCZBLOCK header: Block size must be between 14 and 32") + self.BlockSize = 2**BlockHeader.blockSizeExponent + self.CompressedBlockOffsetList = [initialOffset] + + for compressedBlockSize in BlockHeader.compressedBlockSizeList: + self.CompressedBlockOffsetList.append(self.CompressedBlockOffsetList[-1] + compressedBlockSize) + + self.CompressedBlockSizeList = BlockHeader.compressedBlockSizeList + + def __decompressBlock(self, blockID): + if self.CurrentBlockId == blockID: + return self.CurrentBlock + decompressedBlockSize = self.BlockSize + if blockID >= len(self.CompressedBlockOffsetList) - 1: + if blockID >= len(self.CompressedBlockOffsetList): + raise EOFError("BlockID exceeds the amounts of compressed blocks in that file!") + decompressedBlockSize = self.BlockHeader.decompressedSize % BlockSize + self.nspf.seek(self.CompressedBlockOffsetList[blockID]) + if self.CompressedBlockSizeList[blockID] < decompressedBlockSize: + self.CurrentBlock = ZstdDecompressor().decompress(self.nspf.read(decompressedBlockSize)) + else: + self.CurrentBlock = self.nspf.read(decompressedBlockSize) + self.CurrentBlockId = blockID + return self.CurrentBlock + + def seek(self, offset, whence = 0): + if whence == 0: + self.Position = offset + elif whence == 1: + self.Position += offset + elif whence == 2: + self.Position = self.BlockHeader.decompressedSize + offset + else: + raise ValueError("whence argument must be 0, 1 or 2") + + def read(self, length): + buffer = b"" + blockOffset = self.Position%self.BlockSize + blockID = self.Position//self.BlockSize + + while(len(buffer) - blockOffset < length): + if blockID >= len(self.CompressedBlockOffsetList): + break + + buffer += self.__decompressBlock(blockID) + blockID += 1 + + buffer = buffer[blockOffset:blockOffset+length] + self.Position += length + + return buffer diff --git a/py-test/lib/Header.py b/py-test/lib/Header.py new file mode 100644 index 00000000..26c1b22b --- /dev/null +++ b/py-test/lib/Header.py @@ -0,0 +1,27 @@ +class Section: + def __init__(self, f): + self.f = f + self.offset = f.readInt64() + self.size = f.readInt64() + self.cryptoType = f.readInt64() + f.readInt64() # padding + self.cryptoKey = f.read(16) + self.cryptoCounter = f.read(16) + +class FakeSection: + def __init__(self, offset, size): + self.offset = offset + self.size = size + self.cryptoType = 1 + +class Block: + def __init__(self, f): + self.f = f + self.magic = f.read(8) + self.version = f.readInt8() + self.type = f.readInt8() + self.unused = f.readInt8() + self.blockSizeExponent = f.readInt8() + self.numberOfBlocks = f.readInt32() + self.decompressedSize = f.readInt64() + self.compressedBlockSizeList = [f.readInt32() for _ in range(self.numberOfBlocks)] \ No newline at end of file diff --git a/py-test/lib/PathTools.py b/py-test/lib/PathTools.py new file mode 100644 index 00000000..3bf905d9 --- /dev/null +++ b/py-test/lib/PathTools.py @@ -0,0 +1,50 @@ +from pathlib import Path +from os import listdir + +def expandFiles(path): + files = [] + path = path.resolve() + + if path.is_file(): + files.append(path) + else: + for f_str in listdir(path): + f = Path(f_str) + f = path.joinpath(f) + files.append(f) + return files + + +def isGame(filePath): + return filePath.suffix == '.nsp' or filePath.suffix == '.xci' or filePath.suffix == '.nsz' or filePath.suffix == '.xcz' + +def isUncompressedGame(filePath): + return filePath.suffix == '.nsp' or filePath.suffix == '.xci' + +def isCompressedGame(filePath): + return filePath.suffix == '.nsz' or filePath.suffix == '.xcz' + +def isCompressedGameFile(filePath): + return filePath.suffix == '.ncz' + +def isNspNsz(filePath): + return filePath.suffix == '.nsp' or filePath.suffix == '.nsz' + +def isXciXcz(filePath): + return filePath.suffix == '.xci' or filePath.suffix == '.xcz' + +def changeExtension(filePath, newExtension): + return str(filePath.parent.resolve().joinpath(filePath.stem + newExtension)) + +def targetExtension(filePath): + if filePath.suffix == '.nsp': newExtension = '.nsz' + if filePath.suffix == '.xci': newExtension = '.xcz' + if filePath.suffix == '.nca': newExtension = '.ncz' + if filePath.suffix == '.nsz': newExtension = '.nsp' + if filePath.suffix == '.xcz': newExtension = '.xci' + if filePath.suffix == '.ncz': newExtension = '.nca' + return str(filePath.parent.resolve().joinpath(filePath.stem + newExtension)) + +def getExtensionName(filePath): + return str(Path(filePath).suffix[1:].upper()) + diff --git a/py-test/nut/Hex.py b/py-test/nut/Hex.py new file mode 100644 index 00000000..798771f7 --- /dev/null +++ b/py-test/nut/Hex.py @@ -0,0 +1,40 @@ +from string import ascii_letters, digits, punctuation +from nut import Print + +def bufferToHex(buffer, start, count): + accumulator = '' + for item in range(count): + accumulator += '%02X' % buffer[start + item] + ' ' + return accumulator + +def bufferToAscii(buffer, start, count): + accumulator = '' + for item in range(count): + char = chr(buffer[start + item]) + if char in ascii_letters or \ + char in digits or \ + char in punctuation or \ + char == ' ': + accumulator += char + else: + accumulator += '.' + return accumulator + +def dump(data, size = 16): + bytesRead = len(data) + index = 0 + hexFormat = '{:'+str(size*3)+'}' + asciiFormat = '{:'+str(size)+'}' + + print() + while index < bytesRead: + + hex = bufferToHex(data, index, size) + ascii = bufferToAscii(data, index, size) + + print(hexFormat.format(hex), end='') + print('|',asciiFormat.format(ascii),'|') + + index += size + if bytesRead - index < size: + size = bytesRead - index \ No newline at end of file diff --git a/py-test/nut/Keys.py b/py-test/nut/Keys.py new file mode 100644 index 00000000..057c456b --- /dev/null +++ b/py-test/nut/Keys.py @@ -0,0 +1,176 @@ +import os, sys, re +from traceback import format_exc +from nut import aes128 +from binascii import crc32, hexlify as hx, unhexlify as uhx +from nut import Print +from pathlib import Path +# from multiprocessing import * +from multiprocessing.process import current_process + +keys = {} +titleKeks = [] +keyAreaKeys = [] +loadedKeysFile = "non-existing prod.keys/keys.txt" + +#This are NOT the keys but only a 4 bytes long checksum! +#See https://en.wikipedia.org/wiki/Cyclic_redundancy_check +#An infinite amount of inputs leads to the same CRC32 checksum +#crc32(aes_key_generation_source) = 459881589 but +#crc32(TopSecretsEtM) = 459881589 too => No keys where shared! +#Use https://github.com/bediger4000/crc32-file-collision-generator +#to generate your own CRC32 collisions if you don't believe my proof. +crc32_checksum = { + 'aes_kek_generation_source': 2545229389, + 'aes_key_generation_source': 459881589, + 'titlekek_source': 3510501772, + 'key_area_key_application_source': 4130296074, + 'key_area_key_ocean_source': 3975316347, + 'key_area_key_system_source': 4024798875, + 'master_key_00': 3540309694, + 'master_key_01': 3477638116, + 'master_key_02': 2087460235, + 'master_key_03': 4095912905, + 'master_key_04': 3833085536, + 'master_key_05': 2078263136, + 'master_key_06': 2812171174, + 'master_key_07': 1146095808, + 'master_key_08': 1605958034, + 'master_key_09': 3456782962, + 'master_key_0a': 2012895168, + 'master_key_0b': 3813624150, + 'master_key_0c': 3881579466, + 'master_key_0d': 723654444, + 'master_key_0e': 2690905064, + 'master_key_0f': 4082108335 +} + +def getMasterKeyIndex(i): + if i > 0: + return i-1 + else: + return 0 + +def keyAreaKey(cryptoType, i): + return keyAreaKeys[cryptoType][i] + +def get(key): + return keys[key] + +def getTitleKek(i): + return titleKeks[i] + +def decryptTitleKey(key, i): + kek = getTitleKek(i) + + crypto = aes128.AESECB(uhx(kek)) + return crypto.decrypt(key) + +def encryptTitleKey(key, i): + kek = getTitleKek(i) + + crypto = aes128.AESECB(uhx(kek)) + return crypto.encrypt(key) + +def changeTitleKeyMasterKey(key, currentMasterKeyIndex, newMasterKeyIndex): + return encryptTitleKey(decryptTitleKey(key, currentMasterKeyIndex), newMasterKeyIndex) + +def generateKek(src, masterKey, kek_seed, key_seed): + kek = [] + src_kek = [] + + crypto = aes128.AESECB(masterKey) + kek = crypto.decrypt(kek_seed) + + crypto = aes128.AESECB(kek) + src_kek = crypto.decrypt(src) + + if key_seed != None: + crypto = aes128.AESECB(src_kek) + return crypto.decrypt(key_seed) + else: + return src_kek + +def unwrapAesWrappedTitlekey(wrappedKey, keyGeneration): + aes_kek_generation_source = getKey('aes_kek_generation_source') + aes_key_generation_source = getKey('aes_key_generation_source') + + kek = generateKek(getKey('key_area_key_application_source'), getMasterKey(keyGeneration), aes_kek_generation_source, aes_key_generation_source) + + crypto = aes128.AESECB(kek) + return crypto.decrypt(wrappedKey) + +def getKey(key): + if key not in keys: + Print.error('{0} missing from {1}! This will lead to corrupted output.'.format(key, loadedKeysFile)) + raise IOError('{0} missing from {1}! This will lead to corrupted output.'.format(key, loadedKeysFile)) + foundKey = uhx(keys[key]) + foundKeyChecksum = crc32(foundKey) + if key in crc32_checksum: + if crc32_checksum[key] != foundKeyChecksum: + Print.error('{0} from {1} is invalid (crc32 missmatch)! This will lead to corrupted output.'.format(key, loadedKeysFile)) + raise IOError('{0} from {1} is invalid (crc32 missmatch)! This will lead to corrupted output.'.format(key, loadedKeysFile)) + elif current_process().name == 'MainProcess': + Print.info('Unconfirmed: crc32({0}) = {1}'.format(key, foundKeyChecksum)) + return foundKey + +def getMasterKey(masterKeyIndex): + return getKey('master_key_{0:02x}'.format(masterKeyIndex)) + +def existsMasterKey(masterKeyIndex): + return 'master_key_{0:02x}'.format(masterKeyIndex) in keys + +def load(fileName): + try: + global keyAreaKeys + global titleKeks + global loadedKeysFile + loadedKeysFile = fileName + + with open(fileName, encoding="utf8") as f: + for line in f.readlines(): + r = re.match('\s*([a-z0-9_]+)\s*=\s*([A-F0-9]+)\s*', line, re.I) + if r: + keys[r.group(1)] = r.group(2) + + aes_kek_generation_source = getKey('aes_kek_generation_source') + aes_key_generation_source = getKey('aes_key_generation_source') + titlekek_source = getKey('titlekek_source') + key_area_key_application_source = getKey('key_area_key_application_source') + key_area_key_ocean_source = getKey('key_area_key_ocean_source') + key_area_key_system_source = getKey('key_area_key_system_source') + + keyAreaKeys = [] + for i in range(32): + keyAreaKeys.append([None, None, None]) + + for i in range(32): + if not existsMasterKey(i): + continue + masterKey = getMasterKey(i) + crypto = aes128.AESECB(masterKey) + titleKeks.append(crypto.decrypt(titlekek_source).hex()) + keyAreaKeys[i][0] = generateKek(key_area_key_application_source, masterKey, aes_kek_generation_source, aes_key_generation_source) + keyAreaKeys[i][1] = generateKek(key_area_key_ocean_source, masterKey, aes_kek_generation_source, aes_key_generation_source) + keyAreaKeys[i][2] = generateKek(key_area_key_system_source, masterKey, aes_kek_generation_source, aes_key_generation_source) + except BaseException as e: + Print.error(format_exc()) + Print.error(str(e)) + + + +keyScriptPath = Path(sys.argv[0]) +#While loop to get rid of things like C:\\Python37\\Scripts\\app.exe\\__main__.py +while not keyScriptPath.is_dir(): + keyScriptPath = keyScriptPath.parents[0] +keypath = keyScriptPath.joinpath('keys.txt') +dumpedKeys = Path.home().joinpath(".switch", "prod.keys") +if keypath.is_file(): + load(str(keypath)) +elif dumpedKeys.is_file(): + load(str(dumpedKeys)) +else: + errorMsg = "{0} or {1} not found!\nPlease dump your keys using https://github.com/shchmue/Lockpick_RCM/releases".format(str(keypath), str(dumpedKeys)) + Print.error(errorMsg) + input("Press Enter to exit...") + sys.exit(1) + diff --git a/py-test/nut/Print.py b/py-test/nut/Print.py new file mode 100644 index 00000000..e42ed081 --- /dev/null +++ b/py-test/nut/Print.py @@ -0,0 +1,34 @@ +import sys +import time + +global silent +enableInfo = True +enableError = True +enableWarning = True +enableDebug = False + +silent = False + +def info(s, pleaseNoPrint = None): + if pleaseNoPrint == None: + sys.stdout.write(s + "\n") + else: + while pleaseNoPrint.value() > 0: + #print("Wait") + time.sleep(0.01) + pleaseNoPrint.increment() + sys.stdout.write(s + "\n") + sys.stdout.flush() + pleaseNoPrint.decrement() + +def infoNoNewline(s): + sys.stdout.write(s) + +def error(s): + sys.stdout.write(s + "\n") + +def warning(s): + sys.stdout.write(s + "\n") + +def debug(s): + sys.stdout.write(s + "\n") diff --git a/py-test/nut/Titles.py b/py-test/nut/Titles.py new file mode 100644 index 00000000..a3f227b7 --- /dev/null +++ b/py-test/nut/Titles.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +import re +import time +import json +import nut +import operator +from nut import Print + + +global titles +titles = None + +class Title: + def __init__(self): + self.key = None + self.id = None + + def setId(self, id): + self.id = id.upper() + +global nsuIdMap +nsuIdMap = {} + +global regionTitles +regionTitles = {} + +def data(region = None, language = None): + global regionTitles + global titles + + if region: + if not region in regionTitles: + regionTitles[region] = {} + + if not language in regionTitles[region]: + regionTitles[region][language] = {} + + return regionTitles[region][language] + + if titles == None: + titles = {} + return titles + +def items(region = None, language = None): + if region: + return regionTitles[region][language].items() + + return titles.items() + +def get(key, region = None, language = None): + key = key.upper() + + if not key in data(region, language): + t = Title() + t.setId(key) + data(region, language)[key] = t + return data(region, language)[key] + +def contains(key, region = None): + return key in titles + +def erase(id): + id = id.upper() + del titles[id] + +def set(key, value): + titles[key] = value + + +def keys(region = None, language = None): + if region: + return regionTitles[region][language].keys() + + return titles.keys() if titles != None else {} + + diff --git a/py-test/nut/aes128.py b/py-test/nut/aes128.py new file mode 100644 index 00000000..76120d95 --- /dev/null +++ b/py-test/nut/aes128.py @@ -0,0 +1,428 @@ +# Pure python AES128 implementation +# SciresM, 2017 +from struct import unpack as up, pack as pk +from binascii import hexlify as hx, unhexlify as uhx +from Crypto.Cipher import AES +from Crypto.Util import Counter + +def sxor(s1, s2): + assert(len(s1) == len(s2)) + return b''.join([pk('B', x ^ y) for x,y in zip(s1, s2)]) + +class AESCBC: + '''Class for performing AES CBC cipher operations.''' + + def __init__(self, key, iv): + self.aes = AESECB(key) + if len(iv) != self.aes.block_size: + raise ValueError('IV must be of size %X!' % self.aes.block_size) + self.iv = iv + + def encrypt(self, data, iv=None): + '''Encrypts some data in CBC mode.''' + if iv is None: + iv = self.iv + out = b'' + while data: + encb = self.aes.encrypt_block_ecb(sxor(data[:0x10], iv)) + out += encb + iv = encb + data = data[0x10:] + return out + + def decrypt(self, data, iv=None): + '''Decrypts some data in CBC mode.''' + if len(data) % self.aes.block_size: + raise ValueError('Data is not aligned to block size!') + if iv is None: + iv = self.iv + out = b'' + while data: + decb = sxor(self.aes.decrypt_block_ecb(data[:0x10]), iv) + out += decb + iv = data[:0x10] + data = data[0x10:] + return out + + def set_iv(self, iv): + if len(iv) != self.aes.block_size: + raise ValueError('IV must be of size %X!' % self.aes.block_size) + self.iv = iv + +class AESCTR: + '''Class for performing AES CTR cipher operations.''' + + def __init__(self, key, nonce, offset = 0): + self.key = key + self.nonce = nonce + self.seek(offset) + + def encrypt(self, data, ctr=None): + if ctr is None: + ctr = self.ctr + return self.aes.encrypt(data) + + def decrypt(self, data, ctr=None): + return self.encrypt(data, ctr) + + def seek(self, offset): + self.ctr = Counter.new(64, prefix=self.nonce[0:8], initial_value=(offset >> 4)) + self.aes = AES.new(self.key, AES.MODE_CTR, counter=self.ctr) + + def bktrPrefix(self, ctr_val): + return self.nonce[0:4] + ctr_val.to_bytes(4, 'big') + + def bktrSeek(self, offset, ctr_val, virtualOffset = 0): + offset += virtualOffset + self.ctr = Counter.new(64, prefix=self.bktrPrefix(ctr_val), initial_value=(offset >> 4)) + self.aes = AES.new(self.key, AES.MODE_CTR, counter=self.ctr) + +class AESXTS: + '''Class for performing AES XTS cipher operations''' + + def __init__(self, keys, sector=0): + self.keys = keys[:16], keys[16:] + if not(type(self.keys) is tuple and len(self.keys) == 2): + raise TypeError('XTS mode requires a tuple of two keys.') + self.K1 = AESECB(self.keys[0]) + self.K2 = AESECB(self.keys[1]) + + self.sector = sector + self.block_size = self.K1.block_size + + self.sector_size = 0x200 + + def encrypt(self, data, sector=None): + if sector is None: + sector = self.sector + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + while data: + tweak = self.get_tweak(sector) + out += self.encrypt_sector(data[:self.sector_size], tweak) + data = data[self.sector_size:] + sector += 1 + return out + + def encrypt_sector(self, data, tweak): + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + tweak = self.K2.encrypt(uhx('%032X' % tweak)) + while data: + out += sxor(tweak, self.K1.encrypt(sxor(data[:0x10], tweak))) + _t = int(hx(tweak[::-1]), 16) + _t <<= 1 + if _t & (1 << 128): + _t ^= ((1 << 128) | (0x87)) + tweak = uhx('%032X' % _t)[::-1] + data = data[0x10:] + return out + + def decrypt(self, data, sector=None): + if sector is None: + sector = self.sector + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + while data: + tweak = self.get_tweak(sector) + out += self.decrypt_sector(data[:self.sector_size], tweak) + data = data[self.sector_size:] + sector += 1 + return out + + def decrypt_sector(self, data, tweak): + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + tweak = self.K2.encrypt(uhx('%032X' % tweak)) + while data: + a = self.K1.decrypt(sxor(data[:0x10], tweak)) + out += sxor(tweak, a) + _t = int(hx(tweak[::-1]), 16) + _t <<= 1 + if _t & (1 << 128): + _t ^= ((1 << 128) | (0x87)) + tweak = uhx('%032X' % _t)[::-1] + data = data[0x10:] + return out + + def get_tweak(self, sector=None): + if sector is None: + sector = self.sector + tweak = 0 + for i in range(self.block_size): + tweak |= (sector & 0xFF) << (i * 8) + sector >>= 8 + return tweak + + def set_sector(self, sector): + self.sector = sector + +class AESXTSN: + '''Class for performing Nintendo AES XTS cipher operations''' + + def __init__(self, keys, sector_size=0x200, sector=0): + if not(type(keys) is tuple and len(keys) == 2): + raise TypeError('XTS mode requires a tuple of two keys.') + self.K1 = AESECB(keys[0]) + self.K2 = AESECB(keys[1]) + self.keys = keys + self.sector = sector + self.sector_size = sector_size + self.block_size = self.K1.block_size + + def encrypt(self, data, sector=None): + if sector is None: + sector = self.sector + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + while data: + tweak = self.get_tweak(sector) + out += self.encrypt_sector(data[:self.sector_size], tweak) + data = data[self.sector_size:] + sector += 1 + return out + + def encrypt_sector(self, data, tweak): + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + tweak = self.K2.encrypt(uhx('%032X' % tweak)) + while data: + out += sxor(tweak, self.K1.encrypt_block_ecb(sxor(data[:0x10], tweak))) + _t = int(hx(tweak[::-1]), 16) + _t <<= 1 + if _t & (1 << 128): + _t ^= ((1 << 128) | (0x87)) + tweak = uhx('%032X' % _t)[::-1] + data = data[0x10:] + return out + + def decrypt(self, data, sector=None): + if sector is None: + sector = self.sector + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + while data: + tweak = self.get_tweak(sector) + out += self.decrypt_sector(data[:self.sector_size], tweak) + data = data[self.sector_size:] + sector += 1 + return out + + def decrypt_sector(self, data, tweak): + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + tweak = self.K2.encrypt(uhx('%032X' % tweak)) + while data: + out += sxor(tweak, self.K1.decrypt_block_ecb(sxor(data[:0x10], tweak))) + _t = int(hx(tweak[::-1]), 16) + _t <<= 1 + if _t & (1 << 128): + _t ^= ((1 << 128) | (0x87)) + tweak = uhx('%032X' % _t)[::-1] + data = data[0x10:] + return out + + def get_tweak(self, sector=None): + '''Gets tweak for use in XEX.''' + if sector is None: + sector = self.sector + tweak = 0 + for i in range(self.block_size): + tweak |= (sector & 0xFF) << (i * 8) + sector >>= 8 + return tweak + + def set_sector(self, sector): + self.sector = sector + + def set_sector_size(self, sector_size): + self.sector_size = sector_size + +class AESECB: + '''Class for performing AES ECB cipher operations.''' + + # Constants for performing AES operations -- rcon table, S boxes. + rcon_table = [0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d] + sbox_enc = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16] + + sbox_dec = [0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d] + + mult_1_table = range(0x100) + mult_2_table = [0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5] + mult_3_table = [0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a] + + mult_9_table = [0x00,0x09,0x12,0x1b,0x24,0x2d,0x36,0x3f,0x48,0x41,0x5a,0x53,0x6c,0x65,0x7e,0x77,0x90,0x99,0x82,0x8b,0xb4,0xbd,0xa6,0xaf,0xd8,0xd1,0xca,0xc3,0xfc,0xf5,0xee,0xe7,0x3b,0x32,0x29,0x20,0x1f,0x16,0x0d,0x04,0x73,0x7a,0x61,0x68,0x57,0x5e,0x45,0x4c,0xab,0xa2,0xb9,0xb0,0x8f,0x86,0x9d,0x94,0xe3,0xea,0xf1,0xf8,0xc7,0xce,0xd5,0xdc,0x76,0x7f,0x64,0x6d,0x52,0x5b,0x40,0x49,0x3e,0x37,0x2c,0x25,0x1a,0x13,0x08,0x01,0xe6,0xef,0xf4,0xfd,0xc2,0xcb,0xd0,0xd9,0xae,0xa7,0xbc,0xb5,0x8a,0x83,0x98,0x91,0x4d,0x44,0x5f,0x56,0x69,0x60,0x7b,0x72,0x05,0x0c,0x17,0x1e,0x21,0x28,0x33,0x3a,0xdd,0xd4,0xcf,0xc6,0xf9,0xf0,0xeb,0xe2,0x95,0x9c,0x87,0x8e,0xb1,0xb8,0xa3,0xaa,0xec,0xe5,0xfe,0xf7,0xc8,0xc1,0xda,0xd3,0xa4,0xad,0xb6,0xbf,0x80,0x89,0x92,0x9b,0x7c,0x75,0x6e,0x67,0x58,0x51,0x4a,0x43,0x34,0x3d,0x26,0x2f,0x10,0x19,0x02,0x0b,0xd7,0xde,0xc5,0xcc,0xf3,0xfa,0xe1,0xe8,0x9f,0x96,0x8d,0x84,0xbb,0xb2,0xa9,0xa0,0x47,0x4e,0x55,0x5c,0x63,0x6a,0x71,0x78,0x0f,0x06,0x1d,0x14,0x2b,0x22,0x39,0x30,0x9a,0x93,0x88,0x81,0xbe,0xb7,0xac,0xa5,0xd2,0xdb,0xc0,0xc9,0xf6,0xff,0xe4,0xed,0x0a,0x03,0x18,0x11,0x2e,0x27,0x3c,0x35,0x42,0x4b,0x50,0x59,0x66,0x6f,0x74,0x7d,0xa1,0xa8,0xb3,0xba,0x85,0x8c,0x97,0x9e,0xe9,0xe0,0xfb,0xf2,0xcd,0xc4,0xdf,0xd6,0x31,0x38,0x23,0x2a,0x15,0x1c,0x07,0x0e,0x79,0x70,0x6b,0x62,0x5d,0x54,0x4f,0x46] + mult_B_table = [0x00,0x0b,0x16,0x1d,0x2c,0x27,0x3a,0x31,0x58,0x53,0x4e,0x45,0x74,0x7f,0x62,0x69,0xb0,0xbb,0xa6,0xad,0x9c,0x97,0x8a,0x81,0xe8,0xe3,0xfe,0xf5,0xc4,0xcf,0xd2,0xd9,0x7b,0x70,0x6d,0x66,0x57,0x5c,0x41,0x4a,0x23,0x28,0x35,0x3e,0x0f,0x04,0x19,0x12,0xcb,0xc0,0xdd,0xd6,0xe7,0xec,0xf1,0xfa,0x93,0x98,0x85,0x8e,0xbf,0xb4,0xa9,0xa2,0xf6,0xfd,0xe0,0xeb,0xda,0xd1,0xcc,0xc7,0xae,0xa5,0xb8,0xb3,0x82,0x89,0x94,0x9f,0x46,0x4d,0x50,0x5b,0x6a,0x61,0x7c,0x77,0x1e,0x15,0x08,0x03,0x32,0x39,0x24,0x2f,0x8d,0x86,0x9b,0x90,0xa1,0xaa,0xb7,0xbc,0xd5,0xde,0xc3,0xc8,0xf9,0xf2,0xef,0xe4,0x3d,0x36,0x2b,0x20,0x11,0x1a,0x07,0x0c,0x65,0x6e,0x73,0x78,0x49,0x42,0x5f,0x54,0xf7,0xfc,0xe1,0xea,0xdb,0xd0,0xcd,0xc6,0xaf,0xa4,0xb9,0xb2,0x83,0x88,0x95,0x9e,0x47,0x4c,0x51,0x5a,0x6b,0x60,0x7d,0x76,0x1f,0x14,0x09,0x02,0x33,0x38,0x25,0x2e,0x8c,0x87,0x9a,0x91,0xa0,0xab,0xb6,0xbd,0xd4,0xdf,0xc2,0xc9,0xf8,0xf3,0xee,0xe5,0x3c,0x37,0x2a,0x21,0x10,0x1b,0x06,0x0d,0x64,0x6f,0x72,0x79,0x48,0x43,0x5e,0x55,0x01,0x0a,0x17,0x1c,0x2d,0x26,0x3b,0x30,0x59,0x52,0x4f,0x44,0x75,0x7e,0x63,0x68,0xb1,0xba,0xa7,0xac,0x9d,0x96,0x8b,0x80,0xe9,0xe2,0xff,0xf4,0xc5,0xce,0xd3,0xd8,0x7a,0x71,0x6c,0x67,0x56,0x5d,0x40,0x4b,0x22,0x29,0x34,0x3f,0x0e,0x05,0x18,0x13,0xca,0xc1,0xdc,0xd7,0xe6,0xed,0xf0,0xfb,0x92,0x99,0x84,0x8f,0xbe,0xb5,0xa8,0xa3] + mult_D_table = [0x00,0x0d,0x1a,0x17,0x34,0x39,0x2e,0x23,0x68,0x65,0x72,0x7f,0x5c,0x51,0x46,0x4b,0xd0,0xdd,0xca,0xc7,0xe4,0xe9,0xfe,0xf3,0xb8,0xb5,0xa2,0xaf,0x8c,0x81,0x96,0x9b,0xbb,0xb6,0xa1,0xac,0x8f,0x82,0x95,0x98,0xd3,0xde,0xc9,0xc4,0xe7,0xea,0xfd,0xf0,0x6b,0x66,0x71,0x7c,0x5f,0x52,0x45,0x48,0x03,0x0e,0x19,0x14,0x37,0x3a,0x2d,0x20,0x6d,0x60,0x77,0x7a,0x59,0x54,0x43,0x4e,0x05,0x08,0x1f,0x12,0x31,0x3c,0x2b,0x26,0xbd,0xb0,0xa7,0xaa,0x89,0x84,0x93,0x9e,0xd5,0xd8,0xcf,0xc2,0xe1,0xec,0xfb,0xf6,0xd6,0xdb,0xcc,0xc1,0xe2,0xef,0xf8,0xf5,0xbe,0xb3,0xa4,0xa9,0x8a,0x87,0x90,0x9d,0x06,0x0b,0x1c,0x11,0x32,0x3f,0x28,0x25,0x6e,0x63,0x74,0x79,0x5a,0x57,0x40,0x4d,0xda,0xd7,0xc0,0xcd,0xee,0xe3,0xf4,0xf9,0xb2,0xbf,0xa8,0xa5,0x86,0x8b,0x9c,0x91,0x0a,0x07,0x10,0x1d,0x3e,0x33,0x24,0x29,0x62,0x6f,0x78,0x75,0x56,0x5b,0x4c,0x41,0x61,0x6c,0x7b,0x76,0x55,0x58,0x4f,0x42,0x09,0x04,0x13,0x1e,0x3d,0x30,0x27,0x2a,0xb1,0xbc,0xab,0xa6,0x85,0x88,0x9f,0x92,0xd9,0xd4,0xc3,0xce,0xed,0xe0,0xf7,0xfa,0xb7,0xba,0xad,0xa0,0x83,0x8e,0x99,0x94,0xdf,0xd2,0xc5,0xc8,0xeb,0xe6,0xf1,0xfc,0x67,0x6a,0x7d,0x70,0x53,0x5e,0x49,0x44,0x0f,0x02,0x15,0x18,0x3b,0x36,0x21,0x2c,0x0c,0x01,0x16,0x1b,0x38,0x35,0x22,0x2f,0x64,0x69,0x7e,0x73,0x50,0x5d,0x4a,0x47,0xdc,0xd1,0xc6,0xcb,0xe8,0xe5,0xf2,0xff,0xb4,0xb9,0xae,0xa3,0x80,0x8d,0x9a,0x97] + mult_E_table = [0x00,0x0e,0x1c,0x12,0x38,0x36,0x24,0x2a,0x70,0x7e,0x6c,0x62,0x48,0x46,0x54,0x5a,0xe0,0xee,0xfc,0xf2,0xd8,0xd6,0xc4,0xca,0x90,0x9e,0x8c,0x82,0xa8,0xa6,0xb4,0xba,0xdb,0xd5,0xc7,0xc9,0xe3,0xed,0xff,0xf1,0xab,0xa5,0xb7,0xb9,0x93,0x9d,0x8f,0x81,0x3b,0x35,0x27,0x29,0x03,0x0d,0x1f,0x11,0x4b,0x45,0x57,0x59,0x73,0x7d,0x6f,0x61,0xad,0xa3,0xb1,0xbf,0x95,0x9b,0x89,0x87,0xdd,0xd3,0xc1,0xcf,0xe5,0xeb,0xf9,0xf7,0x4d,0x43,0x51,0x5f,0x75,0x7b,0x69,0x67,0x3d,0x33,0x21,0x2f,0x05,0x0b,0x19,0x17,0x76,0x78,0x6a,0x64,0x4e,0x40,0x52,0x5c,0x06,0x08,0x1a,0x14,0x3e,0x30,0x22,0x2c,0x96,0x98,0x8a,0x84,0xae,0xa0,0xb2,0xbc,0xe6,0xe8,0xfa,0xf4,0xde,0xd0,0xc2,0xcc,0x41,0x4f,0x5d,0x53,0x79,0x77,0x65,0x6b,0x31,0x3f,0x2d,0x23,0x09,0x07,0x15,0x1b,0xa1,0xaf,0xbd,0xb3,0x99,0x97,0x85,0x8b,0xd1,0xdf,0xcd,0xc3,0xe9,0xe7,0xf5,0xfb,0x9a,0x94,0x86,0x88,0xa2,0xac,0xbe,0xb0,0xea,0xe4,0xf6,0xf8,0xd2,0xdc,0xce,0xc0,0x7a,0x74,0x66,0x68,0x42,0x4c,0x5e,0x50,0x0a,0x04,0x16,0x18,0x32,0x3c,0x2e,0x20,0xec,0xe2,0xf0,0xfe,0xd4,0xda,0xc8,0xc6,0x9c,0x92,0x80,0x8e,0xa4,0xaa,0xb8,0xb6,0x0c,0x02,0x10,0x1e,0x34,0x3a,0x28,0x26,0x7c,0x72,0x60,0x6e,0x44,0x4a,0x58,0x56,0x37,0x39,0x2b,0x25,0x0f,0x01,0x13,0x1d,0x47,0x49,0x5b,0x55,0x7f,0x71,0x63,0x6d,0xd7,0xd9,0xcb,0xc5,0xef,0xe1,0xf3,0xfd,0xa7,0xa9,0xbb,0xb5,0x9f,0x91,0x83,0x8d] + + mult_table = [None,mult_1_table,mult_2_table,mult_3_table,None,None,None,None,None,mult_9_table,None,mult_B_table,None,mult_D_table,mult_E_table,None] + + mix_mults = [[0x2,0x3,0x1,0x1],[0x1,0x2,0x3,0x1],[0x1,0x1,0x2,0x3],[0x3,0x1,0x1,0x2]] + unmix_mults = [[0xE,0xB,0xD,0x9],[0x9,0xE,0xB,0xD],[0xD,0x9,0xE,0xB],[0xB,0xD,0x9,0xE]] + + def __init__(self, key): + self.block_size, self.num_rounds = 0x10, 10 # 128-bit AES + if len(key) != self.block_size: + raise ValueError('Key must be of size %X!' % self.block_size) + self.keys = [list(up('>IIII', key))] + for i in range(1, self.num_rounds+1): + new_key = [self.key_schedule_core(self.keys[i-1][3], i) ^ self.keys[i-1][0]] + for j in range(1, 4): + new_key.append(self.keys[i-1][j] ^ new_key[j-1]) + self.keys.append(new_key) + + def encrypt(self, data): + '''Encrypts some data in ECB mode.''' + out = b'' + while data: + out += self.encrypt_block_ecb(data[:0x10]) + data = data[0x10:] + return out + + def decrypt(self, data): + '''Decrypts some data in EBC mode.''' + if len(data) % self.block_size: + raise ValueError('Data is not aligned to block size!') + out = b'' + while data: + out += self.decrypt_block_ecb(data[:0x10]) + data = data[0x10:] + return out + + def encrypt_block_ecb(self, block): + words = list(up('>IIII', self.pad_block(block))) + for i in range(len(words)): + words[i] ^= self.keys[0][i] + for rnd in range(1, self.num_rounds + 1): + for i in range(len(words)): + words[i] = self.send_through_sbox(words[i], self.sbox_enc) + words = self.shift_columns(words) + if rnd != self.num_rounds: + words = self.mix_columns(words) + for i in range(len(words)): + words[i] ^= self.keys[rnd][i] + return pk('>IIII', words[0], words[1], words[2], words[3]) + + def decrypt_block_ecb(self, block): + assert(len(block) == self.block_size) + words = list(up('>IIII', block)) + for rnd in range(self.num_rounds, 0, -1): + for i in range(len(words)): + words[i] ^= self.keys[rnd][i] + if rnd != self.num_rounds: + words = self.unmix_columns(words) + words = self.unshift_columns(words) + for i in range(len(words)): + words[i] = self.send_through_sbox(words[i], self.sbox_dec) + for i in range(len(words)): + words[i] ^= self.keys[0][i] + return pk('>IIII', words[0], words[1], words[2], words[3]) + + # Helper functions + def rotate_op(self, word): + '''Rotate operation''' + return ((word & 0xFFFFFF) << 8) | ((word & 0xFF000000) >> 24) + + def rcon_op(self, i): + '''Rcon operation''' + assert(0 <= i and i < len(self.rcon_table)) + return self.rcon_table[i] + + def send_through_sbox(self, word, sbox=sbox_enc): + '''Sends a 32-bit word through an sbox.''' + return (sbox[((word & (0xFF << 0x18)) >> 0x18)] << 0x18) | \ + (sbox[((word & (0xFF << 0x10)) >> 0x10)] << 0x10) | \ + (sbox[((word & (0xFF << 0x08)) >> 0x08)] << 0x08) | \ + (sbox[((word & (0xFF << 0x00)) >> 0x00)] << 0x00) + + def shift_columns(self, words): + '''Performs column shifting for AES.''' + new_words = [] + new_words.append((words[0] & 0xFF000000) | (words[1] & 0xFF0000) | (words[2] & 0xFF00) | (words[3] & 0xFF)) + new_words.append((words[1] & 0xFF000000) | (words[2] & 0xFF0000) | (words[3] & 0xFF00) | (words[0] & 0xFF)) + new_words.append((words[2] & 0xFF000000) | (words[3] & 0xFF0000) | (words[0] & 0xFF00) | (words[1] & 0xFF)) + new_words.append((words[3] & 0xFF000000) | (words[0] & 0xFF0000) | (words[1] & 0xFF00) | (words[2] & 0xFF)) + return new_words + + def unshift_columns(self, words): + '''Performs column unshifting for AES.''' + new_words = [] + new_words.append((words[0] & 0xFF000000) | (words[3] & 0xFF0000) | (words[2] & 0xFF00) | (words[1] & 0xFF)) + new_words.append((words[1] & 0xFF000000) | (words[0] & 0xFF0000) | (words[3] & 0xFF00) | (words[2] & 0xFF)) + new_words.append((words[2] & 0xFF000000) | (words[1] & 0xFF0000) | (words[0] & 0xFF00) | (words[3] & 0xFF)) + new_words.append((words[3] & 0xFF000000) | (words[2] & 0xFF0000) | (words[1] & 0xFF00) | (words[0] & 0xFF)) + return new_words + + def mix_columns(self, words): + '''Performs column mixing for 128-bit AES''' + return [self.mix_column(words[0], self.mix_mults), self.mix_column(words[1], self.mix_mults), \ + self.mix_column(words[2], self.mix_mults), self.mix_column(words[3], self.mix_mults)] + + def unmix_columns(self, words): + '''Performs column unmixing for 128-bit AES''' + return [self.mix_column(words[0], self.unmix_mults), self.mix_column(words[1], self.unmix_mults), \ + self.mix_column(words[2], self.unmix_mults), self.mix_column(words[3], self.unmix_mults)] + + def mix_column(self, word, mults): + '''Performs column mixing on a single column''' + return ((self.mix(word, mults[0])) << 0x18) | \ + ((self.mix(word, mults[1])) << 0x10) | \ + ((self.mix(word, mults[2])) << 0x08) | \ + ((self.mix(word, mults[3])) << 0x00) + + def mix(self, word, mix): + '''Mixes a word according to a given multiplier.''' + return (self.mult_table[mix[0]][((word >> 0x18) & 0xFF)]) ^ \ + (self.mult_table[mix[1]][((word >> 0x10) & 0xFF)]) ^ \ + (self.mult_table[mix[2]][((word >> 0x08) & 0xFF)]) ^ \ + (self.mult_table[mix[3]][((word >> 0x00) & 0xFF)]) & 0xFF + + def key_schedule_core(self, word, i, sbox=sbox_enc): + '''Performs core key scheduling operation.''' + return self.send_through_sbox(self.rotate_op(word), sbox) ^ (self.rcon_op(i) << 0x18) + + def pad_block(self, block): + '''Pads a block using CMS padding.''' + assert(len(block) <= self.block_size) + num_pad = self.block_size - len(block) + right = (chr(num_pad) * num_pad).encode() + return block + right diff --git a/py-test/verif.py b/py-test/verif.py new file mode 100644 index 00000000..9b2778eb --- /dev/null +++ b/py-test/verif.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +import os +import sys +import re + +from pathlib import Path +import Fs + +def parse_name(file): + res = re.search(r'(?P\[[A-F0-9]{16}\])( )?(?P\[v\d+\])(.*)?(?P\[(BASE|UPD(ATE)?|DLC( \d+)?)\])?(.*)?\.(xci|xcz|nsp|nsz)$', file, re.I) + + if res is None: + return None + + title_id = res.group('title_id')[1:-1] + version = int(res.group('version')[2:-1]) + title_type = None + + if version % 65536 != 0: + return None + + title_oei = int(title_id[-4:-3], 16) + title_ext = title_id[-3:] + + if title_oei % 2 == 0 and title_ext == '000': + title_type = 'BASE' + elif title_oei % 2 == 0 and title_ext == '800': + title_type = 'UPD' + elif title_oei % 2 == 1 and int(title_ext, 16) > 0: + title_type = 'DLC' + + if title_type is None: + return None + + return { + 'title_id': title_id, + 'title_type': title_type, + 'title_ext': title_ext, + 'version': version, + } + +def verify(file): + try: + filename = os.path.abspath(file) + + if file.endswith('.xci'): + f = Fs.factory(filename) + f.open(filename, 'rb') + elif file.endswith('.xcz'): + f = Fs.Xci.Xci(filename) + elif file.endswith('.nsp') or file.endswith('.nsz'): + f = Fs.Nsp.Nsp(filename, 'rb') + else: + return False, {} + + res = parse_name(file) + log_info = f"{file.upper()[-3:]} {res['title_id']} v{round(res['version']/65536)} {res['title_type']}" + if res['title_type'] == 'DLC': + log_info += f" {str(int(res['title_ext'], 16)).zfill(4)}" + print(f'[:INFO:] Verifying... {log_info}\n') + + check = decrypt_verify(f) + + except BaseException as e: + raise e + +def decrypt_verify(self): + listed_files=list() + listed_certs=list() + valid_files=list() + + print('[:INFO:] DECRYPTION TEST') + + if(type(self) == Fs.Xci.Xci): + for nspf in self.hfs0: + if nspf._path == 'secure': + for file in nspf: + print(file._path) + if(type(self) == Fs.Nsp.Nsp): + for nspf in self: + print(nspf._path) diff --git a/py-test/verif_folder.py b/py-test/verif_folder.py new file mode 100644 index 00000000..9d782ed1 --- /dev/null +++ b/py-test/verif_folder.py @@ -0,0 +1,105 @@ +import os +import json +import requests +import verif +import shutil +import re + +squirrel_dir = os.path.dirname(os.path.abspath(__file__)) +logs_dir = os.path.abspath(os.path.join(squirrel_dir, '..', 'logs')) + +import argparse +parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter) +parser.add_argument('-i', '--input', help = 'input folder') +parser.add_argument('-w', '--webhook-url', help = 'discord webhook url', required = False) +args = parser.parse_args() +config = vars(args) + +INCP_PATH = config['input'] +WHOOK_URL = config['webhook_url'] + +def send_hook(message_content): + try: + print(message_content) + payload = { + 'username': 'Contributions', + 'content': message_content.strip() + } + headers = {"Content-type": "application/json"} + response = requests.post(WHOOK_URL, data=json.dumps(payload), headers=headers) + response.raise_for_status() + except: + pass + +def scan_folder(): + ipath = os.path.abspath(INCP_PATH) + fname = os.path.basename(ipath).upper() + + lpath_badfolder = os.path.join(logs_dir, 'bad-folder.log') + lpath_badname = os.path.join(logs_dir, 'bad-names.log') + lpath_badfile = os.path.join(logs_dir, 'bad-file.log') + + if not os.path.exists(logs_dir): + os.makedirs(logs_dir) + + if os.path.exists(lpath_badfolder): + os.remove(lpath_badfolder) + if os.path.exists(lpath_badname): + os.remove(lpath_badname) + if os.path.exists(lpath_badfile): + os.remove(lpath_badfile) + + if not os.path.exists(ipath): + print(f'[:WARN:] Please put your files in "{ipath}" and run this script again.') + return + + for item in sorted(os.listdir(ipath)): + item_path = os.path.join(ipath, item) + if not os.path.isfile(item_path): + continue + if not item.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): + continue + + send_hook(f'\n[:INFO:] File found: {item}') + send_hook(f'[:INFO:] Checking syntax...') + + data = verif.parse_name(item) + + if data is None: + with open(lpath_badname, 'a') as f: + f.write(f'{item}\n') + continue + + if re.match(r'^BASE|UPD(ATE)?|DLC|XCI$', fname) is not None: + if item.lower().endswith(('.xci', '.xcz')): + iscart = True + else: + iscart = False + if fname == 'UPDATE': + fname = 'UPD' + if fname == 'BASE' and data['title_type'] != 'BASE' or fname == 'BASE' and iscart == True: + with open(lpath_badfolder, 'a') as f: + f.write(f'{item}\n') + if fname == 'UPD' and data['title_type'] != 'UPD' or fname == 'UPD' and iscart == True: + with open(lpath_badfolder, 'a') as f: + f.write(f'{item}\n') + if fname == 'DLC' and data['title_type'] != 'DLC' or fname == 'DLC' and iscart == True: + with open(lpath_badfolder, 'a') as f: + f.write(f'{item}\n') + if fname == 'XCI' and iscart == False: + with open(lpath_badfolder, 'a') as f: + f.write(f'{item}\n') + + try: + verif.verify(item_path) + except Exception as e: + send_hook(f'[:WARN:] An error occurred:\n{item}: {str(e)}') + + + +if __name__ == "__main__": + if INCP_PATH: + scan_folder() + else: + parser.print_help() + print() diff --git a/py/lib/NXKeys.py b/py/lib/NXKeys.py index f758a40a..3e50a92d 100644 --- a/py/lib/NXKeys.py +++ b/py/lib/NXKeys.py @@ -12,8 +12,8 @@ keysFiles = [ Path(os.path.join(homePath, 'prod.keys')), - Path(os.path.join(srcPath, '../prod.keys')), - Path(os.path.join(srcPath, '../keys.txt')), + Path(os.path.join(srcPath, '..', 'prod.keys')), + Path(os.path.join(srcPath, '..', 'keys.txt')), ] class Keys(dict): diff --git a/py/requirements.txt b/py/requirements.txt index 4c601ca1..10205fe4 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -17,3 +17,4 @@ google-auth-oauthlib windows-curses oauth2client comtypes +enlighten