From db453da1ee319a4a278e3027adfb5931b2af418e Mon Sep 17 00:00:00 2001
From: Deterous <138427222+Deterous@users.noreply.github.com>
Date: Fri, 10 Nov 2023 12:13:34 +1300
Subject: [PATCH] Merge IRD and PS3ISO classes
---
LibIRD/IRD.cs | 638 +++++++++++++++++++++++++++++++++++++++++------
LibIRD/PS3ISO.cs | 537 ---------------------------------------
LibIRD/ReIRD.cs | 16 +-
3 files changed, 569 insertions(+), 622 deletions(-)
delete mode 100644 LibIRD/PS3ISO.cs
diff --git a/LibIRD/IRD.cs b/LibIRD/IRD.cs
index d422321..b9a84ba 100644
--- a/LibIRD/IRD.cs
+++ b/LibIRD/IRD.cs
@@ -1,4 +1,6 @@
-using System;
+using DiscUtils.Iso9660;
+using DiscUtils;
+using System;
using System.IO;
using System.IO.Compression;
using System.IO.Hashing;
@@ -11,7 +13,7 @@ namespace LibIRD
/// ISO Rebuild Data
///
/// Generates IRD fields and writes to an IRD file
- public class IRD : PS3ISO
+ public class IRD
{
#region Constants
@@ -21,6 +23,17 @@ namespace LibIRD
/// "3IRD"
private static readonly byte[] Magic = { 0x33, 0x49, 0x52, 0x44 };
+ ///
+ /// Blu-ray ISO sector size in bytes
+ ///
+ /// 2048
+ private protected static readonly uint SectorSize = 2048;
+
+ ///
+ /// MD5 hash of null
+ ///
+ private protected static readonly byte[] NullMD5 = new byte[] { 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e };
+
///
/// AES CBC Encryption Key for Data 1 (Disc Key)
///
@@ -129,6 +142,113 @@ namespace LibIRD
}
private byte[] _pic;
+ ///
+ /// The same value stored in PARAM.SFO / TITLE_ID
+ ///
+ /// 9 bytes, ASCII, stored without dashes
+ public string TitleID { get; private set; }
+
+ ///
+ /// The same value stored in PARAM.SFO / TITLE
+ ///
+ /// ASCII
+ public string Title { get; private set; }
+
+ ///
+ /// The same value stored in PARAM.SFO / PS3_SYSTEM_VER
+ ///
+ /// 4 bytes, ASCII, (e.g. "1.20", missing uses "0000"
+ public string SystemVersion { get; private set; }
+
+ ///
+ /// The same value stored in PARAM.SFO / VERSION
+ ///
+ /// 5 bytes, ASCII, e.g. "01.20"
+ public string GameVersion { get; private set; }
+
+ ///
+ /// The same value stored in PARAM.SFO / APP_VER
+ ///
+ /// 5 bytes, ASCII, e.g. "01.00"
+ public string AppVersion { get; private set; }
+
+ ///
+ /// Length of the gzip-compressed header data
+ ///
+ public uint HeaderLength { get; private set; }
+
+ ///
+ /// Gzip-compressed header data
+ ///
+ public byte[] Header { get; private set; }
+
+ ///
+ /// Length of the gzip-compressed footer data
+ ///
+ public uint FooterLength { get; private set; }
+
+ ///
+ /// Gzip-compressed footer data
+ ///
+ public byte[] Footer { get; private set; }
+
+ ///
+ /// Number of complete regions in the image
+ ///
+ public byte RegionCount { get; private set; }
+
+ ///
+ /// MD5 hashes for all complete regions in the image
+ ///
+ /// regions, 16-bytes per hash
+ public byte[][] RegionHashes { get; private set; }
+
+ ///
+ /// Number of files in the image
+ ///
+ public uint FileCount { get; private set; }
+
+ ///
+ /// Starting sector for each file
+ ///
+ /// files, alternating with each entry
+ public long[] FileKeys { get; private set; }
+
+ ///
+ /// MD5 hashes for all decrypted files in the image
+ ///
+ /// files, 16-bytes per hash, alternating with each entry
+ public byte[][] FileHashes { get; private set; }
+
+ ///
+ /// First byte of the PS3_DISC.SFB file
+ ///
+ /// Last byte to read when reading the header
+ private long FirstDataSector { get; set; }
+
+ ///
+ /// First byte of the PS3UPDAT.PUP file
+ ///
+ private long UpdateOffset { get; set; }
+
+ ///
+ /// Last byte of the PS3UPDAT.PUP file
+ ///
+ /// Offset to use when reading the footer
+ private long UpdateEnd { get; set; }
+
+ ///
+ /// First sector of each region
+ ///
+ private long[] RegionStart { get; set; }
+
+ ///
+ /// Last sector of each region
+ ///
+ private long[] RegionEnd { get; set; }
+
+ private protected long Size { get; private set; }
+
#endregion
#region Constructors
@@ -136,7 +256,7 @@ namespace LibIRD
///
/// Manual constructor
///
- private protected IRD(
+ private IRD(
byte version,
string titleID,
string title,
@@ -154,18 +274,21 @@ namespace LibIRD
byte[] d2,
byte[] pic,
uint uid)
- : base(
- titleID,
- title,
- sysVersion,
- gameVersion,
- appVersion,
- header,
- footer,
- regionHashes,
- fileKeys,
- fileHashes)
{
+ TitleID = titleID;
+ Title = title;
+ SystemVersion = sysVersion;
+ GameVersion = gameVersion;
+ AppVersion = appVersion;
+ HeaderLength = (uint)header.Length;
+ Header = header;
+ FooterLength = (uint)footer.Length;
+ Footer = footer;
+ RegionCount = (byte)regionHashes.Length;
+ RegionHashes = regionHashes;
+ FileCount = (uint)fileKeys.Length;
+ FileKeys = fileKeys;
+ FileHashes = fileHashes;
Version = version;
ExtraConfig = extraConfig;
Attachments = attachments;
@@ -178,9 +301,10 @@ namespace LibIRD
///
/// Default constructor for internal derived classes only: resulting object not in usable state
///
- private protected IRD(string isoPath) : base(isoPath)
+ private protected IRD(string isoPath)
{
// Assumes that internally derived class will set D1/D2/PIC in its own constructor
+ GenerateIRD(isoPath);
}
///
@@ -190,12 +314,15 @@ namespace LibIRD
/// Disc Key, byte array of length 16
/// Disc ID, byte array of length 16
/// Disc PIC, byte array of length 115
- public IRD(string isoPath, byte[] discKey, byte[] discID, byte[] discPIC) : base(isoPath)
+ public IRD(string isoPath, byte[] discKey, byte[] discID, byte[] discPIC)
{
- // Parse DiscKey, DiscID, and PIC
+ // Parse ISO, Disc Key, Disc ID, and PIC
GenerateD1(discKey);
GenerateD2(discID);
PIC = discPIC;
+
+ // Generate IRD files from ISO
+ GenerateIRD(isoPath);
}
///
@@ -205,8 +332,90 @@ namespace LibIRD
/// Path to the .getkey.log file
///
///
- public IRD(string isoPath, string getKeyLog) : base(isoPath)
+ public IRD(string isoPath, string getKeyLog)
{
+ // Parse .getkey.log file
+ ParseGetKeyLog(getKeyLog);
+
+ // Generate IRD files from ISO
+ GenerateIRD(isoPath);
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Generates Data 1, via AES-128 CBC decryption of a Disc Key
+ ///
+ /// Byte array containing AES encrypted Disc Key
+ ///
+ ///
+ private protected void GenerateD1(byte[] key)
+ {
+ // Validate key
+ if (key == null)
+ throw new ArgumentNullException(nameof(key));
+ if (key.Length != 16)
+ throw new ArgumentException("Disc Key must be a byte array of length 16", nameof(key));
+
+ // Setup AES decryption
+ using Aes aes = Aes.Create() ?? throw new InvalidOperationException("AES not available. Change your system settings");
+
+ // Set AES settings
+ aes.Key = D1AesKey;
+ aes.IV = D1AesIV;
+ aes.Padding = PaddingMode.None;
+ aes.Mode = CipherMode.CBC;
+
+ // Perform AES decryption
+ using MemoryStream stream = new();
+ using ICryptoTransform dec = aes.CreateDecryptor();
+ using CryptoStream cs = new(stream, dec, CryptoStreamMode.Write);
+ cs.Write(key, 0, 16);
+ cs.FlushFinalBlock();
+
+ // Save decrypted key to field
+ Data1Key = stream.ToArray();
+ }
+
+ ///
+ /// Generates Data 2, via AES-128 CBC encryption of a Disc ID
+ ///
+ /// Byte array containing AES decrypted Disc ID
+ ///
+ ///
+ private protected void GenerateD2(byte[] id)
+ {
+ // Validate id
+ if (id == null)
+ throw new ArgumentNullException(nameof(id));
+ if (id.Length != 16)
+ throw new ArgumentException("Disc ID must be a byte array of length 16", nameof(id));
+
+ // Setup AES encryption
+ using Aes aes = Aes.Create() ?? throw new InvalidOperationException("AES not available. Change your system settings");
+
+ // Set AES settings
+ aes.Key = D2AesKey;
+ aes.IV = D2AesIV;
+ aes.Padding = PaddingMode.None;
+ aes.Mode = CipherMode.CBC;
+
+ // Perform AES encryption
+ using MemoryStream stream = new();
+ using ICryptoTransform enc = aes.CreateEncryptor();
+ using CryptoStream cs = new(stream, enc, CryptoStreamMode.Write);
+ cs.Write(id, 0, 16);
+ cs.FlushFinalBlock();
+
+ // Save encrypted key to field
+ Data2Key = stream.ToArray();
+ }
+
+ private void ParseGetKeyLog(string getKeyLog)
+ {
+
// Validate .getkey.log file path
if (getKeyLog == null)
throw new ArgumentNullException(nameof(getKeyLog));
@@ -281,76 +490,363 @@ namespace LibIRD
PIC = discPIC;
}
- #endregion
-
- #region Methods
-
///
- /// Generates Data 1, via AES-128 CBC decryption of a Disc Key
+ /// Constructor for generating values from an ISO file
///
- /// Byte array containing AES encrypted Disc Key
+ /// Path to the ISO
///
- ///
- private protected void GenerateD1(byte[] key)
+ ///
+ ///
+ private void GenerateIRD(string isoPath)
{
- // Validate key
- if (key == null)
- throw new ArgumentNullException(nameof(key));
- if (key.Length != 16)
- throw new ArgumentException("Disc Key must be a byte array of length 16", nameof(key));
+ // Validate ISO path
+ if (isoPath == null || isoPath.Length <= 0)
+ throw new ArgumentNullException(nameof(isoPath));
- // Setup AES decryption
- using Aes aes = Aes.Create() ?? throw new InvalidOperationException("AES not available. Change your system settings");
+ // Check file exists
+ var iso = new FileInfo(isoPath);
+ if (!iso.Exists)
+ throw new FileNotFoundException(nameof(isoPath));
- // Set AES settings
- aes.Key = D1AesKey;
- aes.IV = D1AesIV;
- aes.Padding = PaddingMode.None;
- aes.Mode = CipherMode.CBC;
+ // Calculate file size
+ Size = iso.Length;
- // Perform AES decryption
- using MemoryStream stream = new();
- using ICryptoTransform dec = aes.CreateDecryptor();
- using CryptoStream cs = new(stream, dec, CryptoStreamMode.Write);
- cs.Write(key, 0, 16);
- cs.FlushFinalBlock();
+ // Parse ISO file as a file stream
+ using FileStream fs = new FileStream(isoPath, FileMode.Open, FileAccess.Read) ?? throw new FileNotFoundException(isoPath);
+ // Validate ISO file stream
+ if (!CDReader.Detect(fs))
+ throw new InvalidFileSystemException("Not a valid ISO file");
- // Save decrypted key to field
- Data1Key = stream.ToArray();
+ // New ISO Reader from DiscUtils
+ CDReader reader = new(fs, true, true);
+
+ // Read PS3 Metadata from PARAM.SFO
+ using (DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read))
+ {
+ // Parse PARAM.SFO file
+ ParamSFO paramSFO = new(s);
+ // Store required values for IRD
+ TitleID = paramSFO["TITLE_ID"];
+ Title = paramSFO["TITLE"];
+ GameVersion = paramSFO["VERSION"];
+ AppVersion = paramSFO["APP_VER"];
+ }
+
+ // Determine system update version
+ GetSystemVersion(fs, reader);
+
+ // Read and compress the ISO header
+ GetHeader(fs, reader);
+
+ // Read and compress the ISO footer
+ GetFooter(fs);
+
+ // Process all regions on ISO
+ HashRegions(fs);
+
+ // TODO: Speed up program by hashing regions and files at the same time (read from filesystem only once)
+
+ // Recursively count all files in ISO to allocate file arrays
+ DiscDirectoryInfo rootDir = reader.GetDirectoryInfo("\\");
+ FileCount = 0;
+ CountFiles(rootDir);
+ FileKeys = new long[FileCount];
+ FileHashes = new byte[FileCount][];
+
+ // Determine file offsets and hashes
+ uint fileCount = FileCount;
+ FileCount = 0;
+ ProcessFiles(fs, reader, rootDir);
+ if (FileCount != fileCount)
+ throw new InvalidFileSystemException("Unexpected ISO filesystem error: ");
+ Array.Sort(FileKeys, FileHashes);
}
///
- /// Generates Data 2, via AES-128 CBC encryption of a Disc ID
+ /// Retreives and stores the system version
///
- /// Byte array containing AES decrypted Disc ID
- ///
- ///
- private protected void GenerateD2(byte[] id)
+ /// PS3UPDAT.PUP update file version number
+ /// ISO filestream
+ ///
+ ///
+ private void GetSystemVersion(FileStream fs, CDReader reader)
{
- // Validate id
- if (id == null)
- throw new ArgumentNullException(nameof(id));
- if (id.Length != 16)
- throw new ArgumentException("Disc ID must be a byte array of length 16", nameof(id));
+ // Determine PUP file offset via cluster
+ DiscUtils.Streams.Range[] updateClusters = reader.PathToClusters("\\PS3_UPDATE\\PS3UPDAT.PUP");
+ if (updateClusters == null || updateClusters.Length <= 0)
+ {
+ // File too small for dedicated cluster, try get the offset from the file extents instead
+ DiscUtils.Streams.StreamExtent[] updateExtents = reader.PathToExtents("\\PS3_UPDATE\\PS3UPDAT.PUP");
+ if (updateExtents == null || updateExtents.Length <= 0)
+ throw new InvalidFileSystemException("Unexpected PS3UPDAT.PUP file extent in ISO filestream");
+ // PS3UPDAT.PUP file begins at start of first extent
+ UpdateOffset = updateExtents[0].Start;
+ // Update file ends at the last extent plus its length
+ UpdateEnd = updateExtents[^1].Start + updateExtents[^1].Length;
+ }
+ else
+ {
+ // PS3UPDAT.PUP file begins at first byte of dedicated cluster
+ UpdateOffset = updateClusters[0] != null ? SectorSize * updateClusters[0].Offset : 0;
+ // Update file ends at the last byte of the last cluster
+ UpdateEnd = SectorSize * (updateClusters[^1].Offset + updateClusters[^1].Count);
+ }
- // Setup AES encryption
- using Aes aes = Aes.Create() ?? throw new InvalidOperationException("AES not available. Change your system settings");
+ // Check PUP file Magic
+ fs.Seek(UpdateOffset, SeekOrigin.Begin);
+ byte[] pupMagic = new byte[5];
+ fs.Read(pupMagic, 0, pupMagic.Length);
+ // If magic is incorrect, set version to "0000" (unknown)
+ if (Encoding.ASCII.GetString(pupMagic) != "SCEUF")
+ SystemVersion = "0000";
+ else
+ {
+ // Determine location of version string
+ fs.Seek(UpdateOffset + 0x3E, SeekOrigin.Begin);
+ byte[] offset = new byte[2];
+ fs.Read(offset, 0, 2);
+ // Move stream to PUP version string
+ Array.Reverse(offset);
+ ushort versionOffset = BitConverter.ToUInt16(offset, 0);
+ fs.Seek(UpdateOffset + versionOffset, SeekOrigin.Begin);
+ // Read version string
+ byte[] version = new byte[4];
+ fs.Read(version, 0, version.Length);
+ // Set version string
+ SystemVersion = Encoding.ASCII.GetString(version);
+ }
+ }
- // Set AES settings
- aes.Key = D2AesKey;
- aes.IV = D2AesIV;
- aes.Padding = PaddingMode.None;
- aes.Mode = CipherMode.CBC;
+ ///
+ /// Retreives and stores the header
+ ///
+ /// ISO filestream
+ ///
+ ///
+ private void GetHeader(FileStream fs, CDReader reader)
+ {
+ // Determine the extent of the header via cluster (Sector 0 to first data sector)
+ DiscUtils.Streams.Range[] sfbClusters = reader.PathToClusters("\\PS3_DISC.SFB");
+ if (sfbClusters == null || sfbClusters.Length <= 0)
+ {
+ // File too small for dedicated cluster, try get the first sector from the file extents instead
+ DiscUtils.Streams.StreamExtent[] sfbExtents = reader.PathToExtents("\\PS3_DISC.SFB");
+ if (sfbExtents == null || sfbExtents.Length <= 0)
+ throw new InvalidFileSystemException("Unexpected PS3UPDAT.PUP file extent in ISO filestream");
+ FirstDataSector = sfbExtents[0].Start;
+ }
+ else
+ {
+ // End of header is at beginning of first byte of dedicated cluster
+ FirstDataSector = sfbClusters[0] != null ? sfbClusters[0].Offset : 0;
+ }
- // Perform AES encryption
- using MemoryStream stream = new();
- using ICryptoTransform enc = aes.CreateEncryptor();
- using CryptoStream cs = new(stream, enc, CryptoStreamMode.Write);
- cs.Write(id, 0, 16);
- cs.FlushFinalBlock();
+ // Begin a GZip stream to write header to
+ using MemoryStream headerStream = new();
+ using (GZipStream gzs = new(headerStream, CompressionLevel.SmallestSize))
+ {
+ // Start reading data from the beginning of the ISO file
+ fs.Seek(0, SeekOrigin.Begin);
+ byte[] buf = new byte[SectorSize];
+ int numBytes;
- // Save encrypted key to field
- Data2Key = stream.ToArray();
+ // Read all data before the first data sector
+ for (int i = 0; i < FirstDataSector; i++)
+ {
+ numBytes = fs.Read(buf, 0, buf.Length);
+ gzs.Write(buf, 0, numBytes);
+ }
+ }
+
+ // Save stream to field
+ Header = headerStream.ToArray();
+ HeaderLength = (uint)Header.Length;
+ }
+
+ ///
+ /// Retreives and stores the footer
+ ///
+ /// ISO filestream
+ private void GetFooter(FileStream fs)
+ {
+ // Begin a GZip stream to write footer to
+ using MemoryStream footerStream = new();
+ using (GZipStream gzs = new(footerStream, CompressionLevel.SmallestSize))
+ {
+ // Start reading data from after last file
+ fs.Seek(UpdateEnd, SeekOrigin.Begin);
+ byte[] buf = new byte[SectorSize];
+ int numBytes = (int)SectorSize;
+
+ // Keep reading data until there is none left to read
+ while (numBytes != 0)
+ {
+ numBytes = fs.Read(buf, 0, buf.Length);
+ gzs.Write(buf, 0, numBytes);
+ }
+ }
+
+ // Save stream to field
+ Footer = footerStream.ToArray();
+ FooterLength = (uint)Footer.Length;
+ }
+
+ ///
+ /// Determines and stores the hashes for each disc region
+ ///
+ ///
+ ///
+ private void HashRegions(FileStream fs)
+ {
+ // Determine the number of unencryted regions
+ fs.Seek(0, SeekOrigin.Begin);
+ byte[] decRegionCount = new byte[4];
+ fs.Read(decRegionCount, 0, 4);
+
+ // Total number of regions is 2x number of unencrypted regions, minus 1
+ RegionCount = (byte)(2 * ((uint)decRegionCount[3]) - 1);
+ if (RegionCount <= 0)
+ throw new InvalidFileSystemException("No regions detected in ISO");
+ RegionHashes = new byte[RegionCount][];
+ RegionStart = new long[RegionCount];
+ RegionEnd = new long[RegionCount];
+
+ // Determine the extent for each region
+ byte[] regionSector = new byte[4];
+ fs.Seek(8, SeekOrigin.Begin);
+ fs.Read(regionSector, 0, 4);
+ Array.Reverse(regionSector, 0, 4);
+ for (int i = 0; i < RegionCount; i++)
+ {
+ // End sector of previous region is start of this region
+ if (i % 2 == 1)
+ RegionStart[i] = BitConverter.ToInt32(regionSector) + 1;
+ else
+ RegionStart[i] = BitConverter.ToInt32(regionSector);
+ // Determine end sector offset of this region
+ fs.Read(regionSector, 0, 4);
+ Array.Reverse(regionSector, 0, 4);
+ if (i % 2 == 1)
+ RegionEnd[i] = BitConverter.ToInt32(regionSector) - 1;
+ else
+ RegionEnd[i] = BitConverter.ToInt32(regionSector);
+ }
+
+ // Remove header from first region
+ RegionStart[0] = FirstDataSector;
+ // Remove footer from last region
+ RegionEnd[^1] = (UpdateEnd / SectorSize) - 1;
+
+ // Determine MD5 hashes for each region
+ using MD5 md5 = MD5.Create();
+ byte[] buf = new byte[SectorSize];
+ for (int i = 0; i < RegionCount; i++)
+ {
+ // Start reading data from first sector of region
+ fs.Seek(SectorSize * RegionStart[i], SeekOrigin.Begin);
+
+ // Compute MD5 hash for just the region portion of the ISO file
+ int numBytes;
+ for (long j = RegionStart[i]; j <= RegionEnd[i]; j++)
+ {
+ // Read one sector at a time
+ numBytes = fs.Read(buf, 0, buf.Length);
+ // Check that an entire sector was read
+ if (numBytes < buf.Length)
+ throw new InvalidFileSystemException("Disc region ended unexpectedly");
+ // Process MD5 sum one sector at a time
+ md5.TransformBlock(buf, 0, buf.Length, null, 0);
+ }
+
+ // Compute and store MD5 hash of region
+ md5.TransformFinalBlock(buf, 0, 0);
+ RegionHashes[i] = md5.Hash;
+ }
+ }
+
+ ///
+ /// Determine and store hashes for all files and files within subdirectories recursively
+ ///
+ ///
+ ///
+ private void ProcessFiles(FileStream fs, CDReader reader, DiscDirectoryInfo dir)
+ {
+ // Process all files in current directory
+ foreach (DiscFileInfo fileInfo in dir.GetFiles())
+ {
+ string filePath = fileInfo.FullName;
+ // Try get the first sector from the file extents instead
+ DiscUtils.Streams.StreamExtent[] fileExtents = reader.PathToExtents(filePath);
+ if (fileExtents == null || fileExtents.Length <= 0)
+ throw new InvalidFileSystemException("Unexpected file extent in ISO filestream for " + filePath);
+ if (fileExtents.Length > 1)
+ throw new InvalidFileSystemException("Non-contiguous file detected");
+ long firstByte = fileExtents[0].Start;
+ long fileLength = fileExtents[0].Length;
+ FileKeys[FileCount] = firstByte / 2048;
+
+ // Determine whether file is in encrypted or decrypted region
+ bool encrypted = false;
+ for (int i = RegionCount - 1; i > 0; i--)
+ {
+ if (RegionStart[i] <= firstByte / 2048)
+ {
+ encrypted = i % 2 == 1;
+ break;
+ }
+ }
+
+ // Decrypt file if encrypted
+ if (encrypted)
+ {
+ FileHashes[FileCount] = NullMD5;
+ FileCount++;
+ continue;
+ }
+
+ // Start reading data from the beginning of the ISO file
+ fs.Seek(firstByte, SeekOrigin.Begin);
+ byte[] buf = new byte[SectorSize];
+ int numBytes;
+ // Read all data before the first data sector
+ MD5 md5 = MD5.Create();
+ for (long i = 0; i < (fileLength / SectorSize); i++)
+ {
+ numBytes = fs.Read(buf, 0, buf.Length);
+ // Check that an entire sector was read
+ if (numBytes < buf.Length)
+ throw new InvalidFileSystemException("Disc region ended unexpectedly");
+ md5.TransformBlock(buf, 0, numBytes, null, 0);
+ }
+ // Read remaining partial sector
+ if (fileLength % SectorSize != 0)
+ {
+ numBytes = fs.Read(buf, 0, (int)(fileLength % SectorSize));
+ md5.TransformBlock(buf, 0, numBytes, null, 0);
+ }
+
+ // Finalise and store MD5 hash
+ md5.TransformFinalBlock(buf, 0, 0);
+ FileHashes[FileCount] = md5.Hash;
+ FileCount++;
+ }
+
+ // Recursively process all subfolders of current directory
+ foreach (DiscDirectoryInfo dirInfo in dir.GetDirectories())
+ {
+ ProcessFiles(fs, reader, dirInfo);
+ }
+ }
+
+ ///
+ /// Recursively determines file count
+ ///
+ ///
+ private void CountFiles(DiscDirectoryInfo dir)
+ {
+ FileCount += (uint)dir.GetFiles().Length;
+ foreach (DiscDirectoryInfo dirInfo in dir.GetDirectories())
+ CountFiles(dirInfo);
}
///
diff --git a/LibIRD/PS3ISO.cs b/LibIRD/PS3ISO.cs
deleted file mode 100644
index 2778b85..0000000
--- a/LibIRD/PS3ISO.cs
+++ /dev/null
@@ -1,537 +0,0 @@
-using DiscUtils;
-using DiscUtils.Iso9660;
-using System;
-using System.IO;
-using System.IO.Compression;
-using System.Security.Cryptography;
-using System.Text;
-
-namespace LibIRD
-{
- ///
- /// PS3 ISO Data
- ///
- public class PS3ISO
- {
- #region Constants
-
- ///
- /// Blu-ray ISO sector size in bytes
- ///
- /// 2048
- private protected static readonly uint SectorSize = 2048;
-
- ///
- /// MD5 hash of null
- ///
- private protected static readonly byte[] NullMD5 = new byte[] { 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e };
-
- #endregion
-
- #region Properties
-
- ///
- /// The same value stored in PARAM.SFO / TITLE_ID
- ///
- /// 9 bytes, ASCII, stored without dashes
- public string TitleID { get; private protected set; }
-
- ///
- /// The same value stored in PARAM.SFO / TITLE
- ///
- /// ASCII
- public string Title { get; private protected set; }
-
- ///
- /// The same value stored in PARAM.SFO / PS3_SYSTEM_VER
- ///
- /// 4 bytes, ASCII, (e.g. "1.20", missing uses "0000"
- public string SystemVersion { get; private protected set; }
-
- ///
- /// The same value stored in PARAM.SFO / VERSION
- ///
- /// 5 bytes, ASCII, e.g. "01.20"
- public string GameVersion { get; private protected set; }
-
- ///
- /// The same value stored in PARAM.SFO / APP_VER
- ///
- /// 5 bytes, ASCII, e.g. "01.00"
- public string AppVersion { get; private protected set; }
-
- ///
- /// Length of the gzip-compressed header data
- ///
- public uint HeaderLength { get; private protected set; }
-
- ///
- /// Gzip-compressed header data
- ///
- public byte[] Header { get; private protected set; }
-
- ///
- /// Length of the gzip-compressed footer data
- ///
- public uint FooterLength { get; private protected set; }
-
- ///
- /// Gzip-compressed footer data
- ///
- public byte[] Footer { get; private protected set; }
-
- ///
- /// Number of complete regions in the image
- ///
- public byte RegionCount { get; private protected set; }
-
- ///
- /// MD5 hashes for all complete regions in the image
- ///
- /// regions, 16-bytes per hash
- public byte[][] RegionHashes { get; private protected set; }
-
- ///
- /// Number of files in the image
- ///
- public uint FileCount { get; private protected set; }
-
- ///
- /// Starting sector for each file
- ///
- /// files, alternating with each entry
- public long[] FileKeys { get; private protected set; }
-
- ///
- /// MD5 hashes for all decrypted files in the image
- ///
- /// files, 16-bytes per hash, alternating with each entry
- public byte[][] FileHashes { get; private protected set; }
-
- ///
- /// First byte of the PS3_DISC.SFB file
- ///
- /// Last byte to read when reading the header
- private long FirstDataSector { get; set; }
-
- ///
- /// First byte of the PS3UPDAT.PUP file
- ///
- private long UpdateOffset { get; set; }
-
- ///
- /// Last byte of the PS3UPDAT.PUP file
- ///
- /// Offset to use when reading the footer
- private long UpdateEnd { get; set; }
-
- ///
- /// First sector of each region
- ///
- private long[] RegionStart { get; set; }
-
- ///
- /// Last sector of each region
- ///
- private long[] RegionEnd { get; set; }
-
- #endregion
-
- #region Constructors
-
- ///
- /// Default generator for reading IRD files rather than ISOs
- ///
- private protected PS3ISO(string titleID,
- string title,
- string sysVersion,
- string gameVersion,
- string appVersion,
- byte[] header,
- byte[] footer,
- byte[][] regionHashes,
- long[] fileKeys,
- byte[][] fileHashes)
- {
- TitleID = titleID;
- Title = title;
- SystemVersion = sysVersion;
- GameVersion = gameVersion;
- AppVersion = appVersion;
- HeaderLength = (uint)header.Length;
- Header = header;
- FooterLength = (uint)footer.Length;
- Footer = footer;
- RegionCount = (byte) regionHashes.Length;
- RegionHashes = regionHashes;
- FileCount = (uint)fileKeys.Length;
- FileKeys = fileKeys;
- FileHashes = fileHashes;
- }
-
- ///
- /// Constructor for generating values from an ISO file
- ///
- /// Path to the ISO
- ///
- ///
- ///
- internal PS3ISO(string isoPath)
- {
- // Validate ISO path
- if (isoPath == null || isoPath.Length <= 0)
- throw new ArgumentNullException(nameof(isoPath));
-
- // Check file exists
- var iso = new FileInfo(isoPath);
- if (!iso.Exists)
- throw new FileNotFoundException(nameof(isoPath));
-
- // TODO: remove these initial values once they're set properly
-
- // Parse ISO file as a file stream
- using FileStream fs = new FileStream(isoPath, FileMode.Open, FileAccess.Read) ?? throw new FileNotFoundException(isoPath);
- // Validate ISO file stream
- if (!CDReader.Detect(fs))
- throw new InvalidFileSystemException("Not a valid ISO file");
-
- // New ISO Reader from DiscUtils
- CDReader reader = new(fs, true, true);
-
- // Read PS3 Metadata from PARAM.SFO
- using (DiscUtils.Streams.SparseStream s = reader.OpenFile("PS3_GAME\\PARAM.SFO", FileMode.Open, FileAccess.Read))
- {
- // Parse PARAM.SFO file
- ParamSFO paramSFO = new(s);
- // Store required values for IRD
- TitleID = paramSFO["TITLE_ID"];
- Title = paramSFO["TITLE"];
- GameVersion = paramSFO["VERSION"];
- AppVersion = paramSFO["APP_VER"];
- }
-
- // Determine system update version
- GetSystemVersion(fs, reader);
-
- // Read and compress the ISO header
- GetHeader(fs, reader);
-
- // Read and compress the ISO footer
- GetFooter(fs);
-
- // Process all regions on ISO
- HashRegions(fs);
-
- // TODO: Speed up program by hashing regions and files at the same time (read from filesystem only once)
-
- // Recursively count all files in ISO to allocate file arrays
- DiscDirectoryInfo rootDir = reader.GetDirectoryInfo("\\");
- FileCount = 0;
- CountFiles(rootDir);
- FileKeys = new long[FileCount];
- FileHashes = new byte[FileCount][];
-
- // Determine file offsets and hashes
- uint fileCount = FileCount;
- FileCount = 0;
- ProcessFiles(fs, reader, rootDir);
- if (FileCount != fileCount)
- throw new InvalidFileSystemException("Unexpected ISO filesystem error: ");
- Array.Sort(FileKeys, FileHashes);
- }
-
- #endregion
-
- #region Methods
-
- ///
- /// Retreives and stores the system version
- ///
- /// PS3UPDAT.PUP update file version number
- /// ISO filestream
- ///
- ///
- private void GetSystemVersion(FileStream fs, CDReader reader)
- {
- // Determine PUP file offset via cluster
- DiscUtils.Streams.Range[] updateClusters = reader.PathToClusters("\\PS3_UPDATE\\PS3UPDAT.PUP");
- if (updateClusters == null || updateClusters.Length <= 0)
- {
- // File too small for dedicated cluster, try get the offset from the file extents instead
- DiscUtils.Streams.StreamExtent[] updateExtents = reader.PathToExtents("\\PS3_UPDATE\\PS3UPDAT.PUP");
- if (updateExtents == null || updateExtents.Length <= 0)
- throw new InvalidFileSystemException("Unexpected PS3UPDAT.PUP file extent in ISO filestream");
- // PS3UPDAT.PUP file begins at start of first extent
- UpdateOffset = updateExtents[0].Start;
- // Update file ends at the last extent plus its length
- UpdateEnd = updateExtents[^1].Start + updateExtents[^1].Length;
- }
- else
- {
- // PS3UPDAT.PUP file begins at first byte of dedicated cluster
- UpdateOffset = updateClusters[0] != null ? SectorSize * updateClusters[0].Offset : 0;
- // Update file ends at the last byte of the last cluster
- UpdateEnd = SectorSize * (updateClusters[^1].Offset + updateClusters[^1].Count);
- }
-
- // Check PUP file Magic
- fs.Seek(UpdateOffset, SeekOrigin.Begin);
- byte[] pupMagic = new byte[5];
- fs.Read(pupMagic, 0, pupMagic.Length);
- // If magic is incorrect, set version to "0000" (unknown)
- if (Encoding.ASCII.GetString(pupMagic) != "SCEUF")
- SystemVersion = "0000";
- else
- {
- // Determine location of version string
- fs.Seek(UpdateOffset + 0x3E, SeekOrigin.Begin);
- byte[] offset = new byte[2];
- fs.Read(offset, 0, 2);
- // Move stream to PUP version string
- Array.Reverse(offset);
- ushort versionOffset = BitConverter.ToUInt16(offset, 0);
- fs.Seek(UpdateOffset + versionOffset, SeekOrigin.Begin);
- // Read version string
- byte[] version = new byte[4];
- fs.Read(version, 0, version.Length);
- // Set version string
- SystemVersion = Encoding.ASCII.GetString(version);
- }
- }
-
- ///
- /// Retreives and stores the header
- ///
- /// ISO filestream
- ///
- ///
- private void GetHeader(FileStream fs, CDReader reader)
- {
- // Determine the extent of the header via cluster (Sector 0 to first data sector)
- DiscUtils.Streams.Range[] sfbClusters = reader.PathToClusters("\\PS3_DISC.SFB");
- if (sfbClusters == null || sfbClusters.Length <= 0)
- {
- // File too small for dedicated cluster, try get the first sector from the file extents instead
- DiscUtils.Streams.StreamExtent[] sfbExtents = reader.PathToExtents("\\PS3_DISC.SFB");
- if (sfbExtents == null || sfbExtents.Length <= 0)
- throw new InvalidFileSystemException("Unexpected PS3UPDAT.PUP file extent in ISO filestream");
- FirstDataSector = sfbExtents[0].Start;
- }
- else
- {
- // End of header is at beginning of first byte of dedicated cluster
- FirstDataSector = sfbClusters[0] != null ? sfbClusters[0].Offset : 0;
- }
-
- // Begin a GZip stream to write header to
- using MemoryStream headerStream = new();
- using (GZipStream gzs = new(headerStream, CompressionLevel.SmallestSize))
- {
- // Start reading data from the beginning of the ISO file
- fs.Seek(0, SeekOrigin.Begin);
- byte[] buf = new byte[SectorSize];
- int numBytes;
-
- // Read all data before the first data sector
- for (int i = 0; i < FirstDataSector; i++)
- {
- numBytes = fs.Read(buf, 0, buf.Length);
- gzs.Write(buf, 0, numBytes);
- }
- }
-
- // Save stream to field
- Header = headerStream.ToArray();
- HeaderLength = (uint)Header.Length;
- }
-
- ///
- /// Retreives and stores the footer
- ///
- /// ISO filestream
- private void GetFooter(FileStream fs)
- {
- // Begin a GZip stream to write footer to
- using MemoryStream footerStream = new();
- using (GZipStream gzs = new(footerStream, CompressionLevel.SmallestSize))
- {
- // Start reading data from after last file
- fs.Seek(UpdateEnd, SeekOrigin.Begin);
- byte[] buf = new byte[SectorSize];
- int numBytes = (int)SectorSize;
-
- // Keep reading data until there is none left to read
- while (numBytes != 0)
- {
- numBytes = fs.Read(buf, 0, buf.Length);
- gzs.Write(buf, 0, numBytes);
- }
- }
-
- // Save stream to field
- Footer = footerStream.ToArray();
- FooterLength = (uint)Footer.Length;
- }
-
- ///
- /// Determines and stores the hashes for each disc region
- ///
- ///
- ///
- private void HashRegions(FileStream fs)
- {
- // Determine the number of unencryted regions
- fs.Seek(0, SeekOrigin.Begin);
- byte[] decRegionCount = new byte[4];
- fs.Read(decRegionCount, 0, 4);
-
- // Total number of regions is 2x number of unencrypted regions, minus 1
- RegionCount = (byte)(2 * ((uint)decRegionCount[3]) - 1);
- if (RegionCount <= 0)
- throw new InvalidFileSystemException("No regions detected in ISO");
- RegionHashes = new byte[RegionCount][];
- RegionStart = new long[RegionCount];
- RegionEnd = new long[RegionCount];
-
- // Determine the extent for each region
- byte[] regionSector = new byte[4];
- fs.Seek(8, SeekOrigin.Begin);
- fs.Read(regionSector, 0, 4);
- Array.Reverse(regionSector, 0, 4);
- for (int i = 0; i < RegionCount; i++)
- {
- // End sector of previous region is start of this region
- if (i % 2 == 1)
- RegionStart[i] = BitConverter.ToInt32(regionSector) + 1;
- else
- RegionStart[i] = BitConverter.ToInt32(regionSector);
- // Determine end sector offset of this region
- fs.Read(regionSector, 0, 4);
- Array.Reverse(regionSector, 0, 4);
- if (i % 2 == 1)
- RegionEnd[i] = BitConverter.ToInt32(regionSector) - 1;
- else
- RegionEnd[i] = BitConverter.ToInt32(regionSector);
- }
-
- // Remove header from first region
- RegionStart[0] = FirstDataSector;
- // Remove footer from last region
- RegionEnd[^1] = (UpdateEnd / SectorSize) - 1;
-
- // Determine MD5 hashes for each region
- using MD5 md5 = MD5.Create();
- byte[] buf = new byte[SectorSize];
- for (int i = 0; i < RegionCount; i++)
- {
- // Start reading data from first sector of region
- fs.Seek(SectorSize * RegionStart[i], SeekOrigin.Begin);
-
- // Compute MD5 hash for just the region portion of the ISO file
- int numBytes;
- for (long j = RegionStart[i]; j <= RegionEnd[i]; j++)
- {
- // Read one sector at a time
- numBytes = fs.Read(buf, 0, buf.Length);
- // Check that an entire sector was read
- if (numBytes < buf.Length)
- throw new InvalidFileSystemException("Disc region ended unexpectedly");
- // Process MD5 sum one sector at a time
- md5.TransformBlock(buf, 0, buf.Length, null, 0);
- }
-
- // Compute and store MD5 hash of region
- md5.TransformFinalBlock(buf, 0, 0);
- RegionHashes[i] = md5.Hash;
- }
- }
-
- ///
- /// Determine and store hashes for all files and files within subdirectories recursively
- ///
- ///
- ///
- private void ProcessFiles(FileStream fs, CDReader reader, DiscDirectoryInfo dir)
- {
- // Process all files in current directory
- foreach (DiscFileInfo fileInfo in dir.GetFiles())
- {
- string filePath = fileInfo.FullName;
- // Try get the first sector from the file extents instead
- DiscUtils.Streams.StreamExtent[] fileExtents = reader.PathToExtents(filePath);
- if (fileExtents == null || fileExtents.Length <= 0)
- throw new InvalidFileSystemException("Unexpected file extent in ISO filestream for " + filePath);
- if (fileExtents.Length > 1)
- throw new InvalidFileSystemException("Non-contiguous file detected");
- long firstByte = fileExtents[0].Start;
- long fileLength = fileExtents[0].Length;
- FileKeys[FileCount] = firstByte / 2048;
-
- // Determine whether file is in encrypted or decrypted region
- bool encrypted = false;
- for (int i = RegionCount - 1; i > 0; i--)
- {
- if (RegionStart[i] <= firstByte / 2048)
- {
- encrypted = i % 2 == 1;
- break;
- }
- }
-
- // Decrypt file if encrypted
- if (encrypted)
- {
- FileHashes[FileCount] = NullMD5;
- FileCount++;
- continue;
- }
-
- // Start reading data from the beginning of the ISO file
- fs.Seek(firstByte, SeekOrigin.Begin);
- byte[] buf = new byte[SectorSize];
- int numBytes;
- // Read all data before the first data sector
- MD5 md5 = MD5.Create();
- for (long i = 0; i < (fileLength / SectorSize); i++)
- {
- numBytes = fs.Read(buf, 0, buf.Length);
- // Check that an entire sector was read
- if (numBytes < buf.Length)
- throw new InvalidFileSystemException("Disc region ended unexpectedly");
- md5.TransformBlock(buf, 0, numBytes, null, 0);
- }
- // Read remaining partial sector
- if (fileLength % SectorSize != 0)
- {
- numBytes = fs.Read(buf, 0, (int)(fileLength % SectorSize));
- md5.TransformBlock(buf, 0, numBytes, null, 0);
- }
-
- // Finalise and store MD5 hash
- md5.TransformFinalBlock(buf, 0, 0);
- FileHashes[FileCount] = md5.Hash;
- FileCount++;
- }
-
- // Recursively process all subfolders of current directory
- foreach (DiscDirectoryInfo dirInfo in dir.GetDirectories())
- {
- ProcessFiles(fs, reader, dirInfo);
- }
- }
-
- ///
- /// Recursively determines file count
- ///
- ///
- private void CountFiles(DiscDirectoryInfo dir)
- {
- FileCount += (uint)dir.GetFiles().Length;
- foreach (DiscDirectoryInfo dirInfo in dir.GetDirectories())
- CountFiles(dirInfo);
- }
-
- #endregion
-
- }
-}
diff --git a/LibIRD/ReIRD.cs b/LibIRD/ReIRD.cs
index fb7def0..3ff4659 100644
--- a/LibIRD/ReIRD.cs
+++ b/LibIRD/ReIRD.cs
@@ -194,31 +194,19 @@ namespace LibIRD
///
public ReIRD(string isoPath, string getKeyLog) : base(isoPath, getKeyLog)
{
- // Validate ISO path
- if (isoPath == null || isoPath.Length <= 0)
- throw new ArgumentNullException(nameof(isoPath));
-
- // Check file exists
- var iso = new FileInfo(isoPath);
- if (!iso.Exists)
- throw new FileNotFoundException(nameof(isoPath));
-
- // Calculate size of ISO
- long size = iso.Length;
-
// Generate Unique Identifier using ISO CRC32
GenerateUID(isoPath);
// Generate Data 2 using Disc ID
byte[] d2 = Data2Key;
- GenerateD2(GenerateID(size));
+ GenerateD2(GenerateID(Size));
// Check that GetKey log matches expected Disc ID
if (!((ReadOnlySpan)Data2Key).SequenceEqual(d2))
throw new InvalidDataException("Unexpected Disc ID in .getkey.log");
// Generate Disc PIC
byte[] pic = PIC;
- GeneratePIC(size);
+ GeneratePIC(Size);
// Check that GetKey log matches expected PIC
if (!((ReadOnlySpan)PIC).SequenceEqual(pic))
throw new InvalidDataException("Unexpected PIC in .getkey.log");