diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b58e39..a6f9db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Changed - Default output iso name is game_id.iso instead of output.iso +- Added quiet mode, enabled with the -q or --quiet flag argument +- Added the ability to manually specify decryption key with -d or --decryption-key ## [0.0.2] - 2019-07-07 ### Added/Fixed -- Decrypting block devices directly (eg. cd/dvd/bd drive) instead of .iso files. For example `-i /dev/sg0` or `-i /dev/sr0`. +- Decrypting block devices directly (eg. cd/dvd/bd drive) instead of .iso files. For example `-i /dev/sg0` or `-i /dev/sr0` ## [0.0.1] - 2019-05-16 ### Added diff --git a/README.md b/README.md index e202d69..0e65aa0 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ This will essentially automatically do the manual method for you. ## How do I use it? ``` -usage: libray [-h] -i ISO [-o OUTPUT] [-k IRD] [-v] +usage: libray [-h] -i ISO [-o OUTPUT] [-k IRD] [-d DECRYPTION_KEY] [-v] [-q] A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs @@ -54,8 +54,10 @@ optional arguments: -o OUTPUT, --output OUTPUT Output filename -k IRD, --ird IRD Path to .ird file + -d DECRYPTION_KEY, --decryption-key DECRYPTION_KEY + Manually specify key -v, --verbose Increase verbosity - + -q, --quiet Quiet mode, only prints on error ``` First off, even before you install libray, you will need a compatible Blu-Ray drive that can read PS3 discs. diff --git a/libray/core.py b/libray/core.py index 0d2d3f4..80616c7 100644 --- a/libray/core.py +++ b/libray/core.py @@ -66,14 +66,14 @@ def size(path): if stat.S_ISBLK(pathstat.st_mode): return open(path, 'rb').seek(0, os.SEEK_END) - # Otherwise, it's hopefully file + # Otherwise, it's hopefully a file return pathstat.st_size def read_seven_bit_encoded_int(fileobj, order): """Read an Int32, 7 bits at a time.""" - # The highest bit of the byte when on, means to continue reading more bytes. + # The highest bit of the byte, when on, means to continue reading more bytes. count = 0 shift = 0 byte = -1 diff --git a/libray/ird.py b/libray/ird.py index 7361c69..bc3f36c 100644 --- a/libray/ird.py +++ b/libray/ird.py @@ -52,7 +52,7 @@ class IRD: def __init__(self, args): - """IRD constructor using args from argparse""" + """IRD constructor using args from argparse.""" self.uncompress(args.ird) # TODO: Try/Except? @@ -83,12 +83,12 @@ class IRD: self.region_count = core.to_int(input_ird.read(1), self.ORDER) self.region_hashes = [] - for i in range(0, self.region_count): + for _ in range(0, self.region_count): self.region_hashes.append(input_ird.read(16)) self.file_count = core.to_int(input_ird.read(4), self.ORDER) self.file_hashes = [] - for i in range(0, self.file_count): + for _ in range(0, self.file_count): key = core.to_int(input_ird.read(8), self.ORDER) val = input_ird.read(16) self.file_hashes.append({'key': key, 'val': val}) @@ -96,7 +96,7 @@ class IRD: if self.version >= 9: self.pic = input_ird.read(115) - unused_bytes = input_ird.read(4) # Yeah, I don't know either. + input_ird.seek(input_ird.tell() + 4) # ? self.data1 = input_ird.read(16) self.data2 = input_ird.read(16) @@ -114,7 +114,7 @@ class IRD: def uncompress(self, filename): - """Uncompress IRD. Assumes given .ird file is not compressed, but then tries to decompress it with zlib/gzfile if it was not uncompressed""" + """Uncompress IRD. Assumes given .ird file is not compressed, but then tries to decompress it with zlib/gzfile if it was not uncompressed.""" uncompress = False with open(filename, 'rb') as input_ird: @@ -131,7 +131,7 @@ class IRD: def print_info(self): # TODO: This could probably have been a __str__? Who cares? - """Print some info about the IRD""" + """Print some info about the IRD.""" print('Info from IRD:') print('Version: %s' % self.version) diff --git a/libray/iso.py b/libray/iso.py index c140311..365f64f 100644 --- a/libray/iso.py +++ b/libray/iso.py @@ -33,22 +33,49 @@ except ImportError: class ISO: - """Class for handling PS3 .iso files + """Class for handling PS3 .iso files. Attributes: - size: Size of .iso in bytes - number_of_regions: Number of regions in the .iso - regions: List with info of every region - game_id: PS3 game id - ird: IRD object (see ird.py) - disc_key: data1 from .ird, encrypted + size: Size of .iso in bytes + number_of_regions: Number of regions in the .iso + regions: List with info of every region + game_id: PS3 game id + ird: IRD object (see ird.py) + disc_key: data1 from .ird, encrypted """ + NUM_INFO_BYTES = 4 + def read_regions(self, input_iso, filename): + """List with info dict (start, end, whether it's encrypted) for every region. + + Basically, every other (odd numbered) region is encrypted. + """ + regions = [] + + encrypted = False + for _ in range(0, self.number_of_regions): + + regions.append({ + 'start': core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR, + 'end': core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR, + 'enc': encrypted + }) + + input_iso.seek(input_iso.tell() - self.NUM_INFO_BYTES) + + encrypted = not encrypted + + # Last region might not actually be 2048 bytes, so we'll just cheat + regions[-1]['end'] = self.size + + return regions + + def __init__(self, args): - """ISO constructor using args from argparse""" + """ISO constructor using args from argparse.""" self.size = core.size(args.iso) @@ -56,38 +83,48 @@ class ISO: core.error('looks like ISO file/mount is empty?') with open(args.iso, 'rb') as input_iso: - self.number_of_regions = core.to_int(input_iso.read(self.NUM_INFO_BYTES)) - unused_bytes = input_iso.read(self.NUM_INFO_BYTES) # Yeah, I don't know either. + # Get number of regions (times two as the number represents both encrypted and decrypted regions ) + self.number_of_regions = core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * 2 + + # Skip unused bytes + input_iso.seek(input_iso.tell() + self.NUM_INFO_BYTES) self.regions = self.read_regions(input_iso, args.iso) - input_iso.seek(core.SECTOR) - playstation = input_iso.read(16) + # Seek to the start of region 2, '+ 16' skips a section containing some 'playstation' + input_iso.seek(core.SECTOR + 16) + self.game_id = input_iso.read(16).decode('utf8').strip() - if args.verbose: + if args.verbose and not args.quiet: self.print_info() - if not args.ird: - core.warning('No IRD file specified, downloading required file') - args.ird = core.ird_by_game_id(self.game_id) # Download ird - - self.ird = ird.IRD(args) - - if self.ird.region_count != len(self.regions)-1: - core.error('Corrupt ISO. Expected %s regions, found %s regions' % (self.ird.region_count, len(self.regions)-1)) - - if self.regions[-1]['start'] > self.size: - core.error('Corrupt ISO. Expected filesize larger than %.2f GiB, actual size is %.2f GiB' % (self.regions[-1]['start'] / 1024**3, self.size / 1024**3 ) ) - cipher = AES.new(core.ISO_SECRET, AES.MODE_CBC, core.ISO_IV) - self.disc_key = cipher.encrypt(self.ird.data1) + + if not args.decryption_key: + if not args.ird: + if not args.quiet: + core.warning('No IRD file specified, downloading required file') + args.ird = core.ird_by_game_id(self.game_id) # Download ird + + self.ird = ird.IRD(args) + + if self.ird.region_count != len(self.regions)-1: + core.error('Corrupt ISO or error in IRD. Expected %s regions, found %s regions' % (self.ird.region_count, len(self.regions)-1)) + + if self.regions[-1]['start'] > self.size: + core.error('Corrupt ISO or error in IRD. Expected filesize larger than %.2f GiB, actual size is %.2f GiB' % (self.regions[-1]['start'] / 1024**3, self.size / 1024**3 ) ) + + self.disc_key = cipher.encrypt(self.ird.data1) + else: + self.disc_key = cipher.encrypt(core.to_bytes(args.decryption_key)) def decrypt(self, args): - """Decrypt self using args from argparse""" + """Decrypt self using args from argparse.""" - print('Decrypting with disc key: %s' % self.disc_key.hex()) + if not args.quiet: + print('Decrypting with disc key: %s' % self.disc_key.hex()) with open(args.iso, 'rb') as input_iso: @@ -100,18 +137,19 @@ class ISO: pbar = tqdm(total= (self.size // 2048) - 4 ) - for i, region in enumerate(self.regions): + for region in self.regions: input_iso.seek(region['start']) + # Unencrypted region, just copy it if not region['enc']: while input_iso.tell() < region['end']: data = input_iso.read(core.SECTOR) - if not data: + if not data and not args.quiet: core.warning('Trying to read past the end of the file') break - pbar.update(1) output_iso.write(data) continue + # Encrypted region, decrypt then write else: while input_iso.tell() < region['end']: num = input_iso.tell() // 2048 @@ -121,43 +159,26 @@ class ISO: num >>= 8 data = input_iso.read(core.SECTOR) - if not data: + if not data and not args.quiet: core.warning('Trying to read past the end of the file') break - pbar.update(1) cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv)) decrypted = cipher.decrypt(data) output_iso.write(decrypted) - pbar.close() + pbar.update(1) - - def read_regions(self, input_iso, filename): - """List with information (start, end, whether it's encrypted) for every region""" - regions = [] - - encrypted = False - for i in range(0, self.number_of_regions*2): - regions.append({ - 'start': core.to_int(input_iso.read(self.NUM_INFO_BYTES))*core.SECTOR, - 'end': core.to_int(input_iso.read(self.NUM_INFO_BYTES))*core.SECTOR, - 'enc': encrypted - }) - input_iso.seek(input_iso.tell() - self.NUM_INFO_BYTES) - encrypted = not encrypted - regions[-1]['end'] = self.size - - return regions + pbar.close() def print_info(self): # TODO: This could probably have been a __str__? Who cares? - """Print some info about the ISO""" + """Print some info about the ISO.""" print('Info from ISO:') - print('Regions: %s (%s)' % (self.number_of_regions, self.number_of_regions*2) ) + print('Regions: %s' % self.number_of_regions) for i, region in enumerate(self.regions): print(i, region) diff --git a/libray/libray.py b/libray/libray.py index ad359af..a4a4b1e 100755 --- a/libray/libray.py +++ b/libray/libray.py @@ -31,16 +31,19 @@ except ImportError: if __name__ == '__main__': - # Parse command line arguments with argpase parser = argparse.ArgumentParser(description='A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs') parser._action_groups.pop() + required = parser.add_argument_group('required arguments') - optional = parser.add_argument_group('optional arguments') required.add_argument('-i', '--iso', dest='iso', type=str, help='Path to .iso file or stream', required=True) + + optional = parser.add_argument_group('optional arguments') optional.add_argument('-o', '--output', dest='output', type=str, help='Output filename', default='') optional.add_argument('-k', '--ird', dest='ird', type=str, help='Path to .ird file', default='') - optional.add_argument('-v', '--verbose', help='Increase verbosity', action='count') + optional.add_argument('-d', '--decryption-key', dest='decryption_key', type=str, help='Manually specify key', default='') + optional.add_argument('-v', '--verbose', dest='verbose', help='Increase verbosity', action='count') + optional.add_argument('-q', '--quiet', dest='quiet', help='Quiet mode, only prints on error', action='store_true') + args = parser.parse_args() core.decrypt(args) -