Push 0.10.0

This commit is contained in:
Oracle 2026-05-18 16:31:58 +02:00
parent 266f3fc76d
commit 12c05afa57
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8
25 changed files with 2016 additions and 645 deletions

View file

@ -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

View file

@ -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)

View file

@ -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())

View file

@ -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)

View file

@ -1 +0,0 @@
libray.py

63
libray/libray Executable file
View 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)

View file

@ -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)

View file

@ -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)