iso.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. # -*- coding: utf8 -*-
  2. # libray - Libre Blu-Ray PS3 ISO Tool
  3. # Copyright © 2018 - 2024 Nichlas Severinsen
  4. #
  5. # This file is part of libray.
  6. #
  7. # libray is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # libray is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with libray. If not, see <https://www.gnu.org/licenses/>.
  19. import os
  20. import sys
  21. import sqlite3
  22. import pathlib
  23. from threading import Thread
  24. import time
  25. import pkg_resources
  26. from tqdm import tqdm
  27. from Crypto.Cipher import AES
  28. try:
  29. from libray import core
  30. from libray import ird
  31. from libray import sfo
  32. except ImportError:
  33. import core
  34. import ird
  35. import sfo
  36. class ISO:
  37. """Class for handling PS3 .iso files.
  38. Attributes:
  39. size: Size of .iso in bytes
  40. number_of_regions: Number of regions in the .iso
  41. regions: List with info of every region
  42. game_id: PS3 game id
  43. ird: IRD object (see ird.py)
  44. disc_key: data1 from .ird, encrypted
  45. """
  46. NUM_INFO_BYTES = 4
  47. def read_regions(self, input_iso):
  48. """List with info dict (start, end, whether it's encrypted) for every region.
  49. Basically, every other (odd numbered) region is encrypted.
  50. """
  51. # The first region is always unencrypted
  52. encrypted = False
  53. regions = [{
  54. 'start': core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR, # Should always be 0?
  55. 'end': core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR + core.SECTOR,
  56. 'enc': encrypted
  57. }]
  58. # We'll read 4 bytes until we hit a non-size (<=0)
  59. while True:
  60. encrypted = not encrypted
  61. end = core.to_int(input_iso.read(self.NUM_INFO_BYTES)) * core.SECTOR
  62. if not end:
  63. break
  64. regions.append({
  65. 'start': regions[-1]['end'],
  66. 'end': end + core.SECTOR - (core.SECTOR if encrypted else 0),
  67. 'enc': encrypted
  68. })
  69. return regions
  70. def __init__(self, args):
  71. """ISO constructor using args from argparse."""
  72. self.size = core.size(args.iso)
  73. if not self.size:
  74. core.error('looks like ISO file/mount is empty?')
  75. with open(args.iso, 'rb') as input_iso:
  76. # Get number of unencrypted regions
  77. self.number_of_unencrypted_regions = core.to_int(input_iso.read(self.NUM_INFO_BYTES))
  78. # Skip unused bytes
  79. input_iso.seek(input_iso.tell() + self.NUM_INFO_BYTES)
  80. self.regions = self.read_regions(input_iso)
  81. # Seek to the start of sector 2, '+ 16' skips a section containing some 'playstation'
  82. input_iso.seek(core.SECTOR + 16)
  83. self.game_id = input_iso.read(16).decode('utf8').strip()
  84. # Find PARAM.SFO
  85. core.vprint('Searching for PARAM.SFO', args)
  86. input_iso.seek(0)
  87. counter = 1
  88. found_param = False
  89. while True:
  90. data = input_iso.read(8)
  91. if not data:
  92. break
  93. # if data == b'PS3LICDA':
  94. # print(data)
  95. if data[0:4] == b'\x00\x50\x53\x46':
  96. found_param = True
  97. # input_iso.seek(input_iso.tell() - 8)
  98. # param = sfo.SFO(input_iso)
  99. # print(param['TITLE'])
  100. # print(param['TITLE_ID'])
  101. break
  102. input_iso.seek((core.SECTOR * counter))
  103. counter += 1
  104. game_title = ''
  105. if found_param:
  106. input_iso.seek(input_iso.tell() - 8)
  107. try:
  108. param = sfo.SFO(input_iso)
  109. core.vprint('PARAM.SFO found', args)
  110. game_title = core.multiman_title(param['TITLE'])
  111. if args.verbose and not args.quiet:
  112. param.print_info()
  113. # Set output to multiman style
  114. if not args.output:
  115. args.output = f'{game_title} [{param["TITLE_ID"]}].iso'
  116. except Exception:
  117. core.warning('Failed reading SFO', args)
  118. self.disc_key = self.get_key_from_args(game_title, args)
  119. if args.verbose and not args.quiet:
  120. self.print_info()
  121. def decrypt(self, args):
  122. """Decrypt self using args from argparse."""
  123. core.vprint(f'Decrypting with disc key: {self.disc_key.hex()}', args)
  124. with open(args.iso, 'rb') as input_iso:
  125. if not args.output:
  126. output_name = f'{self.game_id}.iso'
  127. else:
  128. output_name = args.output
  129. core.vprint(f'Decrypted .iso is output to: {output_name}', args)
  130. with open(output_name, 'wb') as output_iso:
  131. if not args.quiet:
  132. pbar = tqdm(total=(self.size // 2048))
  133. for region in self.regions:
  134. input_iso.seek(region['start'])
  135. # Unencrypted region, just copy it
  136. if not region['enc']:
  137. while input_iso.tell() < region['end']:
  138. data = input_iso.read(core.SECTOR)
  139. if not data:
  140. core.warning('Trying to read past the end of the file', args)
  141. break
  142. output_iso.write(data)
  143. if not args.quiet:
  144. pbar.update(1)
  145. continue
  146. # Encrypted region, decrypt then write
  147. else:
  148. while input_iso.tell() < region['end']:
  149. num = input_iso.tell() // 2048
  150. iv = bytearray([0 for i in range(0, 16)])
  151. for j in range(0, 16):
  152. iv[16 - j - 1] = (num & 0xFF)
  153. num >>= 8
  154. data = input_iso.read(core.SECTOR)
  155. if not data:
  156. core.warning('Trying to read past the end of the file', args)
  157. break
  158. cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv))
  159. decrypted = cipher.decrypt(data)
  160. output_iso.write(decrypted)
  161. if not args.quiet:
  162. pbar.update(1)
  163. if not args.quiet:
  164. pbar.close()
  165. core.vprint('Decryption complete!', args)
  166. def encrypt(self, args):
  167. """Encrypt self using args from argparse."""
  168. core.vprint(f'Re-encrypting with disc key: {self.disc_key.hex()}', args)
  169. with open(args.iso, 'rb') as input_iso:
  170. if not args.output:
  171. output_name = f'{self.game_id}_e.iso'
  172. else:
  173. output_name = args.output
  174. core.vprint(f'Re-encrypted .iso is output to: {output_name}', args)
  175. with open(output_name, 'wb') as output_iso:
  176. if not args.quiet:
  177. pbar = tqdm(total=(self.size // 2048))
  178. for region in self.regions:
  179. input_iso.seek(region['start'])
  180. # Unencrypted region, just copy it
  181. if not region['enc']:
  182. while input_iso.tell() < region['end']:
  183. data = input_iso.read(core.SECTOR)
  184. if not data:
  185. core.warning('Trying to read past the end of the file', args)
  186. break
  187. output_iso.write(data)
  188. if not args.quiet:
  189. pbar.update(1)
  190. continue
  191. # Decrypted region, re-encrypt it
  192. else:
  193. while input_iso.tell() < region['end']:
  194. num = input_iso.tell() // 2048
  195. iv = bytearray([0 for i in range(0, 16)])
  196. for j in range(0, 16):
  197. iv[16 - j - 1] = (num & 0xFF)
  198. num >>= 8
  199. data = input_iso.read(core.SECTOR)
  200. if not data:
  201. core.warning('Trying to read past the end of the file', args)
  202. break
  203. cipher = AES.new(self.disc_key, AES.MODE_CBC, bytes(iv))
  204. encrypted = cipher.encrypt(data)
  205. output_iso.write(encrypted)
  206. if not args.quiet:
  207. pbar.update(1)
  208. if not args.quiet:
  209. pbar.close()
  210. core.vprint('Re-encryption complete!', args)
  211. def get_key_from_args(self, game_title, args):
  212. # key provided with -d / --decryption-key
  213. if args.decryption_key:
  214. return core.to_bytes(args.decryption_key)
  215. def get_key_from_ird(i):
  216. self.ird = ird.IRD(i)
  217. if self.ird.region_count != len(self.regions):
  218. core.error(
  219. f'Corrupt ISO or error in IRD. Expected {self.ird.region_count} regions, found {len(self.regions)} regions')
  220. if self.regions[-1]['start'] > self.size:
  221. core.error(
  222. 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')
  223. cipher = AES.new(core.ISO_SECRET, AES.MODE_CBC, core.ISO_IV)
  224. return cipher.encrypt(self.ird.data1)
  225. # .ird file given with -k / --ird
  226. if args.ird:
  227. return get_key_from_ird(args.ird)
  228. # No key or .ird specified. Let's first check if keys.db is packaged with this release
  229. core.vprint('Checking for bundled redump keys', args)
  230. try:
  231. db = sqlite3.connect(pkg_resources.resource_filename(__name__, 'data/keys.db'))
  232. except FileNotFoundError:
  233. db = sqlite3.connect((pathlib.Path(__file__).resolve() / 'data/') / 'keys.db')
  234. c = db.cursor()
  235. # UPDATE: 2024 - New database now has game/title ids. See if we have that.
  236. core.vprint('Searching using TITLE_ID', args)
  237. keys = c.execute('SELECT name, key FROM games WHERE title_id = ?', [self.game_id.replace('-','')]).fetchall()
  238. if len(keys) == 1:
  239. core.vprint(f'Found potential redump key: "{keys[0][0]}"', args)
  240. return keys[0][1]
  241. # Then check if there's only one game with this exact size
  242. core.vprint('Trying to find redump key based on size', args)
  243. keys = c.execute('SELECT name, key FROM games WHERE size = ?', [str(self.size)]).fetchall()
  244. if len(keys) == 1:
  245. core.vprint(f'Found potential redump key: "{keys[0][0]}"', args)
  246. return keys[0][1]
  247. # If not, see if we can filter it out based on name and size
  248. core.vprint('Trying to find redump key based on size, game title, and country', args)
  249. if not game_title:
  250. raise ValueError
  251. keys = c.execute('SELECT name, key FROM games WHERE lower(name) LIKE ? AND size = ?', [
  252. '%' + '%'.join(game_title.lower().split(' ')) + '%' + core.serial_country(self.game_id).lower() + '%', str(self.size)]).fetchall()
  253. if keys:
  254. core.vprint(f'Found potential redump key: "{keys[0][0]}"', args)
  255. return keys[0][1]
  256. # since checksums can take a while to calculate, bail here unless the
  257. # user has specifically indicated they want to try the CRC32 fallback
  258. if not args.checksum:
  259. core.error('could not find disc key')
  260. # Okay, searching has failed us, but maaaybe the checksum works?
  261. core.vprint('Trying to find redump key based on CRC32', args)
  262. crc32 = None
  263. crc32_continue = [True]
  264. if args.checksum_timeout > 0:
  265. def timeout(allow_execution):
  266. time.sleep(float(args.checksum_timeout))
  267. if crc32 is None:
  268. core.vprint(f'could not calculate CRC32 before {args.checksum_timeout}-second timeout', args)
  269. allow_execution[0] = False
  270. crc_thread = Thread(target=timeout, args=(crc32_continue,), daemon=True)
  271. crc_thread.start()
  272. crc32 = core.crc32(args.iso, crc32_continue)
  273. if crc32 is None:
  274. raise TimeoutError
  275. keys = c.execute('SELECT name, key FROM games WHERE crc32=?', [crc32.lower()]).fetchall()
  276. if len(keys) == 1:
  277. core.vprint(f'Found potential redump key: "{keys[0][0]}" (CRC32={crc32.lower()})', args)
  278. return keys[0][1]
  279. # Fallback to downloading an IRD from the internet (currently disabled)
  280. # try:
  281. # core.warning('No IRD file specified, finding required file', args)
  282. # args.ird = core.ird_by_game_id(self.game_id) # Download ird
  283. # return get_key_from_ird(args.ird)
  284. # except:
  285. # core.vprint('Could not download IRD file', args)
  286. raise ValueError
  287. def print_info(self):
  288. # TODO: This could probably have been a __str__? Who cares?
  289. """Print some info about the ISO."""
  290. print(f'Game ID: {self.game_id}')
  291. print(f'Key: {self.disc_key.hex()}')
  292. print(f'Info from ISO:')
  293. print(f'Unencrypted regions: {self.number_of_unencrypted_regions}')
  294. for i, region in enumerate(self.regions):
  295. print(i, region, region['start'] // core.SECTOR, region['end'] // core.SECTOR)