diff --git a/.gitignore b/.gitignore index 258a3b4..a04272c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ *.iso *.ird *.gz -ird/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 6cf811e..0fe6d4a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ extracting, repackaging, and encrypting PS3 ISOs. A hackable, crossplatform, alternative to ISOTools and ISO-Rebuilder. + [see also](http://www.psdevwiki.com/ps3/Bluray_disc#Encryption) ([archive.fo](https://archive.fo/hN1E6)) [7bit encoded int / RLE / CLP](https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/io/binaryreader.cs#L582-L600) @@ -16,8 +17,16 @@ A hackable, crossplatform, alternative to ISOTools and ISO-Rebuilder. clp = compressed length prefix -## Possibly todo +## Todo +- Automatically download .ird file if not given - Docstrings - Extract ISO (currently doable with `7z x output.iso` +- Test .irds with version < 9 +- Custom command to backup all irds available + +## Advanced + +Figure out the SCSI commands to get data1, if at all possible. + diff --git a/libray/core.py b/libray/core.py index 14f38ae..6067943 100644 --- a/libray/core.py +++ b/libray/core.py @@ -21,6 +21,8 @@ import os import sys import shutil +import requests +from bs4 import BeautifulSoup try: @@ -31,6 +33,8 @@ except ImportError: # Magic numbers / Constant variables SECTOR = 2048 +ALL_IRD_NET_LOC = 'http://jonnysp.bplaced.net/data.php' +GET_IRD_NET_LOC = 'http://jonnysp.bplaced.net/ird/' # Utility functions @@ -76,6 +80,35 @@ def error(msg): def warning(msg): print('WARNING: %s. Continuing regardless' % msg) + +def download_ird(ird_name): + ird_link = GET_IRD_NET_LOC + ird_name + r = requests.get(ird_link, stream=True) + + with open(ird_name, 'wb') as ird_file: + r.raw.decode_content = True + shutil.copyfileobj(r.raw, ird_file) + + +def ird_by_game_id(game_id): + gameid = game_id.replace('-','') + r = requests.get(ALL_IRD_NET_LOC, headers = {'User-Agent': 'Anonymous (You)' }, timeout=5) + soup = BeautifulSoup(r.text, "html.parser") + + ird_name = False + for elem in soup.find_all("a"): + url = elem.get('href').split('/')[-1].replace('\\"','') + if gameid in url: + ird_name = url + + if not ird_name: + error("Unable to download IRD, couldn't find link") + + download_ird(ird_name) + + return(ird_name) + + # Main functions diff --git a/libray/ird.py b/libray/ird.py new file mode 100644 index 0000000..e357222 --- /dev/null +++ b/libray/ird.py @@ -0,0 +1,137 @@ +# -*- coding: utf8 -*- + +# libray - Libre Blu-Ray PS3 ISO Tool +# Copyright (C) 2018 Nichlas Severinsen +# +# This file is part of libray. +# +# libray is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# libray is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with libray. If not, see . + + +import os +import sys +import zlib +import shutil + +try: + from libray import core +except ImportError: + import core + + +class IRD: + + + ORDER = 'little' + TEMP_FILE = 'ird' + MAGIC_STRING = b"3IRD" + + + def __init__(self, args): + + self.uncompress(args.ird) # TODO: Try/Except + + self.size = core.filesize(self.TEMP_FILE) + with open(self.TEMP_FILE, 'rb') as input_ird: + if input_ird.read(4) != self.MAGIC_STRING: + core.error("Either not an IRD file, corruped IRD file, or unknown IRD format") + + self.version = core.to_int(input_ird.read(1), self.ORDER) + self.game_id = input_ird.read(9) + name_length = core.read_seven_bit_encoded_int(input_ird, self.ORDER) + self.game_name = input_ird.read(name_length).decode('utf8') + self.update_version = input_ird.read(4) + self.game_version = input_ird.read(5) + self.app_version = input_ird.read(5) + + if self.version == 7: + self.identifier = input_ird.read(4) + + header_length = (core.to_int(input_ird.read(4), self.ORDER)) + self.header = input_ird.read(header_length) + footer_length = (core.to_int(input_ird.read(4), self.ORDER)) + self.footer = input_ird.read(footer_length) + + self.region_count = core.to_int(input_ird.read(1), self.ORDER) + self.region_hashes = [] + for i 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): + key = core.to_int(input_ird.read(8), self.ORDER) + val = input_ird.read(16) + self.file_hashes.append({'key': key, 'val': val}) + + if self.version >= 9: + self.pic = input_ird.read(115) + + unused_bytes = input_ird.read(4) # Yeah, I don't know either. + + self.data1 = input_ird.read(16) + self.data2 = input_ird.read(16) + + if self.version < 9: + self.pic = input_ird.read(115) + + if self.version < 7: + self.uid = core.to_int(input_ird.read(4), self.ORDER) + + if args.verbose: + self.print_info() + + os.remove(self.TEMP_FILE) + + + def get_if_exists(self, input_ird): + starting_address = input_ird.tell() + length = core.read_seven_bit_encoded_int(input_ird, self.ORDER) + print(length) + if length: + return input_ird.read(length) + + input_ird.seek(starting_address) + return None + + + def uncompress(self, filename): + uncompress = False + with open(filename, 'rb') as input_ird: + if input_ird.read(4) != self.MAGIC_STRING: + uncompress = True + + if uncompress: + with open(filename, 'rb') as gzfile: + with open(self.TEMP_FILE, 'wb') as tmpfile: + tmpfile.write(zlib.decompress(gzfile.read(), zlib.MAX_WBITS|16)) + else: + shutil.copyfile(filename, self.TEMP_FILE) + + + + def print_info(self): + print('Info from IRD:') + print('Version: %s' % self.version) + print('Game ID: %s' % self.game_id) + print('Game Name: %s' % self.game_name) + print('Update Version: %s' % self.update_version) + print('Game Version: %s' % self.game_version) + print('App Version: %s' % self.app_version) + print('Region Count: %s' % self.region_count) + print('File Count: %s' % self.file_count) + print('Data1: %s' % self.data1.hex()) + print('Data2: %s' % self.data2.hex()) + + diff --git a/libray/iso.py b/libray/iso.py index 6bcb01c..29f3837 100644 --- a/libray/iso.py +++ b/libray/iso.py @@ -25,8 +25,10 @@ from Crypto.Cipher import AES try: from libray import core + from libray import ird except ImportError: import core + import ird class ISO: @@ -43,10 +45,6 @@ class ISO: self.regions = self.read_regions(input_iso, args.iso) - input_iso.seek(3968) - self.data1 = input_iso.read(16) - - input_iso.seek(core.SECTOR) playstation = input_iso.read(16) self.game_id = input_iso.read(16).decode('utf8').strip() @@ -54,11 +52,20 @@ class ISO: if args.verbose: 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.data1) + self.disc_key = cipher.encrypt(self.ird.data1) def decrypt(self, args): diff --git a/libray/libray.py b/libray/libray.py index 5665ae3..89cc4cb 100755 --- a/libray/libray.py +++ b/libray/libray.py @@ -19,7 +19,7 @@ # You should have received a copy of the GNU General Public License # along with libray. If not, see . -import sys + import argparse try: @@ -34,13 +34,11 @@ if __name__ == '__main__': parser = argparse.ArgumentParser(description='A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs') parser.add_argument('-v', '--verbose', help="Increase verbosity", action='count') parser.add_argument('-o', '--output', dest='output', type=str, help="Output filename", default='output.iso') + parser.add_argument('-k', '--ird', dest='ird', type=str, help="Path to .ird file", default="") required = parser.add_argument_group('required arguments') required.add_argument('-i', '--iso', dest='iso', type=str, help="Path to .iso file", required=True) args = parser.parse_args() - if args.iso: - core.decrypt(args) - sys.exit() + core.decrypt(args) - print("Not enough arguments given. See --help") \ No newline at end of file