diff --git a/README.md b/README.md index a13ddc95..843255cf 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Nintendo Switch Tools -Tools for XCI, XCZ, NSP and NSZ +Tools for XCI, XCZ, NSP and NSZ Based on nut, NSC_B and nsz # pypi.org -for using nstools.Fs, nstools.lib and nstools.nut: +for using nstools, nsz.Fs and nsz.nut: https://pypi.org/project/nstools/ diff --git a/build/setup.py b/build/setup.py index 84add26b..f7d1829d 100644 --- a/build/setup.py +++ b/build/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name = 'nstools', - version = '1.2.3', + version = '2.0.0b1', url = 'https://github.com/seiya-dev/NSTools', long_description = long_description, long_description_content_type = 'text/markdown', @@ -31,11 +31,10 @@ ], packages = [ - 'nstools.Fs', - 'nstools.nut', - 'nstools.lib', + 'nstools', ], install_requires = [ + 'nsz @ git+https://github.com/nicoboss/nsz.git@2aac384', 'zstandard', 'enlighten', 'requests', diff --git a/py/ns_extract_hashes.py b/py/ns_extract_hashes.py index 9c8c2ff6..ae866dad 100644 --- a/py/ns_extract_hashes.py +++ b/py/ns_extract_hashes.py @@ -1,32 +1,24 @@ #! /usr/bin/python3 from binascii import hexlify as hx, unhexlify as uhx - -import os -import sys - from pathlib import Path +import argparse +import sys -from nstools.nut import Keys - -from nstools.Fs import factory -from nstools.Fs import Pfs0, Nca, Type - -from nstools.lib import FsTools +from nsz.nut import Keys +from nsz.Fs import factory +from nsz.Fs import Pfs0, Nca, Type +from nstools import FsTools # set app path appPath = Path(sys.argv[0]) while not appPath.is_dir(): appPath = appPath.parents[0] -appPath = os.path.abspath(appPath) +appPath = Path(appPath).resolve().as_posix() print(f'[:INFO:] App Path: {appPath}') -# set logs path -# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs')) -# print(f'[:INFO:] Logs Path: {logs_dir}') - -import argparse +# set args parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-i', '--input', help = 'input file') args = parser.parse_args() @@ -45,16 +37,19 @@ def send_hook(message_content): pass def scan_file(): - ipath = os.path.abspath(INCP_PATH) - if not os.path.isfile(ipath): + ipath = Path(INCP_PATH).resolve().as_posix() + + if not Path(ipath).is_file() or Path(ipath).is_symlink(): return - if not ipath.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): + if not Path(ipath).name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): return container = factory(Path(ipath).resolve()) - container.open(ipath, 'rb') + container.open(ipath) + if ipath.lower().endswith(('.xci', '.xcz')): container = container.hfs0['secure'] + try: for nspf in container: if isinstance(nspf, Nca.Nca) and nspf.header.contentType == Type.Content.META: diff --git a/py/ns_extract_meta.py b/py/ns_extract_meta.py index 8e05fc6a..a0b5c9fa 100644 --- a/py/ns_extract_meta.py +++ b/py/ns_extract_meta.py @@ -1,32 +1,24 @@ #! /usr/bin/python3 from binascii import hexlify as hx, unhexlify as uhx - -import os -import sys - from pathlib import Path +import argparse +import sys -from nstools.nut import Keys - -from nstools.Fs import factory -from nstools.Fs import Pfs0, Xci, Nsp, Nca, Type - -from nstools.lib import FsTools +from nsz.nut import Keys +from nsz.Fs import factory +from nsz.Fs import Pfs0, Xci, Nsp, Nca, Type +from nstools import FsTools # set app path appPath = Path(sys.argv[0]) while not appPath.is_dir(): appPath = appPath.parents[0] -appPath = os.path.abspath(appPath) +appPath = Path(appPath).resolve().as_posix() print(f'[:INFO:] App Path: {appPath}') -# set logs path -# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs')) -# print(f'[:INFO:] Logs Path: {logs_dir}') - -import argparse +# set args parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-i', '--input', help = 'input file') args = parser.parse_args() @@ -77,14 +69,16 @@ def extract_meta_from_cnmt(cnmt_sections): print(f'> HASH: {entry.hash.hex()}') def scan_file(): - ipath = os.path.abspath(INCP_PATH) - if not os.path.isfile(ipath): + ipath = Path(INCP_PATH).resolve().as_posix() + + if not Path(ipath).is_file() or Path(ipath).is_symlink(): return - if not ipath.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): + if not Path(ipath).name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): return container = factory(Path(ipath).resolve()) - container.open(ipath, 'rb', meta_only=True) + container.open(ipath, meta_only=True) + try: for cnmt in get_cnmts(container): extract_meta_from_cnmt(cnmt) diff --git a/py/ns_ticket_info.py b/py/ns_ticket_info.py index 323db633..54c1cf6e 100644 --- a/py/ns_ticket_info.py +++ b/py/ns_ticket_info.py @@ -1,26 +1,20 @@ #! /usr/bin/python3 -import os -import sys - from pathlib import Path +import argparse +import sys -from nstools.nut import Keys - -from nstools.Fs import factory, Ticket +from nsz.nut import Keys +from nsz.Fs import factory, Ticket # set app path appPath = Path(sys.argv[0]) while not appPath.is_dir(): appPath = appPath.parents[0] -appPath = os.path.abspath(appPath) +appPath = Path(appPath).resolve().as_posix() print(f'[:INFO:] App Path: {appPath}') -# set logs path -# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs')) -# print(f'[:INFO:] Logs Path: {logs_dir}') - -import argparse +# set args parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-i', '--input', help = 'input file') args = parser.parse_args() @@ -39,15 +33,17 @@ def send_hook(message_content): pass def scan_file(): - ipath = os.path.abspath(INCP_PATH) - if not os.path.isfile(ipath): + ipath = Path(INCP_PATH).resolve().as_posix() + + if not Path(ipath).is_file() or Path(ipath).is_symlink(): return - if not ipath.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): + if not Path(ipath).name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): return container = factory(Path(ipath).resolve()) - container.open(ipath, 'rb') - if ipath.lower().endswith(('.xci', '.xcz')): + container.open(ipath) + + if ipath.name.lower().endswith(('.xci', '.xcz')): container = container.hfs0['secure'] try: diff --git a/py/ns_verify_folder.py b/py/ns_verify_folder.py index f1c7c399..681ffbe8 100644 --- a/py/ns_verify_folder.py +++ b/py/ns_verify_folder.py @@ -1,29 +1,22 @@ #! /usr/bin/python3 -import os -import sys -import json -import requests -import re - from pathlib import Path +import argparse +import requests +import json +import sys -from nstools.nut import Keys - -from nstools.lib import Verify +from nsz.nut import Keys +from nstools import Verify # set app path appPath = Path(sys.argv[0]) while not appPath.is_dir(): appPath = appPath.parents[0] -appPath = os.path.abspath(appPath) +appPath = Path(appPath).resolve().as_posix() print(f'[:INFO:] App Path: {appPath}') -# set logs path -# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs')) -# print(f'[:INFO:] Logs Path: {logs_dir}') - -import argparse +# set args 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) @@ -58,83 +51,45 @@ def send_hook(message_content: str = '', PadPrint: bool = False): 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) + ipath = Path(INCP_PATH).resolve().as_posix() - # 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): + if not Path(ipath).exists(): print(f'[:WARN:] Please put your files in "{ipath}" and run this script again.') return files = list() - for item in sorted(os.listdir(ipath)): - item_path = os.path.join(ipath, item) - if not os.path.isfile(item_path): + + for item in sorted(list(Path(ipath).iterdir())): + item_path = Path(item) + if not item_path.is_file() or item_path.is_symlink(): continue - if not item.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): + if not item_path.name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')): continue - files.append(item) + files.append(item.as_posix()) findex = 0 for item in sorted(files): - item_path = os.path.join(ipath, item) + item_path = Path(item) findex += 1 - send_hook(f'[:INFO:] File found ({findex} of {len(files)}): {item_path}', True) + send_hook(f'[:INFO:] File found ({findex} of {len(files)}): {item_path.name}', True) send_hook(f'[:INFO:] Checking filename...') - data = Verify.parse_name(item) + data = Verify.parse_name(item_path.name) if data is None: - send_hook(f'{item_path}: BAD NAME') - # with open(lpath_badname, 'a') as f: - # f.write(f'{item_path}\n') + send_hook(f'{item_path.name}: BAD NAME') - # if data is not None and 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_path}\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_path}\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_path}\n') - # if fname == 'XCI' and iscart == False: - # with open(lpath_badfolder, 'a') as f: - # f.write(f'{item_path}\n') - - rootpath = os.path.dirname(item_path) - basename = os.path.basename(item_path) + rootpath = item_path.parent.as_posix() + basename = item_path.name basename = f'{basename[:-4]}-{basename[-3:]}-verify' - log_name = os.path.join(rootpath, basename) + log_name = Path(rootpath).joinpath(basename).as_posix() try: send_hook(f'[:INFO:] Verifying...') - nspTest, nspLog = Verify.verify(item_path) + nspTest, nspLog = Verify.verify(item_path.as_posix()) if nspTest != True: send_hook(f'{item_path}: BAD', True) - # with open(lpath_badfile, 'a') as f: - # f.write(f'{item_path}\n') else: send_hook(f'{item_path}: OK', True) if SAVE_VLOG == True: @@ -147,7 +102,6 @@ def scan_folder(): except Exception as e: send_hook(f'[:WARN:] An error occurred:\n{item_path}: {str(e)}') - if __name__ == "__main__": if INCP_PATH: scan_folder() diff --git a/py/nstools/Fs/BaseFs.py b/py/nstools/Fs/BaseFs.py deleted file mode 100644 index 4f07f0c9..00000000 --- a/py/nstools/Fs/BaseFs.py +++ /dev/null @@ -1,178 +0,0 @@ -from binascii import hexlify as hx, unhexlify as uhx - -from nstools.nut import Print - -from . import Bktr -from . import Type - -from .File import File -from .File import MemoryFile -from .Cnmt import Cnmt - - -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, meta_only=False): - r = super(BaseFs, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - - 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(f'{tabs}magic = {self.magic}') - Print.info(f'{tabs}fsType = {self.fsType}') - Print.info(f'{tabs}cryptoType = {self.cryptoType}') - Print.info(f'{tabs}size = {hex(self.size)}') - Print.info(f'{tabs}headerSize = {self._headerSize}') - Print.info(f'{tabs}offset = {hex(self.offset)} - ({hex(self.sectionStart)})') - if self.cryptoCounter: - Print.info(f'{tabs}cryptoCounter = {hx(self.cryptoCounter)}') - - if self.cryptoKey: - Print.info(f'{tabs}cryptoKey = {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/nstools/Fs/Bktr.py b/py/nstools/Fs/Bktr.py deleted file mode 100644 index f6ec76c5..00000000 --- a/py/nstools/Fs/Bktr.py +++ /dev/null @@ -1,268 +0,0 @@ -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 nstools.nut import aes128 -from nstools.nut import Hex -from nstools.nut import Keys, Print - -from .File import File, MemoryFile - - -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/nstools/Fs/Cnmt.py b/py/nstools/Fs/Cnmt.py deleted file mode 100644 index 1e58e78b..00000000 --- a/py/nstools/Fs/Cnmt.py +++ /dev/null @@ -1,80 +0,0 @@ -from binascii import hexlify as hx, unhexlify as uhx - -from nstools.nut import Print, Keys - -from .File import File - - -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, meta_only = False): - super(Cnmt, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - 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/nstools/Fs/File.py b/py/nstools/Fs/File.py deleted file mode 100644 index ef7a4635..00000000 --- a/py/nstools/Fs/File.py +++ /dev/null @@ -1,500 +0,0 @@ -from binascii import hexlify as hx, unhexlify as uhx - -import os.path - -from enum import IntEnum -import hashlib - -from nstools.nut import aes128, Print, Hex - -from . import Type - - -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 = 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, meta_only = False): - 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, meta_only) - - 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 == Type.Crypto.CTR or self.cryptoType == Type.Crypto.BKTR: - if self.cryptoKey: - self.crypto = aes128.AESCTR(self.cryptoKey, nonce = self.cryptoCounter.copy()) - self.cryptoType = Type.Crypto.CTR - - self.enableBufferedIO(0x10, 0x10) - - elif self.cryptoType == Type.Crypto.XTS: - if self.cryptoKey: - self.crypto = aes128.AESXTS(self.cryptoKey) - self.cryptoType = 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 == Type.Crypto.BKTR: - self.cryptoType = Type.Crypto.BKTR - elif self.cryptoType == Type.Crypto.NCA0: - self.cryptoType = Type.Crypto.NCA0 - elif self.cryptoType == Type.Crypto.NONE: - self.cryptoType = Type.Crypto.NONE - - - def open(self, path, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, meta_only=False): - 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 == Type.Crypto.CTR: - self.crypto.seek(self.offset + self._bufferOffset) - elif self.cryptoType == 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 == Type.Crypto.CTR or self.cryptoType == 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 == Type.Crypto.CTR or self.cryptoType == 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/nstools/Fs/Hfs0.py b/py/nstools/Fs/Hfs0.py deleted file mode 100644 index 87015e6b..00000000 --- a/py/nstools/Fs/Hfs0.py +++ /dev/null @@ -1,184 +0,0 @@ -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 -from pathlib import Path - -from nstools.nut import aes128 -from nstools.nut import Hex -from nstools.nut import Keys -from nstools.nut import Print - -from . import factory -from .File import BaseFile -from .File import File -from .Pfs0 import Pfs0 -from .BaseFs import BaseFs - - -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) - self.addpos = self.headerSize - self.written = False - - 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)) - self.written = True - pos = self.tell() - if pos > self.actualSize: - self.actualSize = pos - - def add(self, name, size, pleaseNoPrint = None): - if self.written: - self.addpos = self.tell() - self.written = False - Print.info(f'[ADDING] {name} {hex(size)} bytes to HFS0 at {hex(self.addpos)}', pleaseNoPrint) - partition = self.partition(self.addpos, size, n = BaseFile()) - self.files.append({'name': name, 'size': size, 'offset': self.addpos, 'partition': partition}) - self.addpos += size - return partition - - def get(self, name): - for i in self.files: - if i['name'] == name: - return i['partition'] - 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 updateHashHeader(self): - pass - - def getHeader(self): - stringTable = '\x00'.join(file['name'] for file in self.files)+'\x00' - - 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 = 0 #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, meta_only = False): - r = super(BaseFs, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - 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 - Print.info(f'[OPEN ] {name} {hex(size)} bytes at {hex(offset)}') - - self.readInt32() # junk data - - if meta_only and (name != 'secure' and not 'cnmt' in name): - continue - - f = factory(Path(name)) - - f._path = name - f.offset = offset - f.size = size - self.files.append(self.partition(offset + headerSize, f.size, f, meta_only=meta_only)) - - 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/nstools/Fs/Ivfc.py b/py/nstools/Fs/Ivfc.py deleted file mode 100644 index 09e3dfe7..00000000 --- a/py/nstools/Fs/Ivfc.py +++ /dev/null @@ -1,49 +0,0 @@ -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 nstools.nut import aes128 -from nstools.nut import Hex -from nstools.nut import Keys -from nstools.nut import Print - -from .File import File - - -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/nstools/Fs/Nacp.py b/py/nstools/Fs/Nacp.py deleted file mode 100644 index 00123021..00000000 --- a/py/nstools/Fs/Nacp.py +++ /dev/null @@ -1,613 +0,0 @@ -from binascii import hexlify as hx, unhexlify as uhx -from enum import IntEnum - -from nstools.nut import Print -from nstools.nut import Keys - -from .File import File - - -#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/nstools/Fs/Nca.py b/py/nstools/Fs/Nca.py deleted file mode 100644 index 413a36b9..00000000 --- a/py/nstools/Fs/Nca.py +++ /dev/null @@ -1,305 +0,0 @@ -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 nstools.nut import aes128 -from nstools.nut import Hex -from nstools.nut import Keys -from nstools.nut import Print -from nstools.nut import Titles - -from . import Type - -from .File import File -from .Rom import Rom -from .Pfs0 import Pfs0 -from .BaseFs import BaseFs - - -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 == Type.Fs.PFS0: - return Pfs0(buffer, cryptoKey = cryptoKey) - - if fsType == 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, meta_only=False): - super(NcaHeader, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - 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 = 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) - - self.keyStatus = True - - 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) - self.keyStatus = False - 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, meta_only=False): - super(Nca, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - - self.header = NcaHeader() - self.partition(0x0, 0xC00, self.header, 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() - if self.header.keyStatus != True: - return - - 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) - - try: - fs.open(None, 'rb') - except BaseException as e: - pass - - self.titleKeyDec = None - - def masterKey(self): - return max(self.header.cryptoType, self.header.cryptoType2) - - def buildId(self): - if self.header.contentType != 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 == Type.Content.PROGRAM: - Print.info(tabs + 'build Id: ' + str(self.buildId())) diff --git a/py/nstools/Fs/Nsp.py b/py/nstools/Fs/Nsp.py deleted file mode 100644 index 935714b1..00000000 --- a/py/nstools/Fs/Nsp.py +++ /dev/null @@ -1,452 +0,0 @@ -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 - -import enlighten - -from nstools.nut import aes128 -from nstools.nut import Hex -from nstools.nut import Keys -from nstools.nut import Print - -from nstools.nut import Titles -from nstools.nut import Titles as Title - -from nstools.lib.PathTools import * - -from .File import File -from .Pfs0 import Pfs0 -from .Ticket import Ticket -from .Nca import Nca - - -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, meta_only=False): - super(Nsp, self).open(path or self.path, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - - 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 = pos - - def add(self, name, size, pleaseNoPrint = None): - if self.written: - self.addpos = self.tell() - self.written = False - Print.info(f'[ADDING] {name} {hex(size)} bytes to PFS0 at {hex(self.addpos)}', pleaseNoPrint) - partition = self.partition(self.addpos, size, n = BaseFile()) - self.files.append({'name': name, 'size': size, 'offset': self.addpos, 'partition': partition}) - self.addpos += size - return partition - - def get(self, name): - for i in self.files: - if i['name'] == name: - return i['partition'] - 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() - - #0xff => 0x1, 0x100 => 0x20, 0x1ff => 0x1, 0x120 => 0x20 - def allign0x20(self, n): - return 0x20-n%0x20 - - def getStringTableSize(self): - stringTableNonPadded = '\x00'.join(file['name'] for file in self.files)+'\x00' - headerSizeNonPadded = 0x10 + len(self.files) * 0x18 + len(stringTableNonPadded) - stringTableSizePadded = len(stringTableNonPadded) + self.allign0x20(headerSizeNonPadded) - if self._stringTableSize == None: - self._stringTableSize = stringTableSizePadded - elif len(stringTableNonPadded) > self._stringTableSize: - self._stringTableSize = len(stringTableNonPadded) - return self._stringTableSize - - def updateHashHeader(self): - pass - - def getFirstFileOffset(self): - return self.files[0].offset - - def getHeader(self): - stringTableNonPadded = '\x00'.join(file['name'] for file in self.files)+'\x00' - stringTableSizePadded = self.getStringTableSize() - stringTable = stringTableNonPadded + ('\x00'*(stringTableSizePadded-len(stringTableNonPadded))) - headerSize = 0x10 + len(self.files) * 0x18 + stringTableSizePadded - - h = b'' - h += b'PFS0' - h += len(self.files).to_bytes(4, byteorder='little') - h += (stringTableSizePadded).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.addpos = 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(f'[ADDING] {name} {hex(size)} bytes to PFS0 at {hex(self.addpos)}', pleaseNoPrint) - self.files.append({'name': name, 'size': size, 'offset': self.addpos}) - self.addpos += size - return self - - def get(self, name): - return self - - def close(self): - pass - - #0xff => 0x1, 0x100 => 0x20, 0x1ff => 0x1, 0x120 => 0x20 - def allign0x20(self, n): - return 0x20-n%0x20 - - def getStringTableSize(self): - stringTableNonPadded = '\x00'.join(file['name'] for file in self.files)+'\x00' - headerSizeNonPadded = 0x10 + len(self.files) * 0x18 + len(stringTableNonPadded) - stringTableSizePadded = len(stringTableNonPadded) + self.allign0x20(headerSizeNonPadded) - if self._stringTableSize == None: - self._stringTableSize = stringTableSizePadded - elif len(stringTableNonPadded) > self._stringTableSize: - self._stringTableSize = len(stringTableNonPadded) - return self._stringTableSize - - def getHash(self): - hexHash = self.binhash.hexdigest() - return hexHash - - def updateHashHeader(self): - stringTableNonPadded = '\x00'.join(file['name'] for file in self.files)+'\x00' - stringTableSizePadded = self.getStringTableSize() - stringTable = stringTableNonPadded + ('\x00'*(stringTableSizePadded-len(stringTableNonPadded))) - headerSize = 0x10 + len(self.files) * 0x18 + stringTableSizePadded - - h = b'' - h += b'PFS0' - h += len(self.files).to_bytes(4, byteorder='little') - h += (stringTableSizePadded).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 - - if len(self.files) > 0: - if self.files[0]['offset'] - headerSize > 0: - stringTable += '\x00' * (self.files[0]['offset'] - headerSize) - h += stringTable.encode() - - headerHex = h.hex() - self.binhash.update(h) - - - -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 - - #0xff => 0x1, 0x100 => 0x20, 0x1ff => 0x1, 0x120 => 0x20 - def allign0x20(self, n): - return 0x20-n%0x20 - - def getPaddedHeaderSize(self): - return self._headerSize + self.allign0x20(self._headerSize); - - 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, meta_only=False): - r = super(Pfs0, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - 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 - Print.info(f'[OPEN ] {name} {hex(size)} bytes at {hex(offset)}') - - self.readInt32() # junk data - - if meta_only and not 'cnmt' in name: - continue - - f = 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 file in self.files: - if file != ticket: - try: - file.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/nstools/Fs/Rom.py b/py/nstools/Fs/Rom.py deleted file mode 100644 index 0a82c308..00000000 --- a/py/nstools/Fs/Rom.py +++ /dev/null @@ -1,57 +0,0 @@ -from binascii import hexlify as hx, unhexlify as uhx -from struct import pack as pk, unpack as upk - -import os -import re -import pathlib - -from nstools.nut import Keys -from nstools.nut import Print -from nstools.nut import Hex - -from .File import File -from .File import MemoryFile -from .BaseFs import BaseFs -from .Ivfc import Ivfc - - -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/nstools/Fs/Ticket.py b/py/nstools/Fs/Ticket.py deleted file mode 100644 index 2fa5e627..00000000 --- a/py/nstools/Fs/Ticket.py +++ /dev/null @@ -1,226 +0,0 @@ -from binascii import hexlify as hx, unhexlify as uhx - -from nstools.nut import Print -from nstools.nut import Keys - -from . import Type -from .File import File - - -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[Type.TicketSignature.RSA_4096_SHA1] = 0x200 - self.signatureSizes[Type.TicketSignature.RSA_2048_SHA1] = 0x100 - self.signatureSizes[Type.TicketSignature.ECDSA_SHA1] = 0x3C - self.signatureSizes[Type.TicketSignature.RSA_4096_SHA256] = 0x200 - self.signatureSizes[Type.TicketSignature.RSA_2048_SHA256] = 0x100 - self.signatureSizes[Type.TicketSignature.ECDSA_SHA256] = 0x3C - - def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, meta_only = False): - super(Ticket, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - self.rewind() - self.signatureType = self.readInt32() - try: - self.signatureType = 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/nstools/Fs/Type.py b/py/nstools/Fs/Type.py deleted file mode 100644 index ff48851a..00000000 --- a/py/nstools/Fs/Type.py +++ /dev/null @@ -1,30 +0,0 @@ -from enum import IntEnum - -class Content(IntEnum): - PROGRAM = 0x0 - META = 0x1 - CONTROL = 0x2 - MANUAL = 0x3 # HtmlDocument, LegalInformation - DATA = 0x4 # DeltaFragment - PUBLIC_DATA = 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/nstools/Fs/Xci.py b/py/nstools/Fs/Xci.py deleted file mode 100644 index c06f3c44..00000000 --- a/py/nstools/Fs/Xci.py +++ /dev/null @@ -1,355 +0,0 @@ -from binascii import hexlify as hx, unhexlify as uhx - -import os -import re - -from nstools.nut import Print - -from .File import File -from .File import BaseFile -from .Hfs0 import Hfs0 -from .Hfs0 import Hfs0Stream - - -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(f'[ADDING] {name} {hex(size)} bytes to XCI at {hex(self.f.tell())}', pleaseNoPrint) - partition = self.partition(self.f.tell(), size, n = BaseFile()) - self.files.append({'name': name, 'size': size, 'offset': self.f.tell(), 'partition': partition}) - self.addpos += size - return partition - - 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['partition'] - 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.headerOffset = 0x0 - - self.challengeResponseAuthData = None - self.challengeResponseAuthMac = None - self.challengeResponseAuthNonce = None - - self.titleKey1 = None - self.titleKey2 = None - - 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 isFullXci(self): - self.seek(0x100) - magic = self.read(0x4) - self.seek(0x0) - return magic != b'HEAD' - - def readKeyArea(self): - self.packageId = self.readInt64() - self.seek(0x10) - self.challengeResponseAuthData = self.read(0x10) - self.challengeResponseAuthMac = self.read(0x10) - self.challengeResponseAuthNonce = self.read(0x10) - self.seek(0x200) - self.titleKey1 = self.read(0x8) - self.titleKey2 = self.read(0x8) - - 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, meta_only=False): - r = super(Xci, self).open(path, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - if self.isFullXci(): - self.readKeyArea() - self.headerOffset = 0x1000 - self.seek(self.headerOffset) - self.readHeader() - self.seek(self.hfs0Offset + self.headerOffset) - self.hfs0 = Hfs0(None, cryptoKey = None) - self.partition(self.hfs0Offset + self.headerOffset, None, self.hfs0, cryptoKey = None, meta_only = meta_only) - - 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/nstools/Fs/__init__.py b/py/nstools/Fs/__init__.py deleted file mode 100644 index 38e0cd2c..00000000 --- a/py/nstools/Fs/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -def factory(name): - if name.suffix == '.xci' or name.suffix == '.xcz': - from .Xci import Xci - f = Xci() - elif name.suffix == '.nsp' or name.suffix == '.nsz' or name.suffix == '.nspz' or name.suffix == '.nsx': - from .Nsp import Nsp - f = Nsp() - elif name.suffix == '.nca': - from .Nca import Nca - f = Nca() - elif name.suffix == '.nacp': - from .Nacp import Nacp - f = Nacp() - elif name.suffix == '.tik': - from .Ticket import Ticket - f = Ticket() - elif name.suffix == '.cnmt': - from .Cnmt import Cnmt - f = Cnmt() - elif str(name) in set(['normal', 'logo', 'update', 'secure']): - from .Hfs0 import Hfs0 - f = Hfs0(None) - else: - from .File import File - f = File() - - return f diff --git a/py/nstools/lib/FsCert.py b/py/nstools/FsCert.py similarity index 98% rename from py/nstools/lib/FsCert.py rename to py/nstools/FsCert.py index bea73149..4b6f603b 100644 --- a/py/nstools/lib/FsCert.py +++ b/py/nstools/FsCert.py @@ -1,4 +1,4 @@ -import binascii +from binascii import a2b_base64 class PublicCert: def getPublic(ctype = None): @@ -86,5 +86,5 @@ def getPublic(ctype = None): } if ctype not in certlist: ctype = 'Tinfoil' - chain = bytearray(binascii.a2b_base64(certlist[ctype])) + chain = bytearray(a2b_base64(certlist[ctype])) return chain diff --git a/py/nstools/FsNcz.py b/py/nstools/FsNcz.py new file mode 100644 index 00000000..efbb2d27 --- /dev/null +++ b/py/nstools/FsNcz.py @@ -0,0 +1,74 @@ +from binascii import hexlify as hx, unhexlify as uhx + +from nsz.nut import Keys +from nsz.nut import Print + +from nsz.Fs import Type +from nsz.Fs.File import File +from nsz.Fs.Nca import NcaHeader as NczHeader + +class Ncz(File): + def __init__(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1): + self.header = None + self.sectionFilesystems = [] + self.sections = [] + super(Ncz, 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, meta_only=False): + super(Ncz, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) + + self.header = NczHeader() + self.partition(0x0, 0xC00, self.header, Type.Crypto.XTS, uhx(Keys.get('header_key'))) + self.header.seek(0x400) + + def masterKey(self): + return max(self.header.cryptoType, self.header.cryptoType2) + + def buildId(self): + if self.header.contentType != 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(Ncz, 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 == Type.Content.PROGRAM: + Print.info(tabs + 'build Id: ' + str(self.buildId())) diff --git a/py/nstools/lib/FsTools.py b/py/nstools/FsTools.py similarity index 98% rename from py/nstools/lib/FsTools.py rename to py/nstools/FsTools.py index 04afb7b9..0c43fead 100644 --- a/py/nstools/lib/FsTools.py +++ b/py/nstools/FsTools.py @@ -1,12 +1,12 @@ from binascii import hexlify as hx, unhexlify as uhx from hashlib import sha256, sha1 - from copy import copy -from . import FsNcaMod + +from .FsNcz import Ncz def get_ncz_data(src_nca): nca = copy(src_nca) - nca = FsNcaMod.Nca(nca) + nca = Ncz(nca) return nca def get_data_from_cnmt(nca): diff --git a/py/nstools/lib/NcaKeys.py b/py/nstools/NcaKeys.py similarity index 100% rename from py/nstools/lib/NcaKeys.py rename to py/nstools/NcaKeys.py diff --git a/py/nstools/lib/PathTools.py b/py/nstools/PathTools.py similarity index 99% rename from py/nstools/lib/PathTools.py rename to py/nstools/PathTools.py index 3bf905d9..073d359f 100644 --- a/py/nstools/lib/PathTools.py +++ b/py/nstools/PathTools.py @@ -13,7 +13,6 @@ def expandFiles(path): 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' diff --git a/py/nstools/lib/Verify.py b/py/nstools/Verify.py similarity index 96% rename from py/nstools/lib/Verify.py rename to py/nstools/Verify.py index e644eb49..1c5776f1 100644 --- a/py/nstools/lib/Verify.py +++ b/py/nstools/Verify.py @@ -1,29 +1,25 @@ +from os.path import basename as fsBasename, abspath as fsAbsPath from binascii import hexlify as hx, unhexlify as uhx -from hashlib import sha256, sha1 +from re import search as re_search +from hashlib import sha256 +from pathlib import Path -import os -import sys -import re +from zstandard import ZstdDecompressor +from enlighten import Counter as pb_Counter -from pathlib import Path +from nsz import Header, BlockDecompressorReader +from nsz.Fs import factory +from nsz.Fs import Xci, Nsp +from nsz.Fs import Nca, Ticket +from nsz.Fs import Type from . import FsTools from . import VerifyTools -from . import Header, BlockDecompressorReader from .FsCert import PublicCert -import zstandard -import enlighten - -from nstools.Fs import factory -from nstools.Fs import Xci, Nsp -from nstools.Fs import Nca, Ticket -from nstools.Fs import Type - - def parse_name(file: str): - res_id = re.search(r'(?P\[0100[A-F0-9]{12}\])', file) - res_ver = re.search(r'(?P\[v\d+\])', file) + res_id = re_search(r'(?P\[0100[A-F0-9]{12}\])', file) + res_ver = re_search(r'(?P\[v\d+\])', file) if res_id is None or res_ver is None: return None @@ -68,7 +64,7 @@ def verify(file: str, vlevel: int = 3): vlevel = 3 try: - filename = os.path.abspath(file) + filename = fsAbsPath(file) check = True vmsg = list() @@ -100,7 +96,7 @@ def verify(file: str, vlevel: int = 3): f.flush() f.close() - outlog = os.path.basename(file) + '\n' + outlog = fsBasename(file) + '\n' outlog += '\n'.join(vmsg) + '\n' return check, outlog @@ -192,7 +188,7 @@ def verify_decrypt(nspx, vmsg = None): vmsg.append(tvmsg) if f.header.contentType != Type.Content.PROGRAM: correct = VerifyTools.verify_enforcer(f) - if correct == True and f.header.contentType == Type.Content.PUBLIC_DATA and f.header.getRightsId() == 0: + if correct == True and f.header.contentType == Type.Content.PUBLICDATA and f.header.getRightsId() == 0: correct = VerifyTools.pr_noenc_check_dlc(f) if correct == False: bad_dec = True @@ -571,7 +567,7 @@ def verify_hash(nspx, headerlist, vmsg = None): counter = 0 mbDiv = 1048576 BAR_FMT = u'{desc}{desc_pad}{percentage:3.0f}%|{bar}| {count:{len_total}d}/{total:d} {unit} [{elapsed}<{eta}, {rate:.2f}{unit_pad}{unit}/s]' - bar = enlighten.Counter(total = nca_size//mbDiv, desc='Hashing', unit='MiB', color='red', bar_format=BAR_FMT) + bar = pb_Counter(total = nca_size//mbDiv, desc='Hashing', unit='MiB', color='red', bar_format=BAR_FMT) i = 0 f.rewind(); @@ -651,7 +647,7 @@ def verify_hash(nspx, headerlist, vmsg = None): counter = 0 mbDiv = 1048576 BAR_FMT = u'{desc}{desc_pad}{percentage:3.0f}%|{bar}| {count:{len_total}d}/{total:d} {unit} [{elapsed}<{eta}, {rate:.2f}{unit_pad}{unit}/s]' - bar = enlighten.Counter(total = nca_size//mbDiv, desc='Hashing', unit='MiB', color='red', bar_format=BAR_FMT) + bar = pb_Counter(total = nca_size//mbDiv, desc='Hashing', unit='MiB', color='red', bar_format=BAR_FMT) i = 0 f.rewind(); @@ -704,7 +700,7 @@ def verify_hash(nspx, headerlist, vmsg = None): pos = f.tell() if not useBlockCompression: - decompressor = zstandard.ZstdDecompressor().stream_reader(f) + decompressor = ZstdDecompressor().stream_reader(f) spsize = 0 diff --git a/py/nstools/lib/VerifyTools.py b/py/nstools/VerifyTools.py similarity index 97% rename from py/nstools/lib/VerifyTools.py rename to py/nstools/VerifyTools.py index 231c710e..1cdabd1b 100644 --- a/py/nstools/lib/VerifyTools.py +++ b/py/nstools/VerifyTools.py @@ -7,24 +7,20 @@ from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5, PKCS1_PSS -import io +from zstandard import ZstdDecompressor -from nstools.nut import Hex -from nstools.nut import Keys -from nstools.nut import aes128 +from nsz import Header, BlockDecompressorReader +from nsz.nut import Hex +from nsz.nut import Keys +from nsz.nut import aes128 +from nsz.Fs import Type +from nsz.Fs import File +from nsz.Fs import Nca +from nsz.Fs import Ticket from . import FsTools -from . import Header, BlockDecompressorReader from .NcaKeys import getNcaModulusKey -import zstandard - -from nstools.Fs import Type -from nstools.Fs import File -from nstools.Fs import Nca -from nstools.Fs import Ticket - - RSA_PUBLIC_EXPONENT = 0x10001 FS_HEADER_LENGTH = 0x200 UNCOMPRESSABLE_HEADER_SIZE = 0x4000 @@ -136,7 +132,7 @@ def verify_ncz(self, target): pos = f.tell() if not useBlockCompression: - decompressor = zstandard.ZstdDecompressor().stream_reader(f) + decompressor = ZstdDecompressor().stream_reader(f) count = 0 checkstarter = 0 diff --git a/py/nstools/lib/BlockDecompressorReader.py b/py/nstools/lib/BlockDecompressorReader.py deleted file mode 100644 index e4f35aeb..00000000 --- a/py/nstools/lib/BlockDecompressorReader.py +++ /dev/null @@ -1,65 +0,0 @@ -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[:-1]: - 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 % self.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/nstools/lib/FsNcaMod.py b/py/nstools/lib/FsNcaMod.py deleted file mode 100644 index 7377e84f..00000000 --- a/py/nstools/lib/FsNcaMod.py +++ /dev/null @@ -1,255 +0,0 @@ -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 nstools.nut import aes128 -from nstools.nut import Hex -from nstools.nut import Keys -from nstools.nut import Print -from nstools.nut import Titles - -from nstools.Fs import Type -from nstools.Fs.File import File - - -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 - - -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, meta_only=False): - super(NcaHeader, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - 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 = 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, meta_only=False): - super(Nca, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter, meta_only) - - self.header = NcaHeader() - self.partition(0x0, 0xC00, self.header, 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() - - def masterKey(self): - return max(self.header.cryptoType, self.header.cryptoType2) - - def buildId(self): - if self.header.contentType != 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 == Type.Content.PROGRAM: - Print.info(tabs + 'build Id: ' + str(self.buildId())) diff --git a/py/nstools/lib/Header.py b/py/nstools/lib/Header.py deleted file mode 100644 index 26c1b22b..00000000 --- a/py/nstools/lib/Header.py +++ /dev/null @@ -1,27 +0,0 @@ -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/nstools/nut/Hex.py b/py/nstools/nut/Hex.py deleted file mode 100644 index 3e865fe3..00000000 --- a/py/nstools/nut/Hex.py +++ /dev/null @@ -1,40 +0,0 @@ -from string import ascii_letters, digits, punctuation -from . 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/nstools/nut/Keys.py b/py/nstools/nut/Keys.py deleted file mode 100644 index c296e364..00000000 --- a/py/nstools/nut/Keys.py +++ /dev/null @@ -1,204 +0,0 @@ -import os, sys, re -from traceback import format_exc -from binascii import crc32, hexlify as hx, unhexlify as uhx -from pathlib import Path -from multiprocessing.process import current_process - -from . import aes128 -from . import Print - -keys = {} -titleKeks = [] -keyAreaKeys = [] -loadedKeysFile = "non-existing prod.keys/keys.txt" -keys_loaded = False - -#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, - 'master_key_10': 788455323, - 'master_key_11': 1214507020, - 'master_key_12': 1051942134, - 'master_key_13': 2476807835, - 'master_key_14': 2448653557, - 'master_key_15': 4071812001 -} - -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 - global keys_loaded - 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) - - keys_loaded = True - return keys_loaded - except BaseException as e: - Print.error(format_exc()) - Print.error(str(e)) - - keys_loaded = False - return keys_loaded - -def load_default(): - keyScriptPath = Path(sys.argv[0]) - while not keyScriptPath.is_dir(): - keyScriptPath = keyScriptPath.parents[0] - - keyfiles = [ - Path.home().joinpath(".switch", "prod.keys"), - Path.home().joinpath(".switch", "keys.txt"), - Path("/sdcard/Documents/prod.keys"), - Path("/sdcard/Documents/keys.txt"), - keyScriptPath.joinpath("prod.keys"), - keyScriptPath.joinpath("keys.txt"), - Path(os.environ.get("NSTOOLS_KEYS_FILE", "$NSTOOLS_KEYS_FILE")), - ] - - keys_loaded = False - for kf in keyfiles: - if kf.is_file(): - keys_loaded = load(str(kf)) - if keys_loaded == True: - break - - if keys_loaded == False: - errorMsg = "" - for kf in keyfiles: - if errorMsg != "": - errorMsg += "\nor " - errorMsg += f"{str(kf)}" - errorMsg += " not found\n\nPlease dump your keys using https://github.com/shchmue/Lockpick_RCM/releases\n" - errorMsg = "Failed to load default keys files:\n" + errorMsg - Print.error(errorMsg) - return keys_loaded diff --git a/py/nstools/nut/Print.py b/py/nstools/nut/Print.py deleted file mode 100644 index f8d1d77f..00000000 --- a/py/nstools/nut/Print.py +++ /dev/null @@ -1,45 +0,0 @@ -import sys -import time - -global silent -enableInfo = True -enableError = True -enableWarning = True -enableDebug = False - -silent = False - -def info(s, pleaseNoPrint = None): - if silent or not enableInfo: - return - - 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): - if silent or not enableInfo: - return - sys.stdout.write(s) - -def error(s): - if silent or not enableError: - return - sys.stdout.write(s + "\n") - -def warning(s): - if silent or not enableWarning: - return - sys.stdout.write(s + "\n") - -def debug(s): - if silent or not enableWarning: - return - sys.stdout.write(s + "\n") diff --git a/py/nstools/nut/Titles.py b/py/nstools/nut/Titles.py deleted file mode 100644 index ddaed502..00000000 --- a/py/nstools/nut/Titles.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import os -import re -import time -import json -import operator - -from . 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/nstools/nut/aes128.py b/py/nstools/nut/aes128.py deleted file mode 100644 index 76120d95..00000000 --- a/py/nstools/nut/aes128.py +++ /dev/null @@ -1,428 +0,0 @@ -# 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/requirements.txt b/py/requirements.txt index 736af394..5fd2686e 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -1,4 +1,5 @@ +git+https://github.com/nicoboss/nsz.git@2aac384 zstandard enlighten -requests -pycryptodome +requests>=2.32.3 # CVE-2024-35195 fixed in 2.32.0+ +pycryptodome>=3.21.0 # CVE-2023-52323 fixed in 3.20.0+