Add project files.

This commit is contained in:
Deterous
2023-10-25 17:48:29 +13:00
commit e542dcbfd9
10 changed files with 1842 additions and 0 deletions
+421
View File
@@ -0,0 +1,421 @@
using System;
using System.IO;
using System.IO.Compression;
using System.IO.Hashing;
using System.Security.Cryptography;
using System.Text;
namespace LibIRD
{
/// <summary>
/// IRD Generation, Reading and Writing
/// </summary>
public class IRD
{
#region Constants
/// <summary>
/// "3IRD"
/// </summary>
///
protected static readonly byte[] Magic = new byte[] { 0x33, 0x49, 0x52, 0x44 };
#endregion
#region Publicly Settable Properties
/// <summary>
/// IRD Specification Version
/// </summary>
/// <remarks>1 byte, Versions 6-9 are currently supported</remarks>
public byte Version
{
get => _version;
set
{
if (value == 6 || value == 7 || value == 8 || value == 9)
_version = value;
}
}
public byte _version = 9; // Default to latest IRD version = 9
/// <summary>
/// Unique Identifier
/// </summary>
/// <remarks>Redump-style IRDs use CRC32 hash of the ISO as the UID</remarks>
public uint UID { get; set; } = 0x00000000; // Default to zeroed UID
/// <summary>
/// Extra Config, usually 0x0000
/// </summary>
public ushort ExtraConfig { get; set; } = 0x0000;
/// <summary>
/// Attachments, usually 0x0000
/// </summary>
public ushort Attachments { get; set; } = 0x0000;
/// <summary>
/// D1 key
/// </summary>
/// <remarks>16 bytes</remarks>
public byte[] Data1Key { get; set; }
/// <summary>
/// D2 key
/// </summary>
/// <remarks>16 bytes</remarks>
public byte[] Data2Key { get; set; }
/// <summary>
/// Uncompressed PIC data
/// </summary>
/// <remarks>115 bytes, before D1/D2 keys on version 9</remarks>
public byte[] PIC { get; set; }
#endregion
#region Privately Settable Properties
/// <summary>
/// The same value stored in PARAM.SFO / TITLE_ID
/// </summary>
/// <remarks>9 bytes, ASCII, stored without dashes</remarks>
public string TitleID { get; private set; }
/// <summary>
/// The same value stored in PARAM.SFO / TITLE
/// </summary>
/// <remarks>ASCII</remarks>
public string Title { get; private set; }
/// <summary>
/// The same value stored in PARAM.SFO / PS3_SYSTEM_VER
/// </summary>
/// <remarks>4 bytes, ASCII, (e.g. "1.20", missing uses "0000"</remarks>
public string SystemVersion { get; private set; }
/// <summary>
/// The same value stored in PARAM.SFO / VERSION
/// </summary>
/// <remarks>5 bytes, ASCII, e.g. "01.20"</remarks>
public string GameVersion { get; private set; }
/// <summary>
/// The same value stored in PARAM.SFO / APP_VER
/// </summary>
/// <remarks>5 bytes, ASCII, e.g. "01.00"</remarks>
public string AppVersion { get; private set; }
/// <summary>
/// Length of the gzip-compressed header data
/// </summary>
public uint HeaderLength { get; private set; }
/// <summary>
/// Gzip-compressed header data
/// </summary>
public byte[] Header { get; private set; }
/// <summary>
/// Length of the gzip-compressed footer data
/// </summary>
public uint FooterLength { get; private set; }
/// <summary>
/// Gzip-compressed footer data
/// </summary>
public byte[] Footer { get; private set; }
/// <summary>
/// Number of complete regions in the image
/// </summary>
public byte RegionCount { get; private set; }
/// <summary>
/// MD5 hashes for all complete regions in the image
/// </summary>
/// <remarks><see cref="RegionCount"/> regions, 16-bytes per hash</remarks>
public byte[][] RegionHashes { get; private set; }
/// <summary>
/// Number of decrypted files in the image
/// </summary>
public uint FileCount { get; private set; }
/// <summary>
/// Starting sector for each decrypted file
/// </summary>
/// <remarks><see cref="FileCount"/> files, alternating with each <see cref="FileHashes"/> entry</remarks>
public ulong[] FileKeys { get; private set; }
/// <summary>
/// MD5 hashes for all decrypted files in the image
/// </summary>
/// <remarks><see cref="FileHashes"/> files, 16-bytes per hash, alternating with each <see cref="FileHashes"/> entry</remarks>
public byte[][] FileHashes { get; private set; }
/// <summary>
/// IRD content CRC
/// </summary>
public uint CRC { get; private set; }
#endregion
#region Generate Fields
/// <summary>
/// Generates Data 1, via AES-128 CBC decryption of a Disc Key
/// </summary>
/// <param name="key">Byte array containing AES encrypted Disc Key</param>
/// <exception cref="ArgumentException"></exception>
protected void GenerateD1(byte[] key)
{
// Validate key
if (key == null || key.Length != 16)
throw new ArgumentException("Disc Key must be a byte array of length 16", nameof(key));
// TODO: AES decryption
Data2Key = key;
}
/// <summary>
/// Generates Data 2, via AES-128 CBC encryption of a Disc ID
/// </summary>
/// <param name="id">Byte array containing AES decrypted Disc ID</param>
/// <exception cref="ArgumentException"></exception>
protected void GenerateD2(byte[] id)
{
// Validate id
if (id == null || id.Length != 16)
throw new ArgumentException("Disc ID must be a byte array of length 16", nameof(id));
// TODO: AES encryption
Data2Key = id;
}
#endregion
#region Constructors
// TODO: Constructor using D1 and D2 directly, rather than Disc Key / Disc ID
/// <summary>
/// Default constructor for internal derived classes only: resulting object not in usable state
/// </summary>
private protected IRD()
{
// Assumes that derived class will set private variables and generate in its own constructor
}
/// <summary>
/// Constructor that reads an existing IRD file and stores its fields in the IRD object
/// </summary>
/// <param name="irdPath">Full path to IRD file to read data from</param>
public IRD(string irdPath)
{
Read(irdPath);
}
/// <summary>
/// Public Constructor must be called with required fields
/// </summary>
/// <param name="discPath">Full path to the disc drive / mounted ISO</param>
/// <param name="discKey">Disc Key, byte array of length 16</param>
/// <param name="discID">Disc ID, byte array of length 16</param>
/// <param name="discPIC">Disc PIC, bye array of length 115</param>
public IRD(string discPath, byte[] discKey, byte[] discID, byte[] discPIC)
{
// Store IRD fields that cannot be determined from the disc drive or mounted ISO
GenerateD1(discKey);
GenerateD2(discID);
PIC = discPIC;
// Generate the remaining IRD fields from the disc drive or mounted ISO
Generate(discPath);
}
#endregion
#region Methods
/// <summary>
/// Generate IRD fields from a disc drive or mounted ISO
/// </summary>
/// <param name="discPath">Full path to the disc drive / mounted ISO</param>
/// <exception cref="ArgumentException"></exception>
public void Generate(string discPath)
{
// TODO: Check that discPath is a valid disc drive/ISO
if (discPath == null)
throw new ArgumentNullException(nameof(discPath));
// TODO: Code the difficult part (remove these initial values)
TitleID = "ABCD12345";
Title = "Title";
SystemVersion = "01.00";
GameVersion = "01.00";
AppVersion = "01.00";
HeaderLength = 1;
Header = new byte[HeaderLength];
Header[0] = 0x00;
FooterLength = 1;
Footer = new byte[FooterLength];
Footer[0] = 0x00;
RegionCount = 1;
RegionHashes = new byte[RegionCount][];
RegionHashes[0] = new byte[] { 0x00 };
FileCount = 1;
FileKeys = new ulong[FileCount];
FileKeys[0] = 0x00;
FileHashes = new byte[FileCount][];
FileHashes[0] = new byte[] { 0x00 };
FileHashes[0][0] = 0x00;
}
/// <summary>
/// Read IRD data from file, store fields into class
/// </summary>
/// <param name="irdPath">Full path to IRD file to read data from</param>
/// <exception cref="ArgumentException"></exception>
public void Read(string irdPath)
{
// TODO: Check irdPath is a valid IRD file path
if (irdPath == null || !File.Exists(irdPath))
throw new ArgumentException("IRD File Path invalid", nameof(irdPath));
// TODO: Implement reading IRD file data into fields (remove these intial values)
TitleID = "ABCD12345";
Title = "Title";
SystemVersion = "01.00";
GameVersion = "01.00";
AppVersion = "01.00";
HeaderLength = 1;
Header = new byte[HeaderLength];
Header[0] = 0x00;
FooterLength = 1;
Footer = new byte[FooterLength];
Footer[0] = 0x00;
RegionCount = 1;
RegionHashes = new byte[RegionCount][];
RegionHashes[0] = new byte[] { 0x00 };
FileCount = 1;
FileKeys = new ulong[FileCount];
FileKeys[0] = 0x00;
FileHashes = new byte[FileCount][];
FileHashes[0] = new byte[] { 0x00 };
FileHashes[0][0] = 0x00;
}
/// <summary>
/// Write IRD data to file
/// </summary>
/// <param name="filePath">Full path to IRD file to be written to</param>
public void Write(string filePath)
{
// TODO: Check filePath is a valid path for a new IRD file
// Create the IRD file stream
using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
// Create a GZipped IRD file stream
using (GZipStream outStream = new GZipStream(fs, CompressionLevel.Optimal))
{
// Keep track of the little-endian 32-bit "IEEE 802.3" CRC value
Crc32 crcStream = new Crc32();
// TODO: Run as an async Task?
crcStream.Append(outStream);
// Write IRD data to stream in order
using (BinaryWriter bw = new BinaryWriter(outStream))
{
// IRD File Signature
bw.Write(Magic);
// IRD File Version
bw.Write(_version);
// PARAM.SFO / TITLE_ID
byte[] titleIDBuf = Encoding.ASCII.GetBytes(TitleID);
bw.Write(titleIDBuf, 0, 9);
// PARAM.SFO / TITLE
bw.Write(Title);
// PARAM.SFO / PS3_SYSTEM_VER
byte[] systemVersionBuf = Encoding.ASCII.GetBytes(SystemVersion);
bw.Write(systemVersionBuf, 0, 4);
// PARAM.SFO / VERSION
byte[] buf = Encoding.ASCII.GetBytes(GameVersion);
byte[] gameVersionBuf = new byte[5];
Array.Copy(buf, 0, gameVersionBuf, 0, buf.Length);
bw.Write(gameVersionBuf, 0, 5);
// PARAM.SFO / APP_VER
buf = Encoding.ASCII.GetBytes(AppVersion);
byte[] appVersionBuf = new byte[5];
Array.Copy(buf, 0, appVersionBuf, 0, buf.Length);
bw.Write(appVersionBuf, 0, 5);
// IRD Unique Identifier, for version 7
if (_version == 7)
bw.Write(UID);
// Compress Header
bw.Write(HeaderLength);
bw.Write(Header, 0, (int)HeaderLength);
// Compress Footer
bw.Write(FooterLength);
bw.Write(Footer, 0, (int)FooterLength);
// Number of regions hashed
bw.Write(RegionCount);
// Hashes for each region
for (int i = 0; i < RegionCount; i++)
bw.Write(RegionHashes[i], 0, 16);
// Number of files hashed
bw.Write(FileCount);
// Hashes for each file
for (int i = 0; i < FileCount; i++)
{
bw.Write(FileKeys[i]);
bw.Write(FileHashes[i], 0, 16);
}
// Reserved fields
bw.Write(ExtraConfig);
bw.Write(Attachments);
// PIC data is placed here for Version 9
if (_version >= 9)
bw.Write(PIC, 0, 115);
// Disc Authentication keys
bw.Write(Data1Key, 0, 16);
bw.Write(Data2Key, 0, 16);
// PIC data is placed here prior to Version 9
if (_version < 9)
bw.Write(PIC, 0, 115);
// IRD Unique Identifier, for versions after 7
if (_version > 7)
bw.Write(UID);
}
// Calculate final CRC32
CRC = BitConverter.ToUInt32(crcStream.GetCurrentHash(), 0);
// Write final CRC32 to stream
outStream.Write(BitConverter.GetBytes(CRC), 0, 4);
}
}
}
#endregion
}
}
+13
View File
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net6.0;net7.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<Version>0.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Hashing" Version="7.0.0" />
</ItemGroup>
</Project>
+242
View File
@@ -0,0 +1,242 @@
using System;
namespace LibIRD
{
#region Enumerations
/// <summary>
/// Type used to define all known PS3 disc regions
/// </summary>
public enum Region
{
// Null region
NONE = 0,
// Japan and Asia
Asia = 0x01, // BLAS/BCAS serials
Japan = Asia, // BLJM/BCJS serials
Korea = Asia, // BLKS/BCKS serials
// USA and Canada (NA)
NorthAmerica = 0x03,
USA = NorthAmerica,
Canada = NorthAmerica,
// Europe, Middle East, and Africa (EMEA)
Europe = 0x04,
MiddleEast = Europe,
Africa = Europe,
// Australia and New Zealand (Oceania)
Australia = 0x06, // Some releases use Region.Europe instead
NewZealand = Australia, // Some releases use Region.Europe instead
// Brazil and Latin America (LATAM)
LatinAmerica = 0x09, // e.g. Portuguese + Spanish release
Brazil = LatinAmerica,
// Mexico is often Region.NorthAmerica
// Russia and Eastern Europe
EasternEurope = 0x0A, // e.g. Russian + Polish language release
Russia = EasternEurope,
// Poland is Region.Europe
}
#endregion
/// <summary>
/// Create Reproducible Redump-style IRD file from an ISO, CRC32 and Disc Key
/// </summary>
public class ReIRD : IRD
{
#region Constants
/// <summary>
/// Size of a blu-ray layer in bytes (BD-25 max size)
/// </summary>
/// <remarks>Can be used as the default PS3 layerbreak value</remarks>
private const long BDLayerSize = 25025314816; // 12219392 sectors
#endregion
#region Property Generation
/// <summary>
/// Generates a Disc ID given a size and region, where region is a single byte
/// </summary>
/// <returns>Valid Disc ID, byte array length 16</returns>
private static byte[] GenerateID(long size, Region region = Region.NONE)
{
if (size > BDLayerSize) // if BD-50, Disc ID is fixed
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
else // else if BD-25, Disc ID has a byte referring to disc region
{
return new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0x00, 0x02, 0x00, (byte)region, 0x00, 0x00, 0x00, 0x01 };
}
}
/// <summary>
/// Generates the PIC data for a given ISO size in bytes
/// </summary>
/// <param name="size">Total ISO size in number of bytes</param>
/// <param name="layerbreak">Layer break value, byte at which disc layers are split across</param>
/// <param name="exactIRD">True to generate a PIC in 3k3y style (0x03 at 115th byte for BD-50 discs)</param>
/// <exception cref="ArgumentException"></exception>
private void GeneratePIC(long size, long layerbreak = BDLayerSize, bool exactIRD = false)
{
// Validate size
if (size == 0 || (size % 2048) != 0)
throw new ArgumentException("ISO Size in bytes must be a positive integer multiple of 2048", nameof(size));
// Validate layerbreak
if (layerbreak == 0 || layerbreak >= size)
throw new ArgumentException("Layerbreak in bytes must be a positive integer less than the ISO Size", nameof(size));
// TODO: Generate correct PICs for Hybrid PS3 discs (BD-50 with layerbreak value other than 12219392)
if (size > BDLayerSize) // if BD-50
{
// num_sectors + layer_sector_end (0x00100000) + sectors_between_layers (0x01358C00 - 0x00CA73FE) - 3
byte[] total_sectors = BitConverter.GetBytes((uint)(size / 2048 + 8067071));
// Initial portion of PIC (24 bytes)
PIC = new byte[]{
// [4098 bytes] [2x 0x00] ["DI"] [v1] [10units] [DI num]
0x10, 0x02, 0x00, 0x00, 0x44, 0x49, 0x01, 0x10, 0x00, 0x00, 0x20, 0x00,
// ["BDR"] [2 layers]
0x42, 0x44, 0x4F, 0x01, 0x21, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc (4 bytes)
total_sectors[3], total_sectors[2], total_sectors[1], total_sectors[0],
// 1st Layer sector start location (4 bytes)
0x00, 0x10, 0x00, 0x00,
// 1st Layer sector end location (4 bytes)
0x00, 0xCA, 0x73, 0xFE,
// 32 bytes of zeros
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Initial portion of PIC again, for 2nd layer
// ["DI"] [v1] [11unit][DI num]
0x44, 0x49, 0x01, 0x11, 0x00, 0x01, 0x20, 0x00,
// ["BDR"] [2 layers]
0x42, 0x44, 0x4F, 0x01, 0x21, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc
total_sectors[3], total_sectors[2], total_sectors[1], total_sectors[0],
// 2nd Layer sector start location
0x01, 0x35, 0x8C, 0x00,
// 2nd Layer sector end location
0x01, 0xEF, 0xFF, 0xFE,
// Remaining 32 bytes are zeroes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// 3k3y style: 0x03 at last byte
if (exactIRD)
PIC[114] = 0x03;
}
else // if BD-25
{
// Total sectors used on disc: num_sectors + layer_sector_end (0x00100000) - 1
byte[] total_sectors = BitConverter.GetBytes((uint)(size / 2048 + 1048575));
// Layer sector end location: num_sectors + layer_sector_end (0x00100000) - 2
byte[] end_sector = BitConverter.GetBytes((uint)(size / 2048 + 1048574));
// Initial portion of PIC (24 bytes)
PIC = new byte[]{ 0x10, 0x02, 0x00, 0x00, 0x44, 0x49, 0x01, 0x08, 0x00, 0x00, 0x20, 0x00,
0x42, 0x44, 0x4F, 0x01, 0x11, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// Total sectors used on disc (4 bytes)
total_sectors[3], total_sectors[2], total_sectors[1], total_sectors[0],
// Layer sector start location (4 bytes)
0x00, 0x10, 0x00, 0x00,
// Layer sector end location (4 bytes)
end_sector[3], end_sector[2], end_sector[1], end_sector[0],
// Remaining 79 bytes are zeroes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
}
}
/// <summary>
/// Generates the UID field
/// </summary>
/// <param name="crc32">Redump ISO CRC32 hash</param>
/// <exception cref="ArgumentException"></exception>
private void GenerateUID(byte[] crc32)
{
// Validate CRC32
if (crc32 == null || crc32.Length != 8)
throw new ArgumentException("CRC32 hash must be an byte array of length 8", nameof(crc32));
// Redump ISO CRC32 hash is used as the Unique ID in the IRD
UID = BitConverter.ToUInt32(crc32, 0);
}
#endregion
#region Constructors
/// <summary>
/// Constructor with required redump-style IRD fields
/// </summary>
/// <param name="size">ISO Size in bytes, a multiple of 2048</param>
/// <param name="crc32">CRC32 hash of the redump ISO</param>
/// <param name="key">Disc Key, redump-style (AES encrypted Data 1)</param>
public ReIRD(long size, byte[] crc32, byte[] key)
{
// Generate Unique Identifier using ISO CRC32
GenerateUID(crc32);
// Generate Data 1 using Disc Key
GenerateD1(key);
// Generate Data 2 using Disc ID
GenerateD2(GenerateID(size));
// Generate Disc PIC
GeneratePIC(size);
}
/// <summary>
/// Constructor with additional region to generate a specific Disc ID
/// </summary>
/// <param name="size">ISO Size in bytes, a multiple of 2048</param>
/// <param name="crc32">CRC32 hash of the redump ISO</param>
/// <param name="key">Disc Key, redump-style (AES encrypted Data 1)</param>
/// <param name="region">Disc Region</param>
public ReIRD(long size, byte[] crc32, byte[] key, Region region)
{
// Generate Unique Identifier using ISO CRC32
GenerateUID(crc32);
// Generate Data 1 using Disc Key
GenerateD1(key);
// Generate Data 2 using Disc ID
GenerateD2(GenerateID(size, region));
// Generate Disc PIC
GeneratePIC(size);
}
#endregion
#region Methods
/// <summary>
/// Generate and write the redump-style IRD file, after any optional IRD fields have been set
/// </summary>
/// <param name="discPath">Full path to the disc drive / mounted ISO</param>
/// <param name="irdPath">Full path to IRD file to be written to</param>
public void Create(string discPath, string irdPath)
{
// Generate the IRD hashes
Generate(discPath);
// Write to IRD file
Write(irdPath);
}
#endregion
}
}