2018-07-07 00:38:10 +02:00
# -*- coding: utf8 -*-
# libray - Libre Blu-Ray PS3 ISO Tool
2021-06-02 17:20:01 +02:00
# Copyright © 2018 -2021 Nichlas Severinsen
2019-05-16 10:37:28 +02:00
#
2018-07-07 00:38:10 +02:00
# This file is part of libray.
2019-05-16 10:37:28 +02:00
#
2018-07-07 00:38:10 +02:00
# 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.
2019-05-16 10:37:28 +02:00
#
2018-07-07 00:38:10 +02:00
# 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.
2019-05-16 10:37:28 +02:00
#
2018-07-07 00:38:10 +02:00
# You should have received a copy of the GNU General Public License
# along with libray. If not, see <https://www.gnu.org/licenses/>.
2018-07-07 12:13:11 +02:00
import sys
2021-06-05 22:03:31 +02:00
import sqlite3
2021-06-06 23:09:33 +02:00
import pathlib
2021-06-05 22:03:31 +02:00
import pkg_resources
2018-07-07 12:13:11 +02:00
from tqdm import tqdm
2018-07-07 00:38:10 +02:00
from Crypto . Cipher import AES
2019-06-07 09:00:03 +02:00
2018-07-07 00:38:10 +02:00
try :
from libray import core
2018-07-09 07:40:41 +02:00
from libray import ird
2021-06-06 23:09:33 +02:00
from libray import sfo
2018-07-07 00:38:10 +02:00
except ImportError :
import core
2018-07-09 07:40:41 +02:00
import ird
2021-06-06 23:09:33 +02:00
import sfo
2018-07-07 00:38:10 +02:00
class ISO :
2019-11-03 14:49:24 +01:00
""" Class for handling PS3 .iso files.
2019-07-07 21:18:34 +02:00
2019-06-07 09:00:03 +02:00
Attributes :
2019-11-03 14:49:24 +01:00
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
2019-06-07 09:00:03 +02:00
"""
2019-05-16 10:37:28 +02:00
2019-11-03 14:49:24 +01:00
2018-07-07 00:38:10 +02:00
NUM_INFO_BYTES = 4
2019-11-03 14:49:24 +01:00
def read_regions ( self , input_iso , filename ) :
""" List with info dict (start, end, whether it ' s encrypted) for every region.
2020-08-03 19:52:46 +02:00
2019-11-03 14:49:24 +01:00
Basically , every other ( odd numbered ) region is encrypted .
"""
regions = [ ]
encrypted = False
for _ in range ( 0 , self . number_of_regions ) :
2020-08-03 19:52:46 +02:00
2019-11-03 14:49:24 +01:00
regions . append ( {
' start ' : core . to_int ( input_iso . read ( self . NUM_INFO_BYTES ) ) * core . SECTOR ,
' end ' : core . to_int ( input_iso . read ( self . NUM_INFO_BYTES ) ) * core . SECTOR ,
' enc ' : encrypted
} )
input_iso . seek ( input_iso . tell ( ) - self . NUM_INFO_BYTES )
encrypted = not encrypted
# Last region might not actually be 2048 bytes, so we'll just cheat
regions [ - 1 ] [ ' end ' ] = self . size
return regions
2018-07-07 00:38:10 +02:00
def __init__ ( self , args ) :
2019-11-03 14:49:24 +01:00
""" ISO constructor using args from argparse. """
2019-06-07 09:00:03 +02:00
2019-07-07 21:18:34 +02:00
self . size = core . size ( args . iso )
if not self . size :
core . error ( ' looks like ISO file/mount is empty? ' )
2018-07-07 00:38:10 +02:00
with open ( args . iso , ' rb ' ) as input_iso :
2019-11-03 14:49:24 +01:00
# Get number of regions (times two as the number represents both encrypted and decrypted regions )
self . number_of_regions = core . to_int ( input_iso . read ( self . NUM_INFO_BYTES ) ) * 2
# Skip unused bytes
input_iso . seek ( input_iso . tell ( ) + self . NUM_INFO_BYTES )
2018-07-07 00:38:10 +02:00
self . regions = self . read_regions ( input_iso , args . iso )
2021-06-06 23:09:33 +02:00
# Seek to the start of sector 2, '+ 16' skips a section containing some 'playstation'
2019-11-03 14:49:24 +01:00
input_iso . seek ( core . SECTOR + 16 )
2018-07-07 19:03:47 +02:00
self . game_id = input_iso . read ( 16 ) . decode ( ' utf8 ' ) . strip ( )
2021-06-06 23:09:33 +02:00
# Find PARAM.SFO
core . vprint ( ' Searching for PARAM.SFO ' , args )
input_iso . seek ( 0 )
counter = 1
found_param = False
while True :
2021-06-29 22:08:43 +02:00
data = input_iso . read ( 8 )
2021-06-06 23:09:33 +02:00
if not data :
break
2021-06-29 22:08:43 +02:00
#if data == b'PS3LICDA':
# print(data)
if data [ 0 : 4 ] == b ' \x00 \x50 \x53 \x46 ' :
2021-06-06 23:09:33 +02:00
found_param = True
2021-06-29 22:08:43 +02:00
#input_iso.seek(input_iso.tell() - 8)
#param = sfo.SFO(input_iso)
#print(param['TITLE'])
#print(param['TITLE_ID'])
2021-06-06 23:09:33 +02:00
break
2021-06-29 22:08:43 +02:00
2021-06-06 23:09:33 +02:00
input_iso . seek ( ( core . SECTOR * counter ) )
counter + = 1
game_title = ' '
if found_param :
2021-06-29 22:08:43 +02:00
input_iso . seek ( input_iso . tell ( ) - 8 )
2021-06-06 23:09:33 +02:00
try :
param = sfo . SFO ( input_iso )
core . vprint ( ' PARAM.SFO found ' , args )
2021-06-29 22:08:43 +02:00
2021-06-06 23:09:33 +02:00
game_title = core . multiman_title ( param [ ' TITLE ' ] )
if args . verbose and not args . quiet :
param . print_info ( )
# Set output to multiman style
if not args . output :
args . output = ' %s [ %s ].iso ' % ( game_title , param [ ' TITLE_ID ' ] )
2021-06-29 22:08:43 +02:00
except Exception :
core . warning ( ' Failed reading SFO ' , args )
2021-06-06 23:09:33 +02:00
2019-11-03 14:49:24 +01:00
cipher = AES . new ( core . ISO_SECRET , AES . MODE_CBC , core . ISO_IV )
2018-07-09 07:40:41 +02:00
2021-06-29 22:08:43 +02:00
# TODO: clean up this logic
2019-11-03 14:49:24 +01:00
if not args . decryption_key :
if not args . ird :
2021-06-05 22:03:31 +02:00
# 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 :
2021-06-06 23:09:33 +02:00
try :
db = sqlite3 . connect ( pkg_resources . resource_filename ( __name__ , ' data/keys.db ' ) )
except FileNotFoundError :
db = sqlite3 . connect ( ( pathlib . Path ( __file__ ) . resolve ( ) / ' data/ ' ) / ' keys.db ' )
2021-06-05 22:03:31 +02:00
c = db . cursor ( )
2021-06-06 23:09:33 +02:00
#core.vprint('Calculating crc32', args)
#input_iso.seek(0)
# crc32 = core.crc32(args.iso)
#keys = c.execute('SELECT * FROM games WHERE crc32=?', [crc32.lower()]).fetchall()
2021-06-29 22:08:43 +02:00
# First check if there's only one game with this exact size
core . vprint ( ' Trying to find redump key based on size ' , args )
2021-06-05 22:03:31 +02:00
2021-06-29 22:08:43 +02:00
keys = c . execute ( ' SELECT * FROM games WHERE size = ? ' , [ str ( self . size ) ] ) . fetchall ( )
if len ( keys ) == 1 :
2021-06-05 22:03:31 +02:00
self . disc_key = keys [ 0 ] [ - 1 ]
2021-06-29 22:08:43 +02:00
redump = True
# If not, see if we can filter it out based on name and size
if not redump :
2021-06-30 08:38:00 +02:00
core . vprint ( ' Trying to find redump key based on size, game title, and country ' , args )
2021-06-29 22:08:43 +02:00
if not game_title :
2021-06-06 23:09:33 +02:00
raise ValueError
2021-06-30 08:38:00 +02:00
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 ( )
2021-06-05 22:03:31 +02:00
2021-06-29 22:08:43 +02:00
if keys :
2018-07-09 07:40:41 +02:00
2021-06-29 22:08:43 +02:00
self . disc_key = keys [ 0 ] [ - 1 ]
redump = True
if not self . disc_key :
2021-06-05 22:03:31 +02:00
raise ValueError
2021-06-29 22:08:43 +02:00
core . vprint ( ' Found potential redump key: " %s " ' % keys [ 0 ] [ 0 ] , args )
2021-06-05 22:03:31 +02:00
except :
core . vprint ( ' No keys found ' , args )
if not redump :
# Fallback to checking if an .ird exists
2018-07-09 07:40:41 +02:00
2021-06-05 22:03:31 +02:00
core . warning ( ' No IRD file specified, finding required file ' , args )
args . ird = core . ird_by_game_id ( self . game_id ) # Download ird
2018-07-07 12:13:11 +02:00
2021-06-05 22:03:31 +02:00
self . ird = ird . IRD ( args )
2020-08-03 19:52:46 +02:00
2021-06-05 22:03:31 +02:00
if self . ird . region_count != len ( self . regions ) - 1 :
core . error ( ' Corrupt ISO or error in IRD. Expected %s regions, found %s regions ' % ( self . ird . region_count , len ( self . regions ) - 1 ) )
if self . regions [ - 1 ] [ ' start ' ] > self . size :
core . error ( ' Corrupt ISO or error in IRD. Expected filesize larger than %.2f GiB, actual size is %.2f GiB ' % ( self . regions [ - 1 ] [ ' start ' ] / 1024 * * 3 , self . size / 1024 * * 3 ) )
self . disc_key = cipher . encrypt ( self . ird . data1 )
2019-11-03 14:49:24 +01:00
else :
2021-06-02 18:38:29 +02:00
self . disc_key = core . to_bytes ( args . decryption_key )
2018-07-07 00:38:10 +02:00
2021-06-05 22:03:31 +02:00
if args . verbose and not args . quiet :
self . print_info ( )
2018-07-07 00:38:10 +02:00
def decrypt ( self , args ) :
2019-11-03 14:49:24 +01:00
""" Decrypt self using args from argparse. """
2018-07-07 00:38:10 +02:00
2021-06-05 22:03:31 +02:00
core . vprint ( ' Decrypting with disc key: %s ' % self . disc_key . hex ( ) , args )
2018-07-07 00:38:10 +02:00
with open ( args . iso , ' rb ' ) as input_iso :
2020-08-03 19:52:46 +02:00
2019-08-01 08:34:56 +02:00
if not args . output :
output_name = ' %s .iso ' % self . game_id
else :
output_name = args . output
2021-06-29 22:08:43 +02:00
core . vprint ( ' Decrypted .iso is output to: %s ' % output_name , args )
2019-08-01 08:34:56 +02:00
with open ( output_name , ' wb ' ) as output_iso :
2018-07-07 12:13:11 +02:00
2020-08-03 19:52:46 +02:00
if not args . quiet :
pbar = tqdm ( total = ( self . size / / 2048 ) )
2019-05-16 10:37:28 +02:00
2019-11-03 14:49:24 +01:00
for region in self . regions :
2018-07-07 00:38:10 +02:00
input_iso . seek ( region [ ' start ' ] )
2019-11-03 14:49:24 +01:00
# Unencrypted region, just copy it
2018-07-07 00:38:10 +02:00
if not region [ ' enc ' ] :
while input_iso . tell ( ) < region [ ' end ' ] :
data = input_iso . read ( core . SECTOR )
2021-06-05 22:03:31 +02:00
if not data :
core . warning ( ' Trying to read past the end of the file ' , args )
2018-07-07 19:03:47 +02:00
break
2018-07-07 00:38:10 +02:00
output_iso . write ( data )
2020-08-03 19:52:46 +02:00
if not args . quiet :
pbar . update ( 1 )
2018-07-07 00:38:10 +02:00
continue
2019-11-03 14:49:24 +01:00
# Encrypted region, decrypt then write
2018-07-07 00:38:10 +02:00
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
2019-05-16 10:37:28 +02:00
2018-07-07 00:38:10 +02:00
data = input_iso . read ( core . SECTOR )
2021-06-05 22:03:31 +02:00
if not data :
core . warning ( ' Trying to read past the end of the file ' , args )
2018-07-07 19:03:47 +02:00
break
2019-05-16 10:37:28 +02:00
2018-07-07 00:38:10 +02:00
cipher = AES . new ( self . disc_key , AES . MODE_CBC , bytes ( iv ) )
decrypted = cipher . decrypt ( data )
2019-05-16 10:37:28 +02:00
2018-07-07 00:38:10 +02:00
output_iso . write ( decrypted )
2019-05-16 10:37:28 +02:00
2020-08-03 19:52:46 +02:00
if not args . quiet :
pbar . update ( 1 )
2018-07-07 00:38:10 +02:00
2020-08-03 19:52:46 +02:00
if not args . quiet :
pbar . close ( )
2021-06-05 22:03:31 +02:00
core . vprint ( ' Decryption complete! ' , args )
2018-07-07 00:38:10 +02:00
2021-06-02 20:25:49 +02:00
def encrypt ( self , args ) :
""" Encrypt self using args from argparse. """
2021-06-05 22:03:31 +02:00
core . vprint ( ' Re-encrypting with disc key: %s ' % self . disc_key . hex ( ) , args )
2021-06-02 20:25:49 +02:00
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
2021-06-29 22:08:43 +02:00
core . vprint ( ' Re-encrypted .iso is output to: %s ' % output_name , args )
2021-06-02 20:25:49 +02:00
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 )
2021-06-05 22:03:31 +02:00
if not data :
core . warning ( ' Trying to read past the end of the file ' , args )
2021-06-02 20:25:49 +02:00
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 )
2021-06-05 22:03:31 +02:00
if not data :
core . warning ( ' Trying to read past the end of the file ' , args )
2021-06-02 20:25:49 +02:00
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 )
2021-06-05 22:03:31 +02:00
if not args . quiet :
pbar . close ( )
core . vprint ( ' Re-encryption complete! ' , args )
2021-06-02 20:25:49 +02:00
2018-07-07 00:38:10 +02:00
def print_info ( self ) :
2019-06-07 09:00:03 +02:00
# TODO: This could probably have been a __str__? Who cares?
2019-11-03 14:49:24 +01:00
""" Print some info about the ISO. """
2021-06-02 20:25:49 +02:00
print ( ' Game ID: %s ' % self . game_id )
print ( ' Key: %s ' % self . disc_key . hex ( ) )
2018-07-07 00:38:10 +02:00
print ( ' Info from ISO: ' )
2019-11-03 14:49:24 +01:00
print ( ' Regions: %s ' % self . number_of_regions )
2018-07-07 00:38:10 +02:00
for i , region in enumerate ( self . regions ) :
print ( i , region )