diff --git a/BurnOutSharp.Builders/N3DS.cs b/BurnOutSharp.Builders/N3DS.cs index 62abd79b..b0a560e5 100644 --- a/BurnOutSharp.Builders/N3DS.cs +++ b/BurnOutSharp.Builders/N3DS.cs @@ -32,6 +32,27 @@ namespace BurnOutSharp.Builders return ParseCart(dataStream); } + /// + /// Parse a byte array into a CIA archive + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled CIA archive on success, null on error + 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; } + /// + /// Parse a Stream into a CIA archive + /// + /// Stream to parse + /// Filled CIA archive on success, null on error + 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; + } + /// /// Parse a Stream into an NCSD header /// @@ -711,6 +825,339 @@ namespace BurnOutSharp.Builders return romFSHeader; } + /// + /// Parse a Stream into a CIA header + /// + /// Stream to parse + /// Filled CIA header on success, null on error + 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; + } + + /// + /// Parse a Stream into a certificate + /// + /// Stream to parse + /// Filled certificate on success, null on error + 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; + } + + /// + /// Parse a Stream into a ticket + /// + /// Stream to parse + /// Filled ticket on success, null on error + 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; + } + + /// + /// Parse a Stream into a title metadata + /// + /// Stream to parse + /// Filled title metadata on success, null on error + 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; + } + + /// + /// Parse a Stream into a content info record + /// + /// Stream to parse + /// Filled content info record on success, null on error + 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; + } + + /// + /// Parse a Stream into a content chunk record + /// + /// Stream to parse + /// Filled content chunk record on success, null on error + 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; + } + + /// + /// Parse a Stream into a meta data + /// + /// Stream to parse + /// Filled meta data on success, null on error + 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 } } diff --git a/BurnOutSharp.Models/N3DS/CIA.cs b/BurnOutSharp.Models/N3DS/CIA.cs new file mode 100644 index 00000000..cdef9b55 --- /dev/null +++ b/BurnOutSharp.Models/N3DS/CIA.cs @@ -0,0 +1,50 @@ +namespace BurnOutSharp.Models.N3DS +{ + /// + /// 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. + /// + /// + public class CIA + { + /// + /// CIA header + /// + public CIAHeader Header { get; set; } + + /// + /// Certificate chain + /// + /// + /// https://www.3dbrew.org/wiki/CIA#Certificate_Chain + /// + public Certificate[] CertificateChain { get; set; } + + /// + /// Ticket + /// + public Ticket Ticket { get; set; } + + /// + /// TMD file data + /// + public TitleMetadata TMDFileData { get; set; } + + /// + /// Content file data + /// + public NCCHHeader[] Partitions { get; set; } + + /// + /// Meta file data (Not a necessary component) + /// + public MetaData MetaData { get; set; } + } +} \ No newline at end of file diff --git a/BurnOutSharp.Models/N3DS/CIAHeader.cs b/BurnOutSharp.Models/N3DS/CIAHeader.cs index aec997ad..a0806666 100644 --- a/BurnOutSharp.Models/N3DS/CIAHeader.cs +++ b/BurnOutSharp.Models/N3DS/CIAHeader.cs @@ -1,23 +1,12 @@ namespace BurnOutSharp.Models.N3DS { - /// - /// 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. - /// - /// + /// public sealed class CIAHeader { /// /// Archive header size, usually 0x2020 bytes /// - public int HeaderSize; + public uint HeaderSize; /// /// Type @@ -32,63 +21,31 @@ namespace BurnOutSharp.Models.N3DS /// /// Certificate chain size /// - public int CertificateChainSize; + public uint CertificateChainSize; /// /// Ticket size /// - public int TicketSize; + public uint TicketSize; /// /// TMD file size /// - public int TMDFileSize; + public uint TMDFileSize; /// /// Meta size (0 if no Meta data is present) /// - public int MetaSize; + public uint MetaSize; /// /// Content size /// - public long ContentSize; + public ulong ContentSize; /// /// Content Index /// public byte[] ContentIndex; - - #region Content Index - - /// - /// Certificate chain - /// - /// - /// https://www.3dbrew.org/wiki/CIA#Certificate_Chain - /// - public Certificate[] CertificateChain; - - /// - /// Ticket - /// - public Ticket Ticket; - - /// - /// TMD file data - /// - public TitleMetadata TMDFileData; - - /// - /// Content file data - /// - public NCCHHeader[] Partitions; - - /// - /// Meta file data (Not a necessary component) - /// - public MetaFile MetaFileData; - - #endregion } } \ No newline at end of file diff --git a/BurnOutSharp.Models/N3DS/Certificate.cs b/BurnOutSharp.Models/N3DS/Certificate.cs index 83a31c48..0d6f772d 100644 --- a/BurnOutSharp.Models/N3DS/Certificate.cs +++ b/BurnOutSharp.Models/N3DS/Certificate.cs @@ -29,6 +29,11 @@ namespace BurnOutSharp.Models.N3DS /// public byte[] Signature; + /// + /// Padding to align next data to 0x40 bytes + /// + public byte[] Padding; + /// /// Issuer /// @@ -49,18 +54,23 @@ namespace BurnOutSharp.Models.N3DS /// 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 /// /// Modulus /// - public byte[] Modulus; + public byte[] RSAModulus; /// /// Public Exponent /// - public uint PublicExponent; + public uint RSAPublicExponent; + + /// + /// Padding + /// + public byte[] RSAPadding; #endregion @@ -70,7 +80,12 @@ namespace BurnOutSharp.Models.N3DS /// /// Public Key /// - public byte[] PublicKey; + public byte[] ECCPublicKey; + + /// + /// Padding + /// + public byte[] ECCPadding; #endregion } diff --git a/BurnOutSharp.Models/N3DS/Enums.cs b/BurnOutSharp.Models/N3DS/Enums.cs index 1d4f4580..1d66dea2 100644 --- a/BurnOutSharp.Models/N3DS/Enums.cs +++ b/BurnOutSharp.Models/N3DS/Enums.cs @@ -189,7 +189,7 @@ namespace BurnOutSharp.Models.N3DS { RSA_4096 = 0x00000000, RSA_2048 = 0x01000000, - ECDSA = 0x02000000, + EllipticCurve = 0x02000000, } public enum ResourceLimitCategory diff --git a/BurnOutSharp.Models/N3DS/MetaFile.cs b/BurnOutSharp.Models/N3DS/MetaData.cs similarity index 88% rename from BurnOutSharp.Models/N3DS/MetaFile.cs rename to BurnOutSharp.Models/N3DS/MetaData.cs index 6962899a..618a4155 100644 --- a/BurnOutSharp.Models/N3DS/MetaFile.cs +++ b/BurnOutSharp.Models/N3DS/MetaData.cs @@ -1,11 +1,12 @@ namespace BurnOutSharp.Models.N3DS { /// - public sealed class MetaFile + public sealed class MetaData { /// /// Title ID dependency list - Taken from the application's ExHeader /// + /// TODO: Determine numeric format of each entry public byte[] TitleIDDependencyList; /// diff --git a/BurnOutSharp.Models/N3DS/Ticket.cs b/BurnOutSharp.Models/N3DS/Ticket.cs index 048990f6..a37c4cc4 100644 --- a/BurnOutSharp.Models/N3DS/Ticket.cs +++ b/BurnOutSharp.Models/N3DS/Ticket.cs @@ -27,6 +27,11 @@ namespace BurnOutSharp.Models.N3DS /// public byte[] Signature; + /// + /// Padding + /// + public byte[] Padding; + /// /// Issuer /// @@ -91,7 +96,7 @@ namespace BurnOutSharp.Models.N3DS /// /// Reserved /// - public ushort Reserved2; + public byte[] Reserved2; /// /// Ticket title version @@ -105,7 +110,7 @@ namespace BurnOutSharp.Models.N3DS /// /// Reserved /// - public ulong Reserved3; + public byte[] Reserved3; /// /// License Type @@ -113,8 +118,7 @@ namespace BurnOutSharp.Models.N3DS public byte LicenseType; /// - /// 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 /// public byte CommonKeyYIndex; @@ -149,14 +153,14 @@ namespace BurnOutSharp.Models.N3DS /// /// In demos, the first u32 in the "Limits" section is 0x4, then the second u32 is the max-playcount. /// - public int[] Limits; + public uint[] Limits; /// /// 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. /// - public int ContentIndexSize; + public uint ContentIndexSize; /// /// Content Index diff --git a/BurnOutSharp.Models/N3DS/TitleMetadata.cs b/BurnOutSharp.Models/N3DS/TitleMetadata.cs index b99c4ca1..cb56cc0c 100644 --- a/BurnOutSharp.Models/N3DS/TitleMetadata.cs +++ b/BurnOutSharp.Models/N3DS/TitleMetadata.cs @@ -28,10 +28,15 @@ namespace BurnOutSharp.Models.N3DS /// public byte[] Signature; + /// + /// Padding + /// + public byte[] Padding1; + /// /// Signature Issuer /// - public byte[] SignatureIssuer; + public string Issuer; /// /// Version @@ -86,7 +91,7 @@ namespace BurnOutSharp.Models.N3DS /// /// Reserved /// - public uint Reserved2; + public byte[] Reserved2; /// /// SRL Flag @@ -121,7 +126,7 @@ namespace BurnOutSharp.Models.N3DS /// /// Padding /// - public ushort Padding; + public byte[] Padding2; /// /// SHA-256 Hash of the Content Info Records