Reworking identification
- PARAM.SFO reader (sfo.py) - Search for param first then use title there to identify - long_description (setup.py) - Multiman styling by default - Don't check crc32 as it is very slow
This commit is contained in:
parent
f3e8132a37
commit
82bc798677
8 changed files with 267 additions and 13 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,18 +1,22 @@
|
||||||
*.iso
|
*.iso
|
||||||
*.ird
|
*.ird
|
||||||
*.gz
|
*.gz
|
||||||
|
*.db
|
||||||
|
|
||||||
PS3_GAME/
|
PS3_GAME/
|
||||||
PS3_UPDATE/
|
PS3_UPDATE/
|
||||||
PS3_DISC.SFB
|
PS3_DISC.SFB
|
||||||
|
|
||||||
|
*/PS3_GAME
|
||||||
|
*/PS3_UPDATE
|
||||||
|
*PS3_DISC.SFB
|
||||||
|
|
||||||
tools/keys/*
|
tools/keys/*
|
||||||
tools/*/*
|
tools/*/*
|
||||||
tools/*.db
|
tools/*.db
|
||||||
tools/*.dat
|
tools/*.dat
|
||||||
|
|
||||||
libra/data/*.db
|
libray/data/*.db
|
||||||
*.db
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
|
||||||
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [0.0.7] - Unreleased
|
||||||
|
### Added
|
||||||
|
- PARAM.SFO reader (sfo.py)
|
||||||
|
- Now searches for PARAM.SFO first and uses game title from there instead of crc32.
|
||||||
|
- long_description in setup.py for description on PyPI
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Multiman styling by default if PARAM.SFO is found
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- No longer checks crc32, see Added. Might re-add as last fallback later.
|
||||||
|
|
||||||
## [0.0.6] - 2021-06-05
|
## [0.0.6] - 2021-06-05
|
||||||
### Fixed
|
### Fixed
|
||||||
- Issue #6: fix decrypting using disc key not working
|
- Issue #6: fix decrypting using disc key not working
|
||||||
|
|
|
||||||
|
|
@ -132,14 +132,19 @@ If you get any other errors, or have any other problem with libray, please [crea
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
[see also](http://www.psdevwiki.com/ps3/Bluray_disc#Encryption) ([archive.fo](https://archive.fo/hN1E6))
|
[Bluray disc encryption](http://www.psdevwiki.com/ps3/Bluray_disc#Encryption) ([archive.fo](https://archive.fo/hN1E6))
|
||||||
|
|
||||||
|
[.SFO](https://www.psdevwiki.com/ps3/PARAM.SFO) ([archive.fo](https://archive.fo/HLJZG))
|
||||||
|
|
||||||
|
[TITLE_ID for Physical Media](https://www.psdevwiki.com/ps3/Template:TITLE_ID_for_Physical_Media) ([archive.fo](https://archive.fo/R8tCz))
|
||||||
|
|
||||||
[7bit encoded int / RLE / CLP](https://github.com/microsoft/referencesource/blob/1acafe20a789a55daa17aac6bb47d1b0ec04519f/mscorlib/system/io/binaryreader.cs#L582-L600)
|
[7bit encoded int / RLE / CLP](https://github.com/microsoft/referencesource/blob/1acafe20a789a55daa17aac6bb47d1b0ec04519f/mscorlib/system/io/binaryreader.cs#L582-L600)
|
||||||
|
|
||||||
clp = compressed length prefix
|
clp = compressed length prefix
|
||||||
|
|
||||||
## Building and Deployment
|
## Deployment
|
||||||
|
|
||||||
|
0. `pip3 install wheel twine`
|
||||||
1. Place redump keys in tools/keys and .dat in tools/
|
1. Place redump keys in tools/keys and .dat in tools/
|
||||||
2. Run keys2db.py, ensure it made a file in libray/data/keys.db
|
2. Run keys2db.py, ensure it made a file in libray/data/keys.db
|
||||||
3. Run `python3 setup.py sdist bdist_wheel`
|
3. Run `python3 setup.py sdist bdist_wheel`
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,42 @@ def crc32(filename):
|
||||||
return "%08X" % (crc32 & 0xFFFFFFFF)
|
return "%08X" % (crc32 & 0xFFFFFFFF)
|
||||||
|
|
||||||
|
|
||||||
|
def serial_country(title):
|
||||||
|
"""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'
|
||||||
|
|
||||||
|
raise ValueError('Unknown country?!')
|
||||||
|
|
||||||
|
|
||||||
|
def multiman_title(title):
|
||||||
|
"""Fix special characters in title for Multiman style"""
|
||||||
|
|
||||||
|
replace = {
|
||||||
|
':': ' -',
|
||||||
|
'/': '-',
|
||||||
|
'™': ' -',
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val in replace.items():
|
||||||
|
title = title.replace(key, val)
|
||||||
|
|
||||||
|
return title
|
||||||
|
|
||||||
|
|
||||||
# Main functions
|
# Main functions
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -173,7 +209,7 @@ def decrypt(args):
|
||||||
|
|
||||||
input_iso = iso.ISO(args)
|
input_iso = iso.ISO(args)
|
||||||
|
|
||||||
input_iso.decrypt(args)
|
input_iso.decrypt(args) # TODO: some of the logic should probably be moved up here instead of residing in the decrypt function
|
||||||
|
|
||||||
|
|
||||||
def encrypt(args):
|
def encrypt(args):
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import pathlib
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
|
|
@ -29,9 +30,11 @@ from Crypto.Cipher import AES
|
||||||
try:
|
try:
|
||||||
from libray import core
|
from libray import core
|
||||||
from libray import ird
|
from libray import ird
|
||||||
|
from libray import sfo
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import core
|
import core
|
||||||
import ird
|
import ird
|
||||||
|
import sfo
|
||||||
|
|
||||||
|
|
||||||
class ISO:
|
class ISO:
|
||||||
|
|
@ -93,11 +96,54 @@ class ISO:
|
||||||
|
|
||||||
self.regions = self.read_regions(input_iso, args.iso)
|
self.regions = self.read_regions(input_iso, args.iso)
|
||||||
|
|
||||||
# Seek to the start of region 2, '+ 16' skips a section containing some 'playstation'
|
# Seek to the start of sector 2, '+ 16' skips a section containing some 'playstation'
|
||||||
input_iso.seek(core.SECTOR + 16)
|
input_iso.seek(core.SECTOR + 16)
|
||||||
|
|
||||||
self.game_id = input_iso.read(16).decode('utf8').strip()
|
self.game_id = input_iso.read(16).decode('utf8').strip()
|
||||||
|
|
||||||
|
# Find PARAM.SFO
|
||||||
|
|
||||||
|
core.vprint('Searching for PARAM.SFO', args)
|
||||||
|
|
||||||
|
input_iso.seek(0)
|
||||||
|
counter = 1
|
||||||
|
found_param = False
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
data = input_iso.read(4)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
|
||||||
|
if data == b'\x00\x50\x53\x46':
|
||||||
|
found_param = True
|
||||||
|
break
|
||||||
|
|
||||||
|
input_iso.seek((core.SECTOR * counter))
|
||||||
|
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
game_title = ''
|
||||||
|
|
||||||
|
if found_param:
|
||||||
|
input_iso.seek(input_iso.tell() - 4)
|
||||||
|
try:
|
||||||
|
param = sfo.SFO(input_iso)
|
||||||
|
core.vprint('PARAM.SFO found', args)
|
||||||
|
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'])
|
||||||
|
|
||||||
|
except:
|
||||||
|
core.warning('Failed reading SFO')
|
||||||
|
|
||||||
cipher = AES.new(core.ISO_SECRET, AES.MODE_CBC, core.ISO_IV)
|
cipher = AES.new(core.ISO_SECRET, AES.MODE_CBC, core.ISO_IV)
|
||||||
|
|
||||||
if not args.decryption_key:
|
if not args.decryption_key:
|
||||||
|
|
@ -109,20 +155,35 @@ class ISO:
|
||||||
core.vprint('Checking for bundled redump keys', args)
|
core.vprint('Checking for bundled redump keys', args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db = sqlite3.connect(pkg_resources.resource_filename(__name__, 'data/keys.db'))
|
try:
|
||||||
|
db = sqlite3.connect(pkg_resources.resource_filename(__name__, 'data/keys.db'))
|
||||||
|
except FileNotFoundError:
|
||||||
|
db = sqlite3.connect((pathlib.Path(__file__).resolve() / 'data/') / 'keys.db')
|
||||||
c = db.cursor()
|
c = db.cursor()
|
||||||
|
|
||||||
core.vprint('Calculating crc32', args)
|
if not game_title:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
crc32 = core.crc32(args.iso)
|
core.vprint('Trying to find redump key based on size and game title', args)
|
||||||
|
|
||||||
keys = c.execute('SELECT * FROM games WHERE crc32=?', [crc32.lower()]).fetchall()
|
#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()
|
||||||
|
|
||||||
|
keys = c.execute('SELECT * FROM games WHERE lower(name) LIKE ? AND size = ?', ['%' + game_title.lower() + '%', str(self.size)]).fetchall()
|
||||||
|
|
||||||
if keys:
|
if keys:
|
||||||
|
|
||||||
self.disc_key = keys[0][-1]
|
self.disc_key = keys[0][-1]
|
||||||
|
|
||||||
core.vprint('.ISO identified as "%s"' % keys[0][0], args)
|
if not self.disc_key:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
core.vprint('Found potential redump key: "%s"' % keys[0][0], args)
|
||||||
|
|
||||||
redump = True
|
redump = True
|
||||||
|
|
||||||
|
|
@ -159,6 +220,7 @@ class ISO:
|
||||||
"""Decrypt self using args from argparse."""
|
"""Decrypt self using args from argparse."""
|
||||||
|
|
||||||
core.vprint('Decrypting with disc key: %s' % self.disc_key.hex(), args)
|
core.vprint('Decrypting with disc key: %s' % self.disc_key.hex(), args)
|
||||||
|
core.vprint('Decrypted .iso is output to: %s' % args.output, args)
|
||||||
|
|
||||||
with open(args.iso, 'rb') as input_iso:
|
with open(args.iso, 'rb') as input_iso:
|
||||||
|
|
||||||
|
|
|
||||||
124
libray/sfo.py
Normal file
124
libray/sfo.py
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
# -*- coding: utf8 -*-
|
||||||
|
|
||||||
|
# libray - Libre Blu-Ray PS3 ISO Tool
|
||||||
|
# Copyright © 2018 -2021 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from libray import core
|
||||||
|
except ImportError:
|
||||||
|
import core
|
||||||
|
|
||||||
|
|
||||||
|
class SFO:
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fp):
|
||||||
|
|
||||||
|
self.file_start = fp.tell()
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
# 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'),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Key table
|
||||||
|
|
||||||
|
key_table = []
|
||||||
|
|
||||||
|
for i in range(0, self.tables_entries):
|
||||||
|
|
||||||
|
# Seek to absolute offset + relative offset of key
|
||||||
|
|
||||||
|
fp.seek(self.file_start + self.key_table_start + index_table[i]['key_offset'])
|
||||||
|
|
||||||
|
# Read key string until nullbyte
|
||||||
|
|
||||||
|
key = ''
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
data = fp.read(1)
|
||||||
|
|
||||||
|
if data == b'\x00':
|
||||||
|
break
|
||||||
|
|
||||||
|
key += data.decode('utf8')
|
||||||
|
|
||||||
|
key_table.append(key)
|
||||||
|
|
||||||
|
# Data table
|
||||||
|
|
||||||
|
self.key_data = {}
|
||||||
|
|
||||||
|
for i in range(0, self.tables_entries):
|
||||||
|
|
||||||
|
# Seek to absolute offset + relative offset of data
|
||||||
|
|
||||||
|
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'])
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
10
setup.py
10
setup.py
|
|
@ -1,12 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf8 -*-
|
# -*- coding: utf8 -*-
|
||||||
|
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
|
with open('README.md') as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="libray",
|
name="libray",
|
||||||
version="0.0.6",
|
version="0.0.7",
|
||||||
description='A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs',
|
description='A Libre (FLOSS) Python application for unencrypting, extracting, repackaging, and encrypting PS3 ISOs',
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
author="Nichlas Severinsen",
|
author="Nichlas Severinsen",
|
||||||
author_email="ns@nsz.no",
|
author_email="ns@nsz.no",
|
||||||
url="https://notabug.org/necklace/libray",
|
url="https://notabug.org/necklace/libray",
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
datfile = any_dats[0]
|
datfile = any_dats[0]
|
||||||
|
|
||||||
|
warnings = 0
|
||||||
|
|
||||||
with open(datfile, 'r') as infile:
|
with open(datfile, 'r') as infile:
|
||||||
|
|
||||||
soup = bs4.BeautifulSoup(infile.read(), features='html5lib')
|
soup = bs4.BeautifulSoup(infile.read(), features='html5lib')
|
||||||
|
|
@ -70,7 +72,7 @@ if __name__ == '__main__':
|
||||||
with open(cwd / ('keys/' + name + '.key'), 'rb') as keyfile:
|
with open(cwd / ('keys/' + name + '.key'), 'rb') as keyfile:
|
||||||
entry.append(keyfile.read())
|
entry.append(keyfile.read())
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print('Warning: key not found for ' + name)
|
warnings += 1
|
||||||
c.execute('INSERT INTO games (name, size, crc32, md5, sha1) VALUES (?, ?, ?, ?, ?)', entry)
|
c.execute('INSERT INTO games (name, size, crc32, md5, sha1) VALUES (?, ?, ?, ?, ?)', entry)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -82,6 +84,7 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
shutil.copyfile(db_path, ((cwd.parent / 'libray') / 'data/') / db_path.name)
|
shutil.copyfile(db_path, ((cwd.parent / 'libray') / 'data/') / db_path.name)
|
||||||
|
|
||||||
|
print('Warning: no keyfiles for %s titles' % str(warnings))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue