Add -d and -q
This commit is contained in:
parent
d229015710
commit
0fe0b1d05a
6 changed files with 96 additions and 68 deletions
|
|
@ -6,10 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Changed
|
### Changed
|
||||||
- Default output iso name is game_id.iso instead of output.iso
|
- 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
|
## [0.0.2] - 2019-07-07
|
||||||
### Added/Fixed
|
### 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
|
## [0.0.1] - 2019-05-16
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ This will essentially automatically do the manual method for you.
|
||||||
## How do I use it?
|
## 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,
|
A Libre (FLOSS) Python application for unencrypting, extracting, repackaging,
|
||||||
and encrypting PS3 ISOs
|
and encrypting PS3 ISOs
|
||||||
|
|
@ -54,8 +54,10 @@ optional arguments:
|
||||||
-o OUTPUT, --output OUTPUT
|
-o OUTPUT, --output OUTPUT
|
||||||
Output filename
|
Output filename
|
||||||
-k IRD, --ird IRD Path to .ird file
|
-k IRD, --ird IRD Path to .ird file
|
||||||
|
-d DECRYPTION_KEY, --decryption-key DECRYPTION_KEY
|
||||||
|
Manually specify key
|
||||||
-v, --verbose Increase verbosity
|
-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.
|
First off, even before you install libray, you will need a compatible Blu-Ray drive that can read PS3 discs.
|
||||||
|
|
|
||||||
|
|
@ -66,14 +66,14 @@ def size(path):
|
||||||
if stat.S_ISBLK(pathstat.st_mode):
|
if stat.S_ISBLK(pathstat.st_mode):
|
||||||
return open(path, 'rb').seek(0, os.SEEK_END)
|
return open(path, 'rb').seek(0, os.SEEK_END)
|
||||||
|
|
||||||
# Otherwise, it's hopefully file
|
# Otherwise, it's hopefully a file
|
||||||
|
|
||||||
return pathstat.st_size
|
return pathstat.st_size
|
||||||
|
|
||||||
|
|
||||||
def read_seven_bit_encoded_int(fileobj, order):
|
def read_seven_bit_encoded_int(fileobj, order):
|
||||||
"""Read an Int32, 7 bits at a time."""
|
"""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
|
count = 0
|
||||||
shift = 0
|
shift = 0
|
||||||
byte = -1
|
byte = -1
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class IRD:
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
"""IRD constructor using args from argparse"""
|
"""IRD constructor using args from argparse."""
|
||||||
|
|
||||||
self.uncompress(args.ird) # TODO: Try/Except?
|
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_count = core.to_int(input_ird.read(1), self.ORDER)
|
||||||
self.region_hashes = []
|
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.region_hashes.append(input_ird.read(16))
|
||||||
|
|
||||||
self.file_count = core.to_int(input_ird.read(4), self.ORDER)
|
self.file_count = core.to_int(input_ird.read(4), self.ORDER)
|
||||||
self.file_hashes = []
|
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)
|
key = core.to_int(input_ird.read(8), self.ORDER)
|
||||||
val = input_ird.read(16)
|
val = input_ird.read(16)
|
||||||
self.file_hashes.append({'key': key, 'val': val})
|
self.file_hashes.append({'key': key, 'val': val})
|
||||||
|
|
@ -96,7 +96,7 @@ class IRD:
|
||||||
if self.version >= 9:
|
if self.version >= 9:
|
||||||
self.pic = input_ird.read(115)
|
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.data1 = input_ird.read(16)
|
||||||
self.data2 = input_ird.read(16)
|
self.data2 = input_ird.read(16)
|
||||||
|
|
@ -114,7 +114,7 @@ class IRD:
|
||||||
|
|
||||||
|
|
||||||
def uncompress(self, filename):
|
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
|
uncompress = False
|
||||||
with open(filename, 'rb') as input_ird:
|
with open(filename, 'rb') as input_ird:
|
||||||
|
|
@ -131,7 +131,7 @@ class IRD:
|
||||||
|
|
||||||
def print_info(self):
|
def print_info(self):
|
||||||
# TODO: This could probably have been a __str__? Who cares?
|
# 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('Info from IRD:')
|
||||||
print('Version: %s' % self.version)
|
print('Version: %s' % self.version)
|
||||||
|
|
|
||||||
127
libray/iso.py
127
libray/iso.py
|
|
@ -33,22 +33,49 @@ except ImportError:
|
||||||
|
|
||||||
|
|
||||||
class ISO:
|
class ISO:
|
||||||
"""Class for handling PS3 .iso files
|
"""Class for handling PS3 .iso files.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
size: Size of .iso in bytes
|
size: Size of .iso in bytes
|
||||||
number_of_regions: Number of regions in the .iso
|
number_of_regions: Number of regions in the .iso
|
||||||
regions: List with info of every region
|
regions: List with info of every region
|
||||||
game_id: PS3 game id
|
game_id: PS3 game id
|
||||||
ird: IRD object (see ird.py)
|
ird: IRD object (see ird.py)
|
||||||
disc_key: data1 from .ird, encrypted
|
disc_key: data1 from .ird, encrypted
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
NUM_INFO_BYTES = 4
|
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):
|
def __init__(self, args):
|
||||||
"""ISO constructor using args from argparse"""
|
"""ISO constructor using args from argparse."""
|
||||||
|
|
||||||
self.size = core.size(args.iso)
|
self.size = core.size(args.iso)
|
||||||
|
|
||||||
|
|
@ -56,38 +83,48 @@ class ISO:
|
||||||
core.error('looks like ISO file/mount is empty?')
|
core.error('looks like ISO file/mount is empty?')
|
||||||
|
|
||||||
with open(args.iso, 'rb') as input_iso:
|
with open(args.iso, 'rb') as input_iso:
|
||||||
self.number_of_regions = core.to_int(input_iso.read(self.NUM_INFO_BYTES))
|
# Get number of regions (times two as the number represents both encrypted and decrypted regions )
|
||||||
unused_bytes = input_iso.read(self.NUM_INFO_BYTES) # Yeah, I don't know either.
|
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)
|
self.regions = self.read_regions(input_iso, args.iso)
|
||||||
|
|
||||||
input_iso.seek(core.SECTOR)
|
# Seek to the start of region 2, '+ 16' skips a section containing some 'playstation'
|
||||||
playstation = input_iso.read(16)
|
input_iso.seek(core.SECTOR + 16)
|
||||||
|
|
||||||
self.game_id = input_iso.read(16).decode('utf8').strip()
|
self.game_id = input_iso.read(16).decode('utf8').strip()
|
||||||
|
|
||||||
if args.verbose:
|
if args.verbose and not args.quiet:
|
||||||
self.print_info()
|
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)
|
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):
|
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:
|
with open(args.iso, 'rb') as input_iso:
|
||||||
|
|
||||||
|
|
@ -100,18 +137,19 @@ class ISO:
|
||||||
|
|
||||||
pbar = tqdm(total= (self.size // 2048) - 4 )
|
pbar = tqdm(total= (self.size // 2048) - 4 )
|
||||||
|
|
||||||
for i, region in enumerate(self.regions):
|
for region in self.regions:
|
||||||
input_iso.seek(region['start'])
|
input_iso.seek(region['start'])
|
||||||
|
|
||||||
|
# Unencrypted region, just copy it
|
||||||
if not region['enc']:
|
if not region['enc']:
|
||||||
while input_iso.tell() < region['end']:
|
while input_iso.tell() < region['end']:
|
||||||
data = input_iso.read(core.SECTOR)
|
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')
|
core.warning('Trying to read past the end of the file')
|
||||||
break
|
break
|
||||||
pbar.update(1)
|
|
||||||
output_iso.write(data)
|
output_iso.write(data)
|
||||||
continue
|
continue
|
||||||
|
# Encrypted region, decrypt then write
|
||||||
else:
|
else:
|
||||||
while input_iso.tell() < region['end']:
|
while input_iso.tell() < region['end']:
|
||||||
num = input_iso.tell() // 2048
|
num = input_iso.tell() // 2048
|
||||||
|
|
@ -121,43 +159,26 @@ class ISO:
|
||||||
num >>= 8
|
num >>= 8
|
||||||
|
|
||||||
data = input_iso.read(core.SECTOR)
|
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')
|
core.warning('Trying to read past the end of the file')
|
||||||
break
|
break
|
||||||
pbar.update(1)
|
|
||||||
|
|
||||||
cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv))
|
cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv))
|
||||||
decrypted = cipher.decrypt(data)
|
decrypted = cipher.decrypt(data)
|
||||||
|
|
||||||
output_iso.write(decrypted)
|
output_iso.write(decrypted)
|
||||||
|
|
||||||
pbar.close()
|
pbar.update(1)
|
||||||
|
|
||||||
|
pbar.close()
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def print_info(self):
|
def print_info(self):
|
||||||
# TODO: This could probably have been a __str__? Who cares?
|
# 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('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):
|
for i, region in enumerate(self.regions):
|
||||||
print(i, region)
|
print(i, region)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,19 @@ except ImportError:
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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 = argparse.ArgumentParser(description='A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs')
|
||||||
parser._action_groups.pop()
|
parser._action_groups.pop()
|
||||||
|
|
||||||
required = parser.add_argument_group('required arguments')
|
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)
|
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('-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('-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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
core.decrypt(args)
|
core.decrypt(args)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue