Push 0.10.0
This commit is contained in:
parent
266f3fc76d
commit
12c05afa57
25 changed files with 2016 additions and 645 deletions
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf8 -*-
|
||||
|
||||
# libray - Libre Blu-Ray PS3 ISO Tool
|
||||
# Copyright © 2018 - 2021 Nichlas Severinsen
|
||||
# Copyright © 2018 - 2024 Nichlas Severinsen
|
||||
#
|
||||
# This file is part of libray.
|
||||
#
|
||||
|
|
@ -28,5 +28,8 @@ import pkgutil
|
|||
__all__ = []
|
||||
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
|
||||
__all__.append(module_name)
|
||||
_module = loader.find_module(module_name).load_module(module_name)
|
||||
try:
|
||||
_module = loader.find_module(module_name).load_module(module_name)
|
||||
except AttributeError:
|
||||
_module = loader.find_spec(module_name).loader.load_module(module_name)
|
||||
globals()[module_name] = _module
|
||||
|
|
|
|||
237
libray/core.py
237
libray/core.py
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf8 -*-
|
||||
|
||||
# libray - Libre Blu-Ray PS3 ISO Tool
|
||||
# Copyright © 2018 - 2021 Nichlas Severinsen
|
||||
# Copyright © 2018 - 2024 Nichlas Severinsen
|
||||
#
|
||||
# This file is part of libray.
|
||||
#
|
||||
|
|
@ -29,9 +29,11 @@ from bs4 import BeautifulSoup
|
|||
|
||||
|
||||
try:
|
||||
from libray import iso
|
||||
from libray import iso
|
||||
from libray import ird
|
||||
except ImportError:
|
||||
import iso
|
||||
import iso
|
||||
import ird
|
||||
|
||||
# Magic numbers / Constant variables
|
||||
|
||||
|
|
@ -43,15 +45,15 @@ GET_IRD_NET_LOC = 'http://jonnysp.bplaced.net/ird/'
|
|||
|
||||
|
||||
def to_int(data, byteorder='big'):
|
||||
"""Convert bytes to integer"""
|
||||
if isinstance(data, bytes):
|
||||
return int.from_bytes(data, byteorder)
|
||||
"""Convert bytes to integer"""
|
||||
if isinstance(data, bytes):
|
||||
return int.from_bytes(data, byteorder)
|
||||
|
||||
|
||||
def to_bytes(data):
|
||||
"""Convert a string of HEX to bytes"""
|
||||
if isinstance(data, str):
|
||||
return bytes(bytearray.fromhex(data))
|
||||
"""Convert a string of HEX to bytes"""
|
||||
if isinstance(data, str):
|
||||
return bytes(bytearray.fromhex(data))
|
||||
|
||||
|
||||
ISO_SECRET = to_bytes("380bcf0b53455b3c7817ab4fa3ba90ed")
|
||||
|
|
@ -59,166 +61,183 @@ ISO_IV = to_bytes("69474772af6fdab342743aefaa186287")
|
|||
|
||||
|
||||
def size(path):
|
||||
"""Get size of a file or block device in bytes"""
|
||||
pathstat = os.stat(path)
|
||||
"""Get size of a file or block device in bytes"""
|
||||
pathstat = os.stat(path)
|
||||
|
||||
# Check if it's a block device
|
||||
# Check if it's a block device
|
||||
|
||||
if stat.S_ISBLK(pathstat.st_mode):
|
||||
return open(path, 'rb').seek(0, os.SEEK_END)
|
||||
if stat.S_ISBLK(pathstat.st_mode):
|
||||
return open(path, 'rb').seek(0, os.SEEK_END)
|
||||
|
||||
# Otherwise, it's hopefully a file
|
||||
# Otherwise, it's hopefully a file
|
||||
|
||||
return pathstat.st_size
|
||||
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.
|
||||
count = 0
|
||||
shift = 0
|
||||
byte = -1
|
||||
while (byte & 0x80) != 0 or byte == -1:
|
||||
# Check for a corrupted stream. Read a max of 5 bytes.
|
||||
if shift == (5 * 7):
|
||||
raise ValueError
|
||||
byte = to_int(fileobj.read(1), order)
|
||||
count |= (byte & 0x7F) << shift
|
||||
shift += 7
|
||||
return count
|
||||
"""Read an Int32, 7 bits at a time."""
|
||||
# The highest bit of the byte, when on, means to continue reading more bytes.
|
||||
count = 0
|
||||
shift = 0
|
||||
byte = -1
|
||||
while (byte & 0x80) != 0 or byte == -1:
|
||||
# Check for a corrupted stream. Read a max of 5 bytes.
|
||||
if shift == (5 * 7):
|
||||
raise ValueError
|
||||
byte = to_int(fileobj.read(1), order)
|
||||
count |= (byte & 0x7F) << shift
|
||||
shift += 7
|
||||
return count
|
||||
|
||||
|
||||
def error(msg):
|
||||
"""Print fatal error message and terminate"""
|
||||
print('[ERROR] %s' % msg)
|
||||
sys.exit(1)
|
||||
"""Print fatal error message and terminate"""
|
||||
print('[ERROR] %s' % msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def warning(msg, args):
|
||||
"""Print a warning message. Warning messages can be silenced with --quiet"""
|
||||
"""Print a warning message. Warning messages can be silenced with --quiet"""
|
||||
|
||||
if not args.quiet:
|
||||
print('[WARNING] %s. Continuing regardless.' % msg)
|
||||
if not args.quiet:
|
||||
print('[WARNING] %s. Continuing regardless.' % msg, file=sys.stderr)
|
||||
|
||||
|
||||
def vprint(msg, args):
|
||||
"""Vprint, verbose print, can be silenced with --quiet"""
|
||||
"""Vprint, verbose print, can be silenced with --quiet"""
|
||||
|
||||
if not args.quiet:
|
||||
print('[*] ' + msg)
|
||||
if not args.quiet:
|
||||
print('[*] ' + msg)
|
||||
|
||||
|
||||
def download_ird(ird_name):
|
||||
"""Download an .ird from GET_IRD_NET_LOC"""
|
||||
"""Download an .ird from GET_IRD_NET_LOC"""
|
||||
|
||||
# Check if file already exists and skip if it does
|
||||
if os.path.exists(ird_name): # TODO: might want to check that the file is valid first, could do a HEAD agains the url
|
||||
return
|
||||
# Check if file already exists and skip if it does
|
||||
if os.path.exists(ird_name): # TODO: might want to check that the file is valid first, could do a HEAD agains the url
|
||||
return
|
||||
|
||||
ird_link = GET_IRD_NET_LOC + ird_name
|
||||
r = requests.get(ird_link, stream=True)
|
||||
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)
|
||||
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):
|
||||
"""Using a game_id, download the responding .ird from ALL_IRD_NET_LOC"""
|
||||
gameid = game_id.replace('-','')
|
||||
try:
|
||||
r = requests.get(ALL_IRD_NET_LOC, headers = {'User-Agent': 'Anonymous (You)' }, timeout=5)
|
||||
except requests.exceptions.ReadTimeout:
|
||||
error('Server timed out, fix your connection or manually specify a key/ird.')
|
||||
soup = BeautifulSoup(r.text, "html.parser")
|
||||
"""Using a game_id, download the responding .ird from ALL_IRD_NET_LOC"""
|
||||
gameid = game_id.replace('-', '')
|
||||
try:
|
||||
r = requests.get(ALL_IRD_NET_LOC, headers={'User-Agent': 'Anonymous (You)'}, timeout=5)
|
||||
except requests.exceptions.ReadTimeout:
|
||||
error('Server timed out, fix your connection or manually specify a key/ird.')
|
||||
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
|
||||
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. You could specify the decryption key with -d if you have it.")
|
||||
if not ird_name:
|
||||
error("Unable to download IRD, couldn't find link. You could specify the decryption key with -d if you have it.")
|
||||
|
||||
download_ird(ird_name)
|
||||
download_ird(ird_name)
|
||||
|
||||
return(ird_name)
|
||||
return (ird_name)
|
||||
|
||||
|
||||
def crc32(filename):
|
||||
"""Calculate crc32 for file"""
|
||||
def crc32(filename, keep_going=[True]):
|
||||
"""Calculate crc32 for file"""
|
||||
|
||||
with open(filename, 'rb') as infile:
|
||||
with open(filename, 'rb') as infile:
|
||||
|
||||
crc32 = 0
|
||||
crc32 = 0
|
||||
|
||||
while True:
|
||||
data = infile.read(65536)
|
||||
if not data:
|
||||
break
|
||||
crc32 = zlib.crc32(data, crc32)
|
||||
while keep_going[0] == True:
|
||||
data = infile.read(65536)
|
||||
if not data:
|
||||
break
|
||||
crc32 = zlib.crc32(data, crc32)
|
||||
|
||||
return "%08X" % (crc32 & 0xFFFFFFFF)
|
||||
if keep_going[0] == False:
|
||||
return None
|
||||
|
||||
return "%08X" % (crc32 & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def serial_country(title):
|
||||
"""Get country from disc serial / productcode / title_id"""
|
||||
"""Get country from disc serial / productcode / title_id"""
|
||||
|
||||
if title[2] == 'A':
|
||||
return 'Asia'
|
||||
if title[2] == 'C':
|
||||
return 'China'
|
||||
if title[2] == 'E':
|
||||
return 'Europe'
|
||||
if title[2] == 'H':
|
||||
return 'Hong Kong'
|
||||
if title[2] == 'J' or title[2] == 'P':
|
||||
return 'Japan'
|
||||
if title[2] == 'K':
|
||||
return 'Korea'
|
||||
if title[2] == 'U':
|
||||
return 'USA'
|
||||
if title[2] == 'A':
|
||||
return 'Asia'
|
||||
if title[2] == 'C':
|
||||
return 'China'
|
||||
if title[2] == 'E':
|
||||
return 'Europe'
|
||||
if title[2] == 'H':
|
||||
return 'Hong Kong'
|
||||
if title[2] == 'J' or title[2] == 'P':
|
||||
return 'Japan'
|
||||
if title[2] == 'K':
|
||||
return 'Korea'
|
||||
if title[2] == 'U' or title[2] == 'T':
|
||||
return 'USA'
|
||||
|
||||
raise ValueError('Unknown country?!')
|
||||
raise ValueError('Unknown country?!')
|
||||
|
||||
|
||||
def multiman_title(title):
|
||||
"""Fix special characters in title for Multiman style"""
|
||||
"""Fix special characters in title for Multiman style"""
|
||||
|
||||
replace = {
|
||||
':': ' -',
|
||||
'/': '-',
|
||||
'™': '',
|
||||
'®': '',
|
||||
}
|
||||
replace = {
|
||||
':': ' -',
|
||||
'/': '-',
|
||||
'™': '',
|
||||
'®': '',
|
||||
}
|
||||
|
||||
for key, val in replace.items():
|
||||
title = title.replace(key, val)
|
||||
for key, val in replace.items():
|
||||
title = title.replace(key, val)
|
||||
|
||||
return title
|
||||
return title
|
||||
|
||||
|
||||
# Main functions
|
||||
|
||||
def info(args):
|
||||
"""Print information about .iso and then quit."""
|
||||
|
||||
if args.iso:
|
||||
input_iso = iso.ISO(args)
|
||||
input_iso.print_info()
|
||||
sys.exit()
|
||||
|
||||
if args.ird:
|
||||
input_ird = ird.IRD(args.ird)
|
||||
input_ird.print_info(regions=True)
|
||||
sys.exit()
|
||||
|
||||
|
||||
def decrypt(args):
|
||||
"""Try to decrypt a given .iso using relevant .ird or encryption key from argparse
|
||||
"""Try to decrypt a given .iso using relevant .ird or encryption key from argparse
|
||||
|
||||
If no .ird is given this will try to automatically download an .ird file with the encryption/decryption key for the given game .iso
|
||||
"""
|
||||
If no .ird is given this will try to automatically download an .ird file with the encryption/decryption key for the given game .iso
|
||||
"""
|
||||
|
||||
input_iso = iso.ISO(args)
|
||||
input_iso = iso.ISO(args)
|
||||
|
||||
input_iso.decrypt(args) # TODO: some of the logic should probably be moved up here instead of residing in the decrypt function
|
||||
# TODO: some of the logic should probably be moved up here instead of residing in the decrypt function
|
||||
input_iso.decrypt(args)
|
||||
|
||||
|
||||
def encrypt(args):
|
||||
"""Try to re-encrypt a decrypted .iso using relevant .ird or encryption key from argparse
|
||||
"""Try to re-encrypt a decrypted .iso using relevant .ird or encryption key from argparse
|
||||
|
||||
If no .ird is given this will try to automatically download an .ird file with the encryption/decryption key for the given game .iso
|
||||
"""
|
||||
If no .ird is given this will try to automatically download an .ird file with the encryption/decryption key for the given game .iso
|
||||
"""
|
||||
|
||||
input_iso = iso.ISO(args)
|
||||
input_iso = iso.ISO(args)
|
||||
|
||||
input_iso.encrypt(args)
|
||||
input_iso.encrypt(args)
|
||||
|
|
|
|||
182
libray/ird.py
182
libray/ird.py
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf8 -*-
|
||||
|
||||
# libray - Libre Blu-Ray PS3 ISO Tool
|
||||
# Copyright © 2018 - 2021 Nichlas Severinsen
|
||||
# Copyright © 2018 - 2024 Nichlas Severinsen
|
||||
#
|
||||
# This file is part of libray.
|
||||
#
|
||||
|
|
@ -26,123 +26,123 @@ import shutil
|
|||
|
||||
|
||||
try:
|
||||
from libray import core
|
||||
from libray import core
|
||||
except ImportError:
|
||||
import core
|
||||
import core
|
||||
|
||||
|
||||
class IRD:
|
||||
"""Class for handling .ird files
|
||||
"""Class for handling .ird files
|
||||
|
||||
Attributes:
|
||||
version: IRD version number
|
||||
game_id: PS3 game identifier
|
||||
game_name: Name of PS3 game
|
||||
update_version: PS3 firmware update version
|
||||
game_version: PS3 game version
|
||||
app_version: PS3 app version
|
||||
region_count: How many encrypted regions are in the .iso
|
||||
file_count: How many files are supposed to be in the .iso
|
||||
data1: Encryption key
|
||||
"""
|
||||
Attributes:
|
||||
version: IRD version number
|
||||
game_id: PS3 game identifier
|
||||
game_name: Name of PS3 game
|
||||
update_version: PS3 firmware update version
|
||||
game_version: PS3 game version
|
||||
app_version: PS3 app version
|
||||
region_count: How many encrypted regions are in the .iso
|
||||
file_count: How many files are supposed to be in the .iso
|
||||
data1: Encryption key
|
||||
"""
|
||||
|
||||
ORDER = 'little'
|
||||
TEMP_FILE = 'ird'
|
||||
MAGIC_STRING = b'3IRD'
|
||||
ORDER = 'little'
|
||||
TEMP_FILE = 'ird'
|
||||
MAGIC_STRING = b'3IRD'
|
||||
|
||||
def __init__(self, ird_path, verbose=False):
|
||||
"""IRD constructor using args from argparse."""
|
||||
|
||||
def __init__(self, ird_path, verbose=False):
|
||||
"""IRD constructor using args from argparse."""
|
||||
self.uncompress(ird_path) # TODO: Try/Except?
|
||||
|
||||
self.uncompress(ird_path) # TODO: Try/Except?
|
||||
self.size = core.size(self.TEMP_FILE)
|
||||
|
||||
self.size = core.size(self.TEMP_FILE)
|
||||
if not self.size:
|
||||
core.error('IRD file is empty!')
|
||||
|
||||
if not self.size:
|
||||
core.error('IRD file is empty!')
|
||||
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')
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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 _ in range(0, self.region_count):
|
||||
self.region_hashes.append(input_ird.read(16))
|
||||
|
||||
self.region_count = core.to_int(input_ird.read(1), self.ORDER)
|
||||
self.region_hashes = []
|
||||
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 _ 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})
|
||||
|
||||
self.file_count = core.to_int(input_ird.read(4), self.ORDER)
|
||||
self.file_hashes = []
|
||||
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})
|
||||
if self.version >= 9:
|
||||
self.pic = input_ird.read(115)
|
||||
|
||||
if self.version >= 9:
|
||||
self.pic = input_ird.read(115)
|
||||
input_ird.seek(input_ird.tell() + 4) # ?
|
||||
|
||||
input_ird.seek(input_ird.tell() + 4) # ?
|
||||
self.data1 = input_ird.read(16)
|
||||
self.data2 = input_ird.read(16)
|
||||
|
||||
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 < 9:
|
||||
self.pic = input_ird.read(115)
|
||||
if self.version < 7:
|
||||
self.uid = core.to_int(input_ird.read(4), self.ORDER)
|
||||
|
||||
if self.version < 7:
|
||||
self.uid = core.to_int(input_ird.read(4), self.ORDER)
|
||||
if verbose:
|
||||
self.print_info()
|
||||
|
||||
if verbose:
|
||||
self.print_info()
|
||||
os.remove(self.TEMP_FILE)
|
||||
|
||||
os.remove(self.TEMP_FILE)
|
||||
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 = False
|
||||
with open(filename, 'rb') as input_ird:
|
||||
if input_ird.read(4) != self.MAGIC_STRING:
|
||||
uncompress = True
|
||||
|
||||
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."""
|
||||
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)
|
||||
|
||||
uncompress = False
|
||||
with open(filename, 'rb') as input_ird:
|
||||
if input_ird.read(4) != self.MAGIC_STRING:
|
||||
uncompress = True
|
||||
def print_info(self, regions=False):
|
||||
# TODO: This could probably have been a __str__? Who cares?
|
||||
"""Print some info about the IRD."""
|
||||
|
||||
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):
|
||||
# TODO: This could probably have been a __str__? Who cares?
|
||||
"""Print some info about the IRD."""
|
||||
|
||||
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())
|
||||
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)
|
||||
|
||||
if regions:
|
||||
for i, region_hash in enumerate(self.region_hashes):
|
||||
print('\tRegion Hash %s: %s' % (i, region_hash.hex()))
|
||||
|
||||
print('File Count: %s' % self.file_count)
|
||||
print('Data1: %s' % self.data1.hex())
|
||||
print('Data2: %s' % self.data2.hex())
|
||||
|
|
|
|||
603
libray/iso.py
603
libray/iso.py
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf8 -*-
|
||||
|
||||
# libray - Libre Blu-Ray PS3 ISO Tool
|
||||
# Copyright © 2018 -2021 Nichlas Severinsen
|
||||
# Copyright © 2018 - 2024 Nichlas Severinsen
|
||||
#
|
||||
# This file is part of libray.
|
||||
#
|
||||
|
|
@ -19,385 +19,374 @@
|
|||
# along with libray. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
import pathlib
|
||||
from threading import Thread
|
||||
import time
|
||||
import pkg_resources
|
||||
from tqdm import tqdm
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
|
||||
try:
|
||||
from libray import core
|
||||
from libray import ird
|
||||
from libray import sfo
|
||||
from libray import core
|
||||
from libray import ird
|
||||
from libray import sfo
|
||||
except ImportError:
|
||||
import core
|
||||
import ird
|
||||
import sfo
|
||||
import core
|
||||
import ird
|
||||
import sfo
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
NUM_INFO_BYTES = 4
|
||||
|
||||
|
||||
def read_regions(self, input_iso):
|
||||
"""List with info dict (start, end, whether it's encrypted) for every region.
|
||||
|
||||
Basically, every other (odd numbered) region is encrypted.
|
||||
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
|
||||
"""
|
||||
|
||||
# The first region is always unencrypted
|
||||
NUM_INFO_BYTES = 4
|
||||
|
||||
encrypted = False
|
||||
def read_regions(self, input_iso):
|
||||
"""List with info dict (start, end, whether it's encrypted) for every region.
|
||||
|
||||
regions = [{
|
||||
'start': core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR, # Should always be 0?
|
||||
'end': core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR + core.SECTOR,
|
||||
'enc': encrypted
|
||||
}]
|
||||
Basically, every other (odd numbered) region is encrypted.
|
||||
"""
|
||||
|
||||
# We'll read 4 bytes until we hit a non-size (<=0)
|
||||
# The first region is always unencrypted
|
||||
|
||||
while True:
|
||||
encrypted = False
|
||||
|
||||
encrypted = not encrypted
|
||||
regions = [{
|
||||
'start': core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR, # Should always be 0?
|
||||
'end': core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR + core.SECTOR,
|
||||
'enc': encrypted
|
||||
}]
|
||||
|
||||
end = core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR
|
||||
# We'll read 4 bytes until we hit a non-size (<=0)
|
||||
|
||||
if not end:
|
||||
break
|
||||
while True:
|
||||
|
||||
regions.append({
|
||||
'start': regions[-1]['end'],
|
||||
'end': end + core.SECTOR - (core.SECTOR if encrypted else 0),
|
||||
'enc': encrypted
|
||||
})
|
||||
encrypted = not encrypted
|
||||
|
||||
return regions
|
||||
end = core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR
|
||||
|
||||
if not end:
|
||||
break
|
||||
|
||||
def __init__(self, args):
|
||||
"""ISO constructor using args from argparse."""
|
||||
regions.append({
|
||||
'start': regions[-1]['end'],
|
||||
'end': end + core.SECTOR - (core.SECTOR if encrypted else 0),
|
||||
'enc': encrypted
|
||||
})
|
||||
|
||||
self.size = core.size(args.iso)
|
||||
return regions
|
||||
|
||||
if not self.size:
|
||||
core.error('looks like ISO file/mount is empty?')
|
||||
def __init__(self, args):
|
||||
"""ISO constructor using args from argparse."""
|
||||
|
||||
with open(args.iso, 'rb') as input_iso:
|
||||
# Get number of unencrypted regions
|
||||
self.number_of_unencrypted_regions = core.to_int(input_iso.read(self.NUM_INFO_BYTES))
|
||||
self.size = core.size(args.iso)
|
||||
|
||||
# Skip unused bytes
|
||||
input_iso.seek(input_iso.tell() + self.NUM_INFO_BYTES)
|
||||
if not self.size:
|
||||
core.error('looks like ISO file/mount is empty?')
|
||||
|
||||
self.regions = self.read_regions(input_iso)
|
||||
with open(args.iso, 'rb') as input_iso:
|
||||
# Get number of unencrypted regions
|
||||
self.number_of_unencrypted_regions = core.to_int(input_iso.read(self.NUM_INFO_BYTES))
|
||||
|
||||
# Seek to the start of sector 2, '+ 16' skips a section containing some 'playstation'
|
||||
input_iso.seek(core.SECTOR + 16)
|
||||
# Skip unused bytes
|
||||
input_iso.seek(input_iso.tell() + self.NUM_INFO_BYTES)
|
||||
|
||||
self.game_id = input_iso.read(16).decode('utf8').strip()
|
||||
self.regions = self.read_regions(input_iso)
|
||||
|
||||
# Find PARAM.SFO
|
||||
# Seek to the start of sector 2, '+ 16' skips a section containing some 'playstation'
|
||||
input_iso.seek(core.SECTOR + 16)
|
||||
|
||||
core.vprint('Searching for PARAM.SFO', args)
|
||||
self.game_id = input_iso.read(16).decode('utf8').strip()
|
||||
|
||||
input_iso.seek(0)
|
||||
counter = 1
|
||||
found_param = False
|
||||
# Find PARAM.SFO
|
||||
|
||||
while True:
|
||||
core.vprint('Searching for PARAM.SFO', args)
|
||||
|
||||
data = input_iso.read(8)
|
||||
input_iso.seek(0)
|
||||
counter = 1
|
||||
found_param = False
|
||||
|
||||
if not data:
|
||||
break
|
||||
while True:
|
||||
|
||||
#if data == b'PS3LICDA':
|
||||
# print(data)
|
||||
data = input_iso.read(8)
|
||||
|
||||
if data[0:4] == b'\x00\x50\x53\x46':
|
||||
found_param = True
|
||||
if not data:
|
||||
break
|
||||
|
||||
#input_iso.seek(input_iso.tell() - 8)
|
||||
#param = sfo.SFO(input_iso)
|
||||
#print(param['TITLE'])
|
||||
#print(param['TITLE_ID'])
|
||||
break
|
||||
# if data == b'PS3LICDA':
|
||||
# print(data)
|
||||
|
||||
if data[0:4] == b'\x00\x50\x53\x46':
|
||||
found_param = True
|
||||
|
||||
input_iso.seek((core.SECTOR * counter))
|
||||
# input_iso.seek(input_iso.tell() - 8)
|
||||
# param = sfo.SFO(input_iso)
|
||||
# print(param['TITLE'])
|
||||
# print(param['TITLE_ID'])
|
||||
break
|
||||
|
||||
counter += 1
|
||||
input_iso.seek((core.SECTOR * counter))
|
||||
|
||||
game_title = ''
|
||||
counter += 1
|
||||
|
||||
if found_param:
|
||||
input_iso.seek(input_iso.tell() - 8)
|
||||
try:
|
||||
param = sfo.SFO(input_iso)
|
||||
core.vprint('PARAM.SFO found', args)
|
||||
game_title = ''
|
||||
|
||||
game_title = core.multiman_title(param['TITLE'])
|
||||
if found_param:
|
||||
input_iso.seek(input_iso.tell() - 8)
|
||||
try:
|
||||
param = sfo.SFO(input_iso)
|
||||
core.vprint('PARAM.SFO found', args)
|
||||
|
||||
if args.verbose and not args.quiet:
|
||||
param.print_info()
|
||||
game_title = core.multiman_title(param['TITLE'])
|
||||
|
||||
# Set output to multiman style
|
||||
if args.verbose and not args.quiet:
|
||||
param.print_info()
|
||||
|
||||
if not args.output:
|
||||
args.output = '%s [%s].iso' % (game_title, param['TITLE_ID'])
|
||||
# Set output to multiman style
|
||||
if not args.output:
|
||||
args.output = f'{game_title} [{param["TITLE_ID"]}].iso'
|
||||
|
||||
except Exception:
|
||||
except Exception:
|
||||
core.warning('Failed reading SFO', args)
|
||||
|
||||
core.warning('Failed reading SFO', args)
|
||||
self.disc_key = self.get_key_from_args(game_title, args)
|
||||
if args.verbose and not args.quiet:
|
||||
self.print_info()
|
||||
|
||||
cipher = AES.new(core.ISO_SECRET, AES.MODE_CBC, core.ISO_IV)
|
||||
def decrypt(self, args):
|
||||
"""Decrypt self using args from argparse."""
|
||||
|
||||
# TODO: clean up this logic
|
||||
core.vprint(f'Decrypting with disc key: {self.disc_key.hex()}', args)
|
||||
|
||||
with open(args.iso, 'rb') as input_iso:
|
||||
|
||||
if not args.output:
|
||||
output_name = f'{self.game_id}.iso'
|
||||
else:
|
||||
output_name = args.output
|
||||
|
||||
core.vprint(f'Decrypted .iso is output to: {output_name}', args)
|
||||
|
||||
with open(output_name, 'wb') as output_iso:
|
||||
|
||||
if not args.quiet:
|
||||
pbar = tqdm(total=(self.size // 2048))
|
||||
|
||||
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:
|
||||
core.warning('Trying to read past the end of the file', args)
|
||||
break
|
||||
output_iso.write(data)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.update(1)
|
||||
continue
|
||||
# Encrypted region, decrypt then write
|
||||
else:
|
||||
while input_iso.tell() < region['end']:
|
||||
num = input_iso.tell() // 2048
|
||||
iv = bytearray([0 for i in range(0, 16)])
|
||||
for j in range(0, 16):
|
||||
iv[16 - j - 1] = (num & 0xFF)
|
||||
num >>= 8
|
||||
|
||||
data = input_iso.read(core.SECTOR)
|
||||
if not data:
|
||||
core.warning('Trying to read past the end of the file', args)
|
||||
break
|
||||
|
||||
cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv))
|
||||
decrypted = cipher.decrypt(data)
|
||||
|
||||
output_iso.write(decrypted)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.update(1)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.close()
|
||||
|
||||
core.vprint('Decryption complete!', args)
|
||||
|
||||
def encrypt(self, args):
|
||||
"""Encrypt self using args from argparse."""
|
||||
|
||||
core.vprint(f'Re-encrypting with disc key: {self.disc_key.hex()}', args)
|
||||
|
||||
with open(args.iso, 'rb') as input_iso:
|
||||
|
||||
if not args.output:
|
||||
output_name = f'{self.game_id}_e.iso'
|
||||
else:
|
||||
output_name = args.output
|
||||
|
||||
core.vprint(f'Re-encrypted .iso is output to: {output_name}', args)
|
||||
|
||||
with open(output_name, 'wb') as output_iso:
|
||||
|
||||
if not args.quiet:
|
||||
pbar = tqdm(total=(self.size // 2048))
|
||||
|
||||
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:
|
||||
core.warning('Trying to read past the end of the file', args)
|
||||
break
|
||||
output_iso.write(data)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.update(1)
|
||||
continue
|
||||
# Decrypted region, re-encrypt it
|
||||
else:
|
||||
while input_iso.tell() < region['end']:
|
||||
num = input_iso.tell() // 2048
|
||||
iv = bytearray([0 for i in range(0, 16)])
|
||||
for j in range(0, 16):
|
||||
iv[16 - j - 1] = (num & 0xFF)
|
||||
num >>= 8
|
||||
|
||||
data = input_iso.read(core.SECTOR)
|
||||
if not data:
|
||||
core.warning('Trying to read past the end of the file', args)
|
||||
break
|
||||
|
||||
cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv))
|
||||
encrypted = cipher.encrypt(data)
|
||||
|
||||
output_iso.write(encrypted)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.update(1)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.close()
|
||||
|
||||
core.vprint('Re-encryption complete!', args)
|
||||
|
||||
def get_key_from_args(self, game_title, args):
|
||||
# key provided with -d / --decryption-key
|
||||
if args.decryption_key:
|
||||
return core.to_bytes(args.decryption_key)
|
||||
|
||||
def get_key_from_ird(i):
|
||||
self.ird = ird.IRD(i)
|
||||
if self.ird.region_count != len(self.regions):
|
||||
core.error(
|
||||
f'Corrupt ISO or error in IRD. Expected {self.ird.region_count} regions, found {len(self.regions)} regions')
|
||||
|
||||
if self.regions[-1]['start'] > self.size:
|
||||
core.error(
|
||||
f'Corrupt ISO or error in IRD. Expected filesize larger than {self.regions[-1]["start"]/1024**3:.2f} GiB, actual size is {self.size/1024**3:.2f} GiB')
|
||||
cipher = AES.new(core.ISO_SECRET, AES.MODE_CBC, core.ISO_IV)
|
||||
return cipher.encrypt(self.ird.data1)
|
||||
|
||||
# .ird file given with -k / --ird
|
||||
if args.ird:
|
||||
return get_key_from_ird(args.ird)
|
||||
|
||||
if not args.decryption_key:
|
||||
if not args.ird:
|
||||
# No key or .ird specified. Let's first check if keys.db is packaged with this release
|
||||
|
||||
redump = False
|
||||
|
||||
core.vprint('Checking for bundled redump keys', args)
|
||||
|
||||
try:
|
||||
try:
|
||||
db = sqlite3.connect(pkg_resources.resource_filename(__name__, 'data/keys.db'))
|
||||
except FileNotFoundError:
|
||||
except FileNotFoundError:
|
||||
db = sqlite3.connect((pathlib.Path(__file__).resolve() / 'data/') / 'keys.db')
|
||||
c = db.cursor()
|
||||
c = db.cursor()
|
||||
|
||||
#core.vprint('Calculating crc32', args)
|
||||
# UPDATE: 2024 - New database now has game/title ids. See if we have that.
|
||||
|
||||
#input_iso.seek(0)
|
||||
core.vprint('Searching using TITLE_ID', args)
|
||||
keys = c.execute('SELECT name, key FROM games WHERE title_id = ?', [self.game_id.replace('-','')]).fetchall()
|
||||
if len(keys) == 1:
|
||||
core.vprint(f'Found potential redump key: "{keys[0][0]}"', args)
|
||||
return keys[0][1]
|
||||
|
||||
# crc32 = core.crc32(args.iso)
|
||||
# Then check if there's only one game with this exact size
|
||||
core.vprint('Trying to find redump key based on size', args)
|
||||
keys = c.execute('SELECT name, key FROM games WHERE size = ?', [str(self.size)]).fetchall()
|
||||
if len(keys) == 1:
|
||||
core.vprint(f'Found potential redump key: "{keys[0][0]}"', args)
|
||||
return keys[0][1]
|
||||
|
||||
#keys = c.execute('SELECT * FROM games WHERE crc32=?', [crc32.lower()]).fetchall()
|
||||
|
||||
# First check if there's only one game with this exact size
|
||||
|
||||
core.vprint('Trying to find redump key based on size', args)
|
||||
|
||||
keys = c.execute('SELECT * FROM games WHERE size = ?', [str(self.size)]).fetchall()
|
||||
|
||||
if len(keys) == 1:
|
||||
|
||||
self.disc_key = keys[0][-1]
|
||||
|
||||
redump = True
|
||||
|
||||
# If not, see if we can filter it out based on name and size
|
||||
|
||||
if not redump:
|
||||
|
||||
core.vprint('Trying to find redump key based on size, game title, and country', args)
|
||||
|
||||
if not game_title:
|
||||
raise ValueError
|
||||
|
||||
keys = c.execute('SELECT * FROM games WHERE lower(name) LIKE ? AND size = ?', ['%' + '%'.join(game_title.lower().split(' ')) + '%' + core.serial_country(self.game_id).lower() + '%', str(self.size)]).fetchall()
|
||||
|
||||
if keys:
|
||||
|
||||
self.disc_key = keys[0][-1]
|
||||
|
||||
redump = True
|
||||
|
||||
if not self.disc_key:
|
||||
# If not, see if we can filter it out based on name and size
|
||||
core.vprint('Trying to find redump key based on size, game title, and country', args)
|
||||
if not game_title:
|
||||
raise ValueError
|
||||
|
||||
core.vprint('Found potential redump key: "%s"' % keys[0][0], args)
|
||||
keys = c.execute('SELECT name, key FROM games WHERE lower(name) LIKE ? AND size = ?', [
|
||||
'%' + '%'.join(game_title.lower().split(' ')) + '%' + core.serial_country(self.game_id).lower() + '%', str(self.size)]).fetchall()
|
||||
if keys:
|
||||
core.vprint(f'Found potential redump key: "{keys[0][0]}"', args)
|
||||
return keys[0][1]
|
||||
|
||||
except:
|
||||
core.vprint('No keys found', args)
|
||||
# since checksums can take a while to calculate, bail here unless the
|
||||
# user has specifically indicated they want to try the CRC32 fallback
|
||||
if not args.checksum:
|
||||
core.error('could not find disc key')
|
||||
|
||||
if not redump:
|
||||
# Okay, searching has failed us, but maaaybe the checksum works?
|
||||
core.vprint('Trying to find redump key based on CRC32', args)
|
||||
crc32 = None
|
||||
crc32_continue = [True]
|
||||
if args.checksum_timeout > 0:
|
||||
def timeout(allow_execution):
|
||||
time.sleep(float(args.checksum_timeout))
|
||||
if crc32 is None:
|
||||
core.vprint(f'could not calculate CRC32 before {args.checksum_timeout}-second timeout', args)
|
||||
allow_execution[0] = False
|
||||
crc_thread = Thread(target=timeout, args=(crc32_continue,), daemon=True)
|
||||
crc_thread.start()
|
||||
|
||||
# Fallback to checking if an .ird exists
|
||||
crc32 = core.crc32(args.iso, crc32_continue)
|
||||
if crc32 is None:
|
||||
raise TimeoutError
|
||||
|
||||
core.warning('No IRD file specified, finding required file', args)
|
||||
args.ird = core.ird_by_game_id(self.game_id) # Download ird
|
||||
keys = c.execute('SELECT name, key FROM games WHERE crc32=?', [crc32.lower()]).fetchall()
|
||||
if len(keys) == 1:
|
||||
core.vprint(f'Found potential redump key: "{keys[0][0]}" (CRC32={crc32.lower()})', args)
|
||||
return keys[0][1]
|
||||
|
||||
self.ird = ird.IRD(args.ird)
|
||||
# Fallback to downloading an IRD from the internet (currently disabled)
|
||||
# try:
|
||||
# core.warning('No IRD file specified, finding required file', args)
|
||||
# args.ird = core.ird_by_game_id(self.game_id) # Download ird
|
||||
# return get_key_from_ird(args.ird)
|
||||
# except:
|
||||
# core.vprint('Could not download IRD file', args)
|
||||
|
||||
if self.ird.region_count != len(self.regions):
|
||||
core.error('Corrupt ISO or error in IRD. Expected %s regions, found %s regions' % (self.ird.region_count, len(self.regions)))
|
||||
|
||||
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:
|
||||
|
||||
# .ird file given with -k / --ird
|
||||
|
||||
self.ird = ird.IRD(args.ird)
|
||||
|
||||
if self.ird.region_count != len(self.regions):
|
||||
core.error('Corrupt ISO or error in IRD. Expected %s regions, found %s regions' % (self.ird.region_count, len(self.regions)))
|
||||
|
||||
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 = core.to_bytes(args.decryption_key)
|
||||
|
||||
if args.verbose and not args.quiet:
|
||||
self.print_info()
|
||||
|
||||
|
||||
def decrypt(self, args):
|
||||
"""Decrypt self using args from argparse."""
|
||||
|
||||
core.vprint('Decrypting with disc key: %s' % self.disc_key.hex(), args)
|
||||
|
||||
with open(args.iso, 'rb') as input_iso:
|
||||
|
||||
if not args.output:
|
||||
output_name = '%s.iso' % self.game_id
|
||||
else:
|
||||
output_name = args.output
|
||||
|
||||
core.vprint('Decrypted .iso is output to: %s' % output_name, args)
|
||||
|
||||
with open(output_name, 'wb') as output_iso:
|
||||
|
||||
if not args.quiet:
|
||||
pbar = tqdm(total= (self.size // 2048) )
|
||||
|
||||
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:
|
||||
core.warning('Trying to read past the end of the file', args)
|
||||
break
|
||||
output_iso.write(data)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.update(1)
|
||||
continue
|
||||
# Encrypted region, decrypt then write
|
||||
else:
|
||||
while input_iso.tell() < region['end']:
|
||||
num = input_iso.tell() // 2048
|
||||
iv = bytearray([0 for i in range(0,16)])
|
||||
for j in range(0,16):
|
||||
iv[16 - j - 1] = (num & 0xFF)
|
||||
num >>= 8
|
||||
|
||||
data = input_iso.read(core.SECTOR)
|
||||
if not data:
|
||||
core.warning('Trying to read past the end of the file', args)
|
||||
break
|
||||
|
||||
cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv))
|
||||
decrypted = cipher.decrypt(data)
|
||||
|
||||
output_iso.write(decrypted)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.update(1)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.close()
|
||||
|
||||
core.vprint('Decryption complete!', args)
|
||||
|
||||
|
||||
def encrypt(self, args):
|
||||
"""Encrypt self using args from argparse."""
|
||||
|
||||
core.vprint('Re-encrypting with disc key: %s' % self.disc_key.hex(), args)
|
||||
|
||||
with open(args.iso, 'rb') as input_iso:
|
||||
|
||||
if not args.output:
|
||||
output_name = '%s_e.iso' % self.game_id
|
||||
else:
|
||||
output_name = args.output
|
||||
|
||||
core.vprint('Re-encrypted .iso is output to: %s' % output_name, args)
|
||||
|
||||
with open(output_name, 'wb') as output_iso:
|
||||
|
||||
if not args.quiet:
|
||||
pbar = tqdm(total= (self.size // 2048) )
|
||||
|
||||
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:
|
||||
core.warning('Trying to read past the end of the file', args)
|
||||
break
|
||||
output_iso.write(data)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.update(1)
|
||||
continue
|
||||
# Decrypted region, re-encrypt it
|
||||
else:
|
||||
while input_iso.tell() < region['end']:
|
||||
num = input_iso.tell() // 2048
|
||||
iv = bytearray([0 for i in range(0,16)])
|
||||
for j in range(0,16):
|
||||
iv[16 - j - 1] = (num & 0xFF)
|
||||
num >>= 8
|
||||
|
||||
data = input_iso.read(core.SECTOR)
|
||||
if not data:
|
||||
core.warning('Trying to read past the end of the file', args)
|
||||
break
|
||||
|
||||
cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv))
|
||||
encrypted = cipher.encrypt(data)
|
||||
|
||||
output_iso.write(encrypted)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.update(1)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.close()
|
||||
|
||||
core.vprint('Re-encryption complete!', args)
|
||||
|
||||
|
||||
def print_info(self):
|
||||
# TODO: This could probably have been a __str__? Who cares?
|
||||
"""Print some info about the ISO."""
|
||||
print('Game ID: %s' % self.game_id)
|
||||
print('Key: %s' % self.disc_key.hex())
|
||||
print('Info from ISO:')
|
||||
print('Unencrypted regions: %s' % self.number_of_unencrypted_regions)
|
||||
for i, region in enumerate(self.regions):
|
||||
print(i, region, region['start'] // core.SECTOR, region['end'] // core.SECTOR)
|
||||
raise ValueError
|
||||
|
||||
def print_info(self):
|
||||
# TODO: This could probably have been a __str__? Who cares?
|
||||
"""Print some info about the ISO."""
|
||||
print(f'Game ID: {self.game_id}')
|
||||
print(f'Key: {self.disc_key.hex()}')
|
||||
print(f'Info from ISO:')
|
||||
print(f'Unencrypted regions: {self.number_of_unencrypted_regions}')
|
||||
for i, region in enumerate(self.regions):
|
||||
print(i, region, region['start'] // core.SECTOR, region['end'] // core.SECTOR)
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
libray.py
|
||||
63
libray/libray
Executable file
63
libray/libray
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf8 -*-
|
||||
|
||||
# libray - Libre Blu-Ray PS3 ISO Tool
|
||||
# Copyright © 2018 - 2024 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
try:
|
||||
from libray import core
|
||||
except ImportError:
|
||||
import core
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs')
|
||||
|
||||
required = parser.add_mutually_exclusive_group(required=True)
|
||||
required.add_argument('-i', '--iso', dest='iso', type=str, help='Path to .iso file or stream')
|
||||
required.add_argument('-k', '--ird', dest='ird', type=str, help='Path to .ird file', default='')
|
||||
|
||||
optional = parser.add_argument_group('optional arguments')
|
||||
optional.add_argument('-o', '--output', dest='output', type=str, help='Output filename', default='')
|
||||
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')
|
||||
# -e is reserved for "extract" so re-encrypt is "-r"
|
||||
optional.add_argument('-r', '--re-encrypt', dest='reencrypt', help='Re-encrypt .iso', action='store_true')
|
||||
optional.add_argument('-c', '--checksum', dest='checksum', help='Allow fallback to CRC32 checksum (disabled by default)', action='store_true')
|
||||
optional.add_argument('-t', '--checksum-timeout', dest='checksum_timeout', type=int, help='How many seconds to wait for CRC32 checksum (default 15)', default=15)
|
||||
optional.add_argument('--info', dest='info', action='store_true', help='Print info about .iso or .ird, then quit.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.info:
|
||||
core.info(args)
|
||||
|
||||
if not args.iso:
|
||||
core.error('No .iso file given. Use -i/--iso path/to/file.iso')
|
||||
|
||||
if args.reencrypt:
|
||||
core.encrypt(args)
|
||||
else:
|
||||
core.decrypt(args)
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# -*- coding: utf8 -*-
|
||||
|
||||
# libray - Libre Blu-Ray PS3 ISO Tool
|
||||
# Copyright © 2018 - 2021 Nichlas Severinsen
|
||||
# Copyright © 2018 - 2024 Nichlas Severinsen
|
||||
#
|
||||
# This file is part of libray.
|
||||
#
|
||||
|
|
@ -24,34 +24,40 @@ import argparse
|
|||
|
||||
|
||||
try:
|
||||
from libray import core
|
||||
from libray import core
|
||||
except ImportError:
|
||||
import core
|
||||
import core
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description='A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs')
|
||||
parser._action_groups.pop()
|
||||
parser = argparse.ArgumentParser(
|
||||
description='A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs')
|
||||
|
||||
required = parser.add_argument_group('required arguments')
|
||||
required.add_argument('-i', '--iso', dest='iso', type=str, help='Path to .iso file or stream', required=True)
|
||||
required = parser.add_mutually_exclusive_group(required=True)
|
||||
required.add_argument('-i', '--iso', dest='iso', type=str, help='Path to .iso file or stream')
|
||||
required.add_argument('-k', '--ird', dest='ird', type=str, help='Path to .ird file', default='')
|
||||
|
||||
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('-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')
|
||||
# -e is reserved for "extract" so re-encrypt is "-r"
|
||||
optional.add_argument('-r', '--re-encrypt', dest='reencrypt', help='Re-encrypt .iso', action='store_true')
|
||||
optional = parser.add_argument_group('optional arguments')
|
||||
optional.add_argument('-o', '--output', dest='output', type=str, help='Output filename', default='')
|
||||
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')
|
||||
# -e is reserved for "extract" so re-encrypt is "-r"
|
||||
optional.add_argument('-r', '--re-encrypt', dest='reencrypt', help='Re-encrypt .iso', action='store_true')
|
||||
optional.add_argument('-c', '--checksum', dest='checksum', help='Allow fallback to CRC32 checksum (disabled by default)', action='store_true')
|
||||
optional.add_argument('-t', '--checksum-timeout', dest='checksum_timeout', type=int, help='How many seconds to wait for CRC32 checksum (default 15)', default=15)
|
||||
optional.add_argument('--info', dest='info', action='store_true', help='Print info about .iso or .ird, then quit.')
|
||||
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.reencrypt:
|
||||
if args.info:
|
||||
core.info(args)
|
||||
|
||||
core.encrypt(args)
|
||||
if not args.iso:
|
||||
core.error('No .iso file given. Use -i/--iso path/to/file.iso')
|
||||
|
||||
else:
|
||||
|
||||
core.decrypt(args)
|
||||
if args.reencrypt:
|
||||
core.encrypt(args)
|
||||
else:
|
||||
core.decrypt(args)
|
||||
|
|
|
|||
135
libray/sfo.py
135
libray/sfo.py
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf8 -*-
|
||||
|
||||
# libray - Libre Blu-Ray PS3 ISO Tool
|
||||
# Copyright © 2018 -2021 Nichlas Severinsen
|
||||
# Copyright © 2018 - 2024 Nichlas Severinsen
|
||||
#
|
||||
# This file is part of libray.
|
||||
#
|
||||
|
|
@ -20,105 +20,104 @@
|
|||
|
||||
|
||||
try:
|
||||
from libray import core
|
||||
from libray import core
|
||||
except ImportError:
|
||||
import core
|
||||
import core
|
||||
|
||||
|
||||
class SFO:
|
||||
"""Class for handling .sfo files
|
||||
"""Class for handling .sfo files
|
||||
|
||||
Attributes:
|
||||
magic: Magic header
|
||||
version: .SFO version
|
||||
key_table_start: Absolute offset for key_table in .SFO
|
||||
data_table_start: Absolute offset for index_table in .SFO
|
||||
tables_entries: Number of entries in index_table and key_table
|
||||
key_data: Parsed keys and data tables from .SFO transformed into dict
|
||||
"""
|
||||
Attributes:
|
||||
magic: Magic header
|
||||
version: .SFO version
|
||||
key_table_start: Absolute offset for key_table in .SFO
|
||||
data_table_start: Absolute offset for index_table in .SFO
|
||||
tables_entries: Number of entries in index_table and key_table
|
||||
key_data: Parsed keys and data tables from .SFO transformed into dict
|
||||
"""
|
||||
|
||||
def __init__(self, fp):
|
||||
def __init__(self, fp):
|
||||
|
||||
self.file_start = fp.tell()
|
||||
self.file_start = fp.tell()
|
||||
|
||||
# Header
|
||||
# Header
|
||||
|
||||
self.magic = fp.read(4)
|
||||
self.version = fp.read(4)
|
||||
self.key_table_start = core.to_int(fp.read(4), 'little')
|
||||
self.data_table_start = core.to_int(fp.read(4), 'little')
|
||||
self.tables_entries = core.to_int(fp.read(4), 'little')
|
||||
self.magic = fp.read(4)
|
||||
self.version = fp.read(4)
|
||||
self.key_table_start = core.to_int(fp.read(4), 'little')
|
||||
self.data_table_start = core.to_int(fp.read(4), 'little')
|
||||
self.tables_entries = core.to_int(fp.read(4), 'little')
|
||||
|
||||
# Index table
|
||||
# Index table
|
||||
|
||||
index_table = []
|
||||
index_table = []
|
||||
|
||||
for _ in range(0, self.tables_entries):
|
||||
index_table.append({
|
||||
'key_offset': core.to_int(fp.read(2), 'little'),
|
||||
'data_fmt': fp.read(2),
|
||||
'data_len': core.to_int(fp.read(4), 'little'),
|
||||
'data_max_len': core.to_int(fp.read(4), 'little'),
|
||||
'data_offset': core.to_int(fp.read(4), 'little'),
|
||||
})
|
||||
for _ in range(0, self.tables_entries):
|
||||
index_table.append({
|
||||
'key_offset': core.to_int(fp.read(2), 'little'),
|
||||
'data_fmt': fp.read(2),
|
||||
'data_len': core.to_int(fp.read(4), 'little'),
|
||||
'data_max_len': core.to_int(fp.read(4), 'little'),
|
||||
'data_offset': core.to_int(fp.read(4), 'little'),
|
||||
})
|
||||
|
||||
# Key table
|
||||
# Key table
|
||||
|
||||
key_table = []
|
||||
key_table = []
|
||||
|
||||
for i in range(0, self.tables_entries):
|
||||
for i in range(0, self.tables_entries):
|
||||
|
||||
# Seek to absolute offset + relative offset of key
|
||||
# Seek to absolute offset + relative offset of key
|
||||
|
||||
fp.seek(self.file_start + self.key_table_start + index_table[i]['key_offset'])
|
||||
fp.seek(self.file_start + self.key_table_start +
|
||||
index_table[i]['key_offset'])
|
||||
|
||||
# Read key string until nullbyte
|
||||
# Read key string until nullbyte
|
||||
|
||||
key = ''
|
||||
key = ''
|
||||
|
||||
while True:
|
||||
while True:
|
||||
|
||||
data = fp.read(1)
|
||||
data = fp.read(1)
|
||||
|
||||
if data == b'\x00':
|
||||
break
|
||||
if data == b'\x00':
|
||||
break
|
||||
|
||||
key += data.decode('utf8')
|
||||
key += data.decode('utf8')
|
||||
|
||||
key_table.append(key)
|
||||
key_table.append(key)
|
||||
|
||||
# Data table
|
||||
# Data table
|
||||
|
||||
self.key_data = {}
|
||||
self.key_data = {}
|
||||
|
||||
for i in range(0, self.tables_entries):
|
||||
for i in range(0, self.tables_entries):
|
||||
|
||||
# Seek to absolute offset + relative offset of data
|
||||
# Seek to absolute offset + relative offset of data
|
||||
|
||||
fp.seek(self.file_start + self.data_table_start + index_table[i]['data_offset'])
|
||||
fp.seek(self.file_start + self.data_table_start + index_table[i]['data_offset'])
|
||||
|
||||
if index_table[i]['data_fmt'] == b'\x04\x02': #UTF8
|
||||
data = fp.read(index_table[i]['data_len'] - 1).decode('utf8')
|
||||
elif index_table[i]['data_fmt'] == b'\x04\x04': #int32
|
||||
data = core.to_int(fp.read(index_table[i]['data_len']), 'little')
|
||||
else: # Meh
|
||||
data = fp.read(index_table[i]['data_len'])
|
||||
if index_table[i]['data_fmt'] == b'\x04\x02': # UTF8
|
||||
data = fp.read(index_table[i]['data_len'] - 1).decode('utf8')
|
||||
elif index_table[i]['data_fmt'] == b'\x04\x04': # int32
|
||||
data = core.to_int(
|
||||
fp.read(index_table[i]['data_len']), 'little')
|
||||
else: # Meh
|
||||
data = fp.read(index_table[i]['data_len'])
|
||||
|
||||
self.key_data[key_table[i]] = data
|
||||
self.key_data[key_table[i]] = data
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Overload [] so we can directly select data using key from .SFO"""
|
||||
return self.key_data[key]
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Overload [] so we can directly select data using key from .SFO"""
|
||||
return self.key_data[key]
|
||||
def print_info(self):
|
||||
|
||||
print('Magic:', self.magic)
|
||||
print('Version: ', self.version)
|
||||
print('key_table_start:', self.key_table_start)
|
||||
print('data_table_start:', self.data_table_start)
|
||||
print('tables_entries:', self.tables_entries)
|
||||
|
||||
def print_info(self):
|
||||
|
||||
print('Magic:', self.magic)
|
||||
print('Version: ', self.version)
|
||||
print('key_table_start:', self.key_table_start)
|
||||
print('data_table_start:', self.data_table_start)
|
||||
print('tables_entries:', self.tables_entries)
|
||||
|
||||
print(self.key_data)
|
||||
|
||||
print(self.key_data)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue