Add CIA builder

This commit is contained in:
Matt Nadareski
2023-01-09 09:53:57 -08:00
parent 07a926e50c
commit b5d318013b
8 changed files with 545 additions and 66 deletions

View File

@@ -32,6 +32,27 @@ namespace BurnOutSharp.Builders
return ParseCart(dataStream);
}
/// <summary>
/// Parse a byte array into a CIA archive
/// </summary>
/// <param name="data">Byte array to parse</param>
/// <param name="offset">Offset into the byte array</param>
/// <returns>Filled CIA archive on success, null on error</returns>
public static CIA ParseCIA(byte[] data, int offset)
{
// If the data is invalid
if (data == null)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and parse that
MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset);
return ParseCIA(dataStream);
}
#endregion
#region Stream Data
@@ -190,6 +211,99 @@ namespace BurnOutSharp.Builders
return cart;
}
/// <summary>
/// Parse a Stream into a CIA archive
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled CIA archive on success, null on error</returns>
public static CIA ParseCIA(Stream data)
{
// If the data is invalid
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
return null;
// If the offset is out of bounds
if (data.Position < 0 || data.Position >= data.Length)
return null;
// Cache the current offset
int initialOffset = (int)data.Position;
// Create a new CIA archive to fill
var cia = new CIA();
#region CIA Header
// Try to parse the header
var header = ParseCIAHeader(data);
if (header == null)
return null;
// Set the CIA archive header
cia.Header = header;
#endregion
#region Certificate Chain
// Create the certificate chain
cia.CertificateChain = new Certificate[3];
// Try to parse the certificates
for (int i = 0; i < 3; i++)
{
var certificate = ParseCertificate(data);
if (certificate == null)
return null;
cia.CertificateChain[i] = certificate;
}
#endregion
#region Ticket
// Try to parse the ticket
var ticket = ParseTicket(data);
if (ticket == null)
return null;
// Set the ticket
cia.Ticket = ticket;
#endregion
#region Title Metadata
// Try to parse the title metadata
var titleMetadata = ParseTitleMetadata(data);
if (titleMetadata == null)
return null;
// Set the title metadata
cia.TMDFileData = titleMetadata;
#endregion
#region Meta Data
// If we have a meta data
if (header.MetaSize > 0)
{
// Try to parse the meta
var meta = ParseMetaData(data);
if (meta == null)
return null;
// Set the meta
cia.MetaData = meta;
}
#endregion
return cia;
}
/// <summary>
/// Parse a Stream into an NCSD header
/// </summary>
@@ -711,6 +825,339 @@ namespace BurnOutSharp.Builders
return romFSHeader;
}
/// <summary>
/// Parse a Stream into a CIA header
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled CIA header on success, null on error</returns>
private static CIAHeader ParseCIAHeader(Stream data)
{
// TODO: Use marshalling here instead of building
CIAHeader ciaHeader = new CIAHeader();
ciaHeader.HeaderSize = data.ReadUInt32();
ciaHeader.Type = data.ReadUInt16();
ciaHeader.Version = data.ReadUInt16();
ciaHeader.CertificateChainSize = data.ReadUInt32();
ciaHeader.TicketSize = data.ReadUInt32();
ciaHeader.TMDFileSize = data.ReadUInt32();
ciaHeader.MetaSize = data.ReadUInt32();
ciaHeader.ContentSize = data.ReadUInt64();
ciaHeader.ContentIndex = data.ReadBytes(0x2000);
return ciaHeader;
}
/// <summary>
/// Parse a Stream into a certificate
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled certificate on success, null on error</returns>
private static Certificate ParseCertificate(Stream data)
{
// TODO: Use marshalling here instead of building
Certificate certificate = new Certificate();
certificate.SignatureType = (SignatureType)data.ReadUInt32();
switch (certificate.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
certificate.SignatureSize = 0x200;
certificate.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
certificate.SignatureSize = 0x100;
certificate.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
certificate.SignatureSize = 0x3C;
certificate.PaddingSize = 0x40;
break;
case SignatureType.RSA_4096_SHA256:
certificate.SignatureSize = 0x200;
certificate.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA256:
certificate.SignatureSize = 0x100;
certificate.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA256:
certificate.SignatureSize = 0x3C;
certificate.PaddingSize = 0x40;
break;
default:
return null;
}
certificate.Signature = data.ReadBytes(certificate.SignatureSize);
certificate.Padding = data.ReadBytes(certificate.PaddingSize);
byte[] issuer = data.ReadBytes(0x40);
certificate.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
certificate.KeyType = (PublicKeyType)data.ReadUInt32();
byte[] name = data.ReadBytes(0x40);
certificate.Name = Encoding.ASCII.GetString(name).TrimEnd('\0');
certificate.ExpirationTime = data.ReadUInt32();
switch (certificate.KeyType)
{
case PublicKeyType.RSA_4096:
certificate.RSAModulus = data.ReadBytes(0x200);
certificate.RSAPublicExponent = data.ReadUInt32();
certificate.RSAPadding = data.ReadBytes(0x34);
break;
case PublicKeyType.RSA_2048:
certificate.RSAModulus = data.ReadBytes(0x100);
certificate.RSAPublicExponent = data.ReadUInt32();
certificate.RSAPadding = data.ReadBytes(0x34);
break;
case PublicKeyType.EllipticCurve:
certificate.ECCPublicKey = data.ReadBytes(0x3C);
certificate.ECCPadding = data.ReadBytes(0x3C);
break;
default:
return null;
}
return certificate;
}
/// <summary>
/// Parse a Stream into a ticket
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ticket on success, null on error</returns>
private static Ticket ParseTicket(Stream data)
{
// TODO: Use marshalling here instead of building
Ticket ticket = new Ticket();
ticket.SignatureType = (SignatureType)data.ReadUInt32();
switch (ticket.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
ticket.SignatureSize = 0x200;
ticket.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
ticket.SignatureSize = 0x100;
ticket.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
ticket.SignatureSize = 0x3C;
ticket.PaddingSize = 0x40;
break;
case SignatureType.RSA_4096_SHA256:
ticket.SignatureSize = 0x200;
ticket.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA256:
ticket.SignatureSize = 0x100;
ticket.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA256:
ticket.SignatureSize = 0x3C;
ticket.PaddingSize = 0x40;
break;
default:
return null;
}
ticket.Signature = data.ReadBytes(ticket.SignatureSize);
ticket.Padding = data.ReadBytes(ticket.PaddingSize);
byte[] issuer = data.ReadBytes(0x40);
ticket.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
ticket.ECCPublicKey = data.ReadBytes(0x3C);
ticket.Version = data.ReadByteValue();
ticket.CaCrlVersion = data.ReadByteValue();
ticket.SignerCrlVersion = data.ReadByteValue();
ticket.TitleKey = data.ReadBytes(0x10);
ticket.Reserved1 = data.ReadByteValue();
ticket.TicketID = data.ReadUInt64();
ticket.ConsoleID = data.ReadUInt32();
ticket.TitleID = data.ReadUInt64();
ticket.Reserved2 = data.ReadBytes(2);
ticket.TicketTitleVersion = data.ReadUInt16();
ticket.Reserved3 = data.ReadBytes(8);
ticket.LicenseType = data.ReadByteValue();
ticket.CommonKeyYIndex = data.ReadByteValue();
ticket.Reserved4 = data.ReadBytes(0x2A);
ticket.eShopAccountID = data.ReadUInt32();
ticket.Reserved5 = data.ReadByteValue();
ticket.Audit = data.ReadByteValue();
ticket.Reserved6 = data.ReadBytes(0x42);
ticket.Limits = new uint[0x10];
for (int i = 0; i < ticket.Limits.Length; i++)
{
ticket.Limits[i] = data.ReadUInt32();
}
// Seek to the content index size
data.Seek(4, SeekOrigin.Current);
// Read the size (big-endian)
byte[] contentIndexSize = data.ReadBytes(4);
Array.Reverse(contentIndexSize);
ticket.ContentIndexSize = BitConverter.ToUInt32(contentIndexSize, 0);
// Seek back to the start of the content index
data.Seek(-8, SeekOrigin.Current);
ticket.ContentIndex = data.ReadBytes((int)ticket.ContentIndexSize);
ticket.CertificateChain = new Certificate[2];
for (int i = 0; i < 2; i++)
{
var certificate = ParseCertificate(data);
if (certificate == null)
return null;
ticket.CertificateChain[i] = certificate;
}
return ticket;
}
/// <summary>
/// Parse a Stream into a title metadata
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled title metadata on success, null on error</returns>
private static TitleMetadata ParseTitleMetadata(Stream data)
{
// TODO: Use marshalling here instead of building
TitleMetadata titleMetadata = new TitleMetadata();
titleMetadata.SignatureType = (SignatureType)data.ReadUInt32();
switch (titleMetadata.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
titleMetadata.SignatureSize = 0x200;
titleMetadata.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
titleMetadata.SignatureSize = 0x100;
titleMetadata.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
titleMetadata.SignatureSize = 0x3C;
titleMetadata.PaddingSize = 0x40;
break;
case SignatureType.RSA_4096_SHA256:
titleMetadata.SignatureSize = 0x200;
titleMetadata.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA256:
titleMetadata.SignatureSize = 0x100;
titleMetadata.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA256:
titleMetadata.SignatureSize = 0x3C;
titleMetadata.PaddingSize = 0x40;
break;
default:
return null;
}
titleMetadata.Signature = data.ReadBytes(titleMetadata.SignatureSize);
titleMetadata.Padding1 = data.ReadBytes(titleMetadata.PaddingSize);
byte[] issuer = data.ReadBytes(0x40);
titleMetadata.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
titleMetadata.Version = data.ReadByteValue();
titleMetadata.CaCrlVersion = data.ReadByteValue();
titleMetadata.SignerCrlVersion = data.ReadByteValue();
titleMetadata.Reserved1 = data.ReadByteValue();
titleMetadata.SystemVersion = data.ReadUInt64();
titleMetadata.TitleID = data.ReadUInt64();
titleMetadata.TitleType = data.ReadUInt32();
titleMetadata.GroupID = data.ReadUInt16();
titleMetadata.SaveDataSize = data.ReadUInt32();
titleMetadata.SRLPrivateSaveDataSize = data.ReadUInt32();
titleMetadata.Reserved2 = data.ReadBytes(4);
titleMetadata.SRLFlag = data.ReadByteValue();
titleMetadata.Reserved3 = data.ReadBytes(0x31);
titleMetadata.AccessRights = data.ReadUInt32();
titleMetadata.TitleVersion = data.ReadUInt16();
titleMetadata.ContentCount = data.ReadUInt16();
titleMetadata.BootContent = data.ReadUInt16();
titleMetadata.Padding2 = data.ReadBytes(2);
titleMetadata.SHA256HashContentInfoRecords = data.ReadBytes(0x20);
titleMetadata.ContentInfoRecords = new ContentInfoRecord[64];
for (int i = 0; i < 64; i++)
{
titleMetadata.ContentInfoRecords[i] = ParseContentInfoRecord(data);
}
titleMetadata.ContentChunkRecords = new ContentChunkRecord[titleMetadata.ContentCount];
for (int i = 0; i < titleMetadata.ContentCount; i++)
{
titleMetadata.ContentChunkRecords[i] = ParseContentChunkRecord(data);
}
titleMetadata.CertificateChain = new Certificate[2];
for (int i = 0; i < 2; i++)
{
var certificate = ParseCertificate(data);
if (certificate == null)
return null;
titleMetadata.CertificateChain[i] = certificate;
}
return titleMetadata;
}
/// <summary>
/// Parse a Stream into a content info record
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled content info record on success, null on error</returns>
private static ContentInfoRecord ParseContentInfoRecord(Stream data)
{
// TODO: Use marshalling here instead of building
ContentInfoRecord contentInfoRecord = new ContentInfoRecord();
contentInfoRecord.ContentIndexOffset = data.ReadUInt16();
contentInfoRecord.ContentCommandCount = data.ReadUInt16();
contentInfoRecord.UnhashedContentRecordsSHA256Hash = data.ReadBytes(0x20);
return contentInfoRecord;
}
/// <summary>
/// Parse a Stream into a content chunk record
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled content chunk record on success, null on error</returns>
private static ContentChunkRecord ParseContentChunkRecord(Stream data)
{
// TODO: Use marshalling here instead of building
ContentChunkRecord contentChunkRecord = new ContentChunkRecord();
contentChunkRecord.ContentId = data.ReadUInt32();
contentChunkRecord.ContentIndex = (ContentIndex)data.ReadUInt16();
contentChunkRecord.ContentType = (TMDContentType)data.ReadUInt16();
contentChunkRecord.ContentSize = data.ReadUInt64();
contentChunkRecord.SHA256Hash = data.ReadBytes(0x20);
return contentChunkRecord;
}
/// <summary>
/// Parse a Stream into a meta data
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled meta data on success, null on error</returns>
private static MetaData ParseMetaData(Stream data)
{
// TODO: Use marshalling here instead of building
MetaData metaData = new MetaData();
metaData.TitleIDDependencyList = data.ReadBytes(0x180);
metaData.Reserved1 = data.ReadBytes(0x180);
metaData.CoreVersion = data.ReadUInt32();
metaData.Reserved2 = data.ReadBytes(0xFC);
metaData.IconData = data.ReadBytes(0x36C0);
return metaData;
}
#endregion
}
}

View File

@@ -0,0 +1,50 @@
namespace BurnOutSharp.Models.N3DS
{
/// <summary>
/// CIA stands for CTR Importable Archive. This format allows the installation of
/// titles to the 3DS. CIA files and titles on Nintendo's CDN contain identical data.
/// As a consequence, valid CIA files can be generated from CDN content. This also
/// means CIA files can contain anything that titles on Nintendo's CDN can contain.
///
/// Under normal circumstances CIA files are used where downloading a title is
/// impractical or not possible. Such as distributing a Download Play child, or
/// installing forced Gamecard updates. Those CIA(s) are stored by the titles in
/// question, in an auxiliary CFA file.
/// </summary>
/// <see href="https://www.3dbrew.org/wiki/CIA"/>
public class CIA
{
/// <summary>
/// CIA header
/// </summary>
public CIAHeader Header { get; set; }
/// <summary>
/// Certificate chain
/// </summary>
/// <remarks>
/// https://www.3dbrew.org/wiki/CIA#Certificate_Chain
/// </remarks>
public Certificate[] CertificateChain { get; set; }
/// <summary>
/// Ticket
/// </summary>
public Ticket Ticket { get; set; }
/// <summary>
/// TMD file data
/// </summary>
public TitleMetadata TMDFileData { get; set; }
/// <summary>
/// Content file data
/// </summary>
public NCCHHeader[] Partitions { get; set; }
/// <summary>
/// Meta file data (Not a necessary component)
/// </summary>
public MetaData MetaData { get; set; }
}
}

View File

@@ -1,23 +1,12 @@
namespace BurnOutSharp.Models.N3DS
{
/// <summary>
/// CIA stands for CTR Importable Archive. This format allows the installation of
/// titles to the 3DS. CIA files and titles on Nintendo's CDN contain identical data.
/// As a consequence, valid CIA files can be generated from CDN content. This also
/// means CIA files can contain anything that titles on Nintendo's CDN can contain.
///
/// Under normal circumstances CIA files are used where downloading a title is
/// impractical or not possible. Such as distributing a Download Play child, or
/// installing forced Gamecard updates. Those CIA(s) are stored by the titles in
/// question, in an auxiliary CFA file.
/// </summary>
/// <see href="https://www.3dbrew.org/wiki/CIA"/>
/// <see href="https://www.3dbrew.org/wiki/CIA#CIA_Header"/>
public sealed class CIAHeader
{
/// <summary>
/// Archive header size, usually 0x2020 bytes
/// </summary>
public int HeaderSize;
public uint HeaderSize;
/// <summary>
/// Type
@@ -32,63 +21,31 @@ namespace BurnOutSharp.Models.N3DS
/// <summary>
/// Certificate chain size
/// </summary>
public int CertificateChainSize;
public uint CertificateChainSize;
/// <summary>
/// Ticket size
/// </summary>
public int TicketSize;
public uint TicketSize;
/// <summary>
/// TMD file size
/// </summary>
public int TMDFileSize;
public uint TMDFileSize;
/// <summary>
/// Meta size (0 if no Meta data is present)
/// </summary>
public int MetaSize;
public uint MetaSize;
/// <summary>
/// Content size
/// </summary>
public long ContentSize;
public ulong ContentSize;
/// <summary>
/// Content Index
/// </summary>
public byte[] ContentIndex;
#region Content Index
/// <summary>
/// Certificate chain
/// </summary>
/// <remarks>
/// https://www.3dbrew.org/wiki/CIA#Certificate_Chain
/// </remarks>
public Certificate[] CertificateChain;
/// <summary>
/// Ticket
/// </summary>
public Ticket Ticket;
/// <summary>
/// TMD file data
/// </summary>
public TitleMetadata TMDFileData;
/// <summary>
/// Content file data
/// </summary>
public NCCHHeader[] Partitions;
/// <summary>
/// Meta file data (Not a necessary component)
/// </summary>
public MetaFile MetaFileData;
#endregion
}
}

View File

@@ -29,6 +29,11 @@ namespace BurnOutSharp.Models.N3DS
/// </summary>
public byte[] Signature;
/// <summary>
/// Padding to align next data to 0x40 bytes
/// </summary>
public byte[] Padding;
/// <summary>
/// Issuer
/// </summary>
@@ -49,18 +54,23 @@ namespace BurnOutSharp.Models.N3DS
/// </summary>
public uint ExpirationTime;
// This contains the Public Key(i.e. Modulus & Public Exponent)
#region RSA
// This contains the Public Key (i.e. Modulus & Public Exponent)
#region RSA-4096 and RSA-2048
/// <summary>
/// Modulus
/// </summary>
public byte[] Modulus;
public byte[] RSAModulus;
/// <summary>
/// Public Exponent
/// </summary>
public uint PublicExponent;
public uint RSAPublicExponent;
/// <summary>
/// Padding
/// </summary>
public byte[] RSAPadding;
#endregion
@@ -70,7 +80,12 @@ namespace BurnOutSharp.Models.N3DS
/// <summary>
/// Public Key
/// </summary>
public byte[] PublicKey;
public byte[] ECCPublicKey;
/// <summary>
/// Padding
/// </summary>
public byte[] ECCPadding;
#endregion
}

View File

@@ -189,7 +189,7 @@ namespace BurnOutSharp.Models.N3DS
{
RSA_4096 = 0x00000000,
RSA_2048 = 0x01000000,
ECDSA = 0x02000000,
EllipticCurve = 0x02000000,
}
public enum ResourceLimitCategory

View File

@@ -1,11 +1,12 @@
namespace BurnOutSharp.Models.N3DS
{
/// <see href="https://www.3dbrew.org/wiki/CIA#Meta"/>
public sealed class MetaFile
public sealed class MetaData
{
/// <summary>
/// Title ID dependency list - Taken from the application's ExHeader
/// </summary>
/// TODO: Determine numeric format of each entry
public byte[] TitleIDDependencyList;
/// <summary>

View File

@@ -27,6 +27,11 @@ namespace BurnOutSharp.Models.N3DS
/// </summary>
public byte[] Signature;
/// <summary>
/// Padding
/// </summary>
public byte[] Padding;
/// <summary>
/// Issuer
/// </summary>
@@ -91,7 +96,7 @@ namespace BurnOutSharp.Models.N3DS
/// <summary>
/// Reserved
/// </summary>
public ushort Reserved2;
public byte[] Reserved2;
/// <summary>
/// Ticket title version
@@ -105,7 +110,7 @@ namespace BurnOutSharp.Models.N3DS
/// <summary>
/// Reserved
/// </summary>
public ulong Reserved3;
public byte[] Reserved3;
/// <summary>
/// License Type
@@ -113,8 +118,7 @@ namespace BurnOutSharp.Models.N3DS
public byte LicenseType;
/// <summary>
/// Index to the common keyY used for this ticket, usually 0x1 for retail system titles;
/// see below.
/// Index to the common keyY used for this ticket, usually 0x1 for retail system titles
/// </summary>
public byte CommonKeyYIndex;
@@ -149,14 +153,14 @@ namespace BurnOutSharp.Models.N3DS
/// <remarks>
/// In demos, the first u32 in the "Limits" section is 0x4, then the second u32 is the max-playcount.
/// </remarks>
public int[] Limits;
public uint[] Limits;
/// <summary>
/// The Content Index of a ticket has its own size defined within itself,
/// with seemingly a minimal of 20 bytes, the second u32 in big endian defines
/// the full value of X.
/// </summary>
public int ContentIndexSize;
public uint ContentIndexSize;
/// <summary>
/// Content Index

View File

@@ -28,10 +28,15 @@ namespace BurnOutSharp.Models.N3DS
/// </summary>
public byte[] Signature;
/// <summary>
/// Padding
/// </summary>
public byte[] Padding1;
/// <summary>
/// Signature Issuer
/// </summary>
public byte[] SignatureIssuer;
public string Issuer;
/// <summary>
/// Version
@@ -86,7 +91,7 @@ namespace BurnOutSharp.Models.N3DS
/// <summary>
/// Reserved
/// </summary>
public uint Reserved2;
public byte[] Reserved2;
/// <summary>
/// SRL Flag
@@ -121,7 +126,7 @@ namespace BurnOutSharp.Models.N3DS
/// <summary>
/// Padding
/// </summary>
public ushort Padding;
public byte[] Padding2;
/// <summary>
/// SHA-256 Hash of the Content Info Records