Fill in more CIA-specific objects

This commit is contained in:
Matt Nadareski
2021-09-27 10:26:22 -07:00
parent bea979dca5
commit bbbf71a603
7 changed files with 744 additions and 19 deletions

View File

@@ -66,6 +66,24 @@ namespace NDecrypt.N3DS
NewKeyYGenerator = 0x20,
}
internal enum ContentIndex : ushort
{
/// <summary>
/// Main Content (.CXI for 3DS executable content/.CFA for 3DS Data Archives/.SRL for TWL content)
/// </summary>
MainContent = 0x0000,
/// <summary>
/// Home Menu Manual (.CFA)
/// </summary>
HomeMenuManual = 0x0001,
/// <summary>
/// DLP Child Container (.CFA)
/// </summary>
DLPChildContainer = 0x0002,
}
internal enum ContentPlatform : byte
{
CTR = 0x01,
@@ -165,6 +183,13 @@ namespace NDecrypt.N3DS
MediaCardDevice2X = 0x07,
}
internal enum PublicKeyType : uint
{
RSA_4096 = 0x00000000,
RSA_2048 = 0x00000001,
ECDSA = 0x00000002,
}
internal enum ResourceLimitCategory
{
APPLICATION = 0,
@@ -173,10 +198,30 @@ namespace NDecrypt.N3DS
OTHER = 3,
}
internal enum SignatureType : uint
{
RSA_4096_SHA1 = 0x010000,
RSA_2048_SHA1 = 0x010001,
ECDSA_SHA1 = 0x010002,
RSA_4096_SHA256 = 0x010003,
RSA_2048_SHA256 = 0x010004,
ECDSA_SHA256 = 0x010005,
}
[Flags]
internal enum StorageInfoOtherAttributes : byte
{
NotUseROMFS = 0x01,
UseExtendedSavedataAccess = 0x02,
}
[Flags]
internal enum TMDContentType : ushort
{
Encrypted = 0x0001,
Disc = 0x0002,
CFM = 0x0004,
Optional = 0x4000,
Shared = 0x8000,
}
}

View File

@@ -2,6 +2,7 @@ using System.IO;
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/CIA
internal class CIAHeader
{
/// <summary>
@@ -44,35 +45,25 @@ namespace NDecrypt.N3DS.Headers
/// </summary>
public long ContentSize { get; private set; }
/// <summary>
/// Content Index
/// </summary>
public byte[] ContentIndex { get; private set; }
#region Content Index
/// <summary>
/// Certificate chain
/// </summary>
/// <remarks>
/// https://www.3dbrew.org/wiki/Certificates
/// https://www.3dbrew.org/wiki/Ticket#Certificate_Chain
/// https://www.3dbrew.org/wiki/CIA#Certificate_Chain
/// </remarks>
public byte[] CertificateChain { get; set; }
public Certificate[] CertificateChain { get; set; }
/// <summary>
/// Ticket
/// </summary>
/// <remarks>
/// https://www.3dbrew.org/wiki/Ticket#Structure
/// </remarks>
public byte[] Ticket { get; set; }
public Ticket Ticket { get; set; }
/// <summary>
/// TMD file data
/// </summary>
/// <remarks>
/// https://www.3dbrew.org/wiki/Title_metadata
/// </remarks>
public byte[] TMDFileData { get; set; }
public TitleMetadata TMDFileData { get; set; }
/// <summary>
/// Content file data
@@ -87,6 +78,8 @@ namespace NDecrypt.N3DS.Headers
/// </summary>
public MetaFile MetaFileData { get; set; }
#endregion
/// <summary>
/// Read from a stream and get a CIA header, if possible
/// </summary>
@@ -106,11 +99,15 @@ namespace NDecrypt.N3DS.Headers
header.TMDFileSize = reader.ReadInt32();
header.MetaSize = reader.ReadInt32();
header.ContentSize = reader.ReadInt64();
header.ContentIndex = reader.ReadBytes(0x2000);
header.CertificateChain = reader.ReadBytes(header.CertificateChainSize);
header.Ticket = reader.ReadBytes(header.TicketSize);
header.TMDFileData = reader.ReadBytes(header.TMDFileSize);
header.CertificateChain = new Certificate[3];
for (int i = 0; i < 3; i++)
{
header.CertificateChain[i] = Certificate.Read(reader);
}
header.Ticket = Ticket.Read(reader, header.TicketSize);
header.TMDFileData = TitleMetadata.Read(reader, header.TMDFileSize);
header.ContentFileData = reader.ReadBytes((int)header.ContentSize);
if (header.MetaSize > 0)

View File

@@ -0,0 +1,137 @@
using System.IO;
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/Certificates
internal class Certificate
{
/// <summary>
/// Signature Type
/// </summary>
public SignatureType SignatureType { get; private set; }
/// <summary>
/// Signature size
/// </summary>
public ushort SignatureSize { get; private set; }
/// <summary>
/// Padding size
/// </summary>
public byte PaddingSize { get; private set; }
/// <summary>
/// Signature
/// </summary>
public byte[] Signature { get; private set; }
/// <summary>
/// Issuer
/// </summary>
public byte[] Issuer { get; private set; }
/// <summary>
/// Key Type
/// </summary>
public PublicKeyType KeyType { get; private set; }
/// <summary>
/// Name
/// </summary>
public byte[] Name { get; private set; }
/// <summary>
/// Expiration time as UNIX Timestamp, used at least for CTCert
/// </summary>
public uint ExpirationTime { get; private set; }
// This contains the Public Key(i.e. Modulus & Public Exponent)
#region RSA
/// <summary>
/// Modulus
/// </summary>
public byte[] Modulus { get; private set; }
/// <summary>
/// Public Exponent
/// </summary>
public uint PublicExponent { get; private set; }
#endregion
// This contains the ECC public key, and is as follows:
#region ECC
/// <summary>
/// Public Key
/// </summary>
public byte[] PublicKey { get; private set; }
#endregion
/// <summary>
/// Read from a stream and get certificate, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <returns>Certificate object, null on error</returns>
public static Certificate Read(BinaryReader reader)
{
Certificate ct = new Certificate();
try
{
ct.SignatureType = (SignatureType)reader.ReadUInt32();
switch (ct.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
case SignatureType.RSA_4096_SHA256:
ct.SignatureSize = 0x200;
ct.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
case SignatureType.RSA_2048_SHA256:
ct.SignatureSize = 0x100;
ct.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
case SignatureType.ECDSA_SHA256:
ct.SignatureSize = 0x03C;
ct.PaddingSize = 0x40;
break;
}
ct.Signature = reader.ReadBytes(ct.SignatureSize);
reader.ReadBytes(ct.PaddingSize); // Padding
ct.Issuer = reader.ReadBytes(0x40);
ct.KeyType = (PublicKeyType)reader.ReadUInt32();
ct.Name = reader.ReadBytes(0x40);
ct.ExpirationTime = reader.ReadUInt32();
switch (ct.KeyType)
{
case PublicKeyType.RSA_4096:
ct.Modulus = reader.ReadBytes(0x200);
ct.PublicExponent = reader.ReadUInt32();
reader.ReadBytes(0x34); // Padding
break;
case PublicKeyType.RSA_2048:
ct.Modulus = reader.ReadBytes(0x100);
ct.PublicExponent = reader.ReadUInt32();
reader.ReadBytes(0x34); // Padding
break;
case PublicKeyType.ECDSA:
ct.PublicKey = reader.ReadBytes(0x3C);
reader.ReadBytes(0x3C); // Padding
break;
}
return ct;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,59 @@
using System.IO;
namespace NDecrypt.N3DS.Headers
{
internal class ContentChunkRecord
{
/// <summary>
/// Content id
/// </summary>
public uint ContentId { get; private set; }
/// <summary>
/// Content index
/// </summary>
/// <remarks>
/// This does not apply to DLC.
/// </remarks>
public ContentIndex ContentIndex { get; private set; }
/// <summary>
/// Content type
/// </summary>
public TMDContentType ContentType { get; private set; }
/// <summary>
/// Content size
/// </summary>
public ulong ContentSize { get; private set; }
/// <summary>
/// SHA-256 hash
/// </summary>
public byte[] SHA256Hash { get; private set; }
/// <summary>
/// Read from a stream and get content chunk record, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <returns>Content chunk record object, null on error</returns>
public static ContentChunkRecord Read(BinaryReader reader)
{
ContentChunkRecord ccr = new ContentChunkRecord();
try
{
ccr.ContentId = reader.ReadUInt32();
ccr.ContentIndex = (ContentIndex)reader.ReadUInt16();
ccr.ContentType = (TMDContentType)reader.ReadUInt16();
ccr.ContentSize = reader.ReadUInt64();
ccr.SHA256Hash = reader.ReadBytes(0x20);
return ccr;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,44 @@
using System.IO;
namespace NDecrypt.N3DS.Headers
{
internal class ContentInfoRecord
{
/// <summary>
/// Content index offset
/// </summary>
public ushort ContentIndexOffset { get; private set; }
/// <summary>
/// Content command count [k]
/// </summary>
public ushort ContentCommandCount { get; private set; }
/// <summary>
/// SHA-256 hash of the next k content records that have not been hashed yet
/// </summary>
public byte[] UnhashedContentRecordsSHA256Hash { get; private set; }
/// <summary>
/// Read from a stream and get content info record, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <returns>Content info record object, null on error</returns>
public static ContentInfoRecord Read(BinaryReader reader)
{
ContentInfoRecord cir = new ContentInfoRecord();
try
{
cir.ContentIndexOffset = reader.ReadUInt16();
cir.ContentCommandCount = reader.ReadUInt16();
cir.UnhashedContentRecordsSHA256Hash = reader.ReadBytes(0x20);
return cir;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,229 @@
using System.IO;
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/Ticket
internal class Ticket
{
/// <summary>
/// Signature Type
/// </summary>
public SignatureType SignatureType { get; private set; }
/// <summary>
/// Signature size
/// </summary>
public ushort SignatureSize { get; private set; }
/// <summary>
/// Padding size
/// </summary>
public byte PaddingSize { get; private set; }
/// <summary>
/// Signature
/// </summary>
public byte[] Signature { get; private set; }
/// <summary>
/// Issuer
/// </summary>
public byte[] Issuer { get; private set; }
/// <summary>
/// ECC PublicKey
/// </summary>
public byte[] ECCPublicKey { get; private set; }
/// <summary>
/// Version (For 3DS this is always 1)
/// </summary>
public byte Version { get; private set; }
/// <summary>
/// CaCrlVersion
/// </summary>
public byte CaCrlVersion { get; private set; }
/// <summary>
/// SignerCrlVersion
/// </summary>
public byte SignerCrlVersion { get; private set; }
/// <summary>
/// TitleKey (normal-key encrypted using one of the common keyYs; see below)
/// </summary>
/// <remarks>
/// The titlekey is decrypted by using the AES engine with the ticket common-key keyslot.
/// The keyY is selected through an index (ticket offset 0xB1) into a plaintext array
/// of 6 keys ("common keyYs") stored in the data section of Process9. AES-CBC mode is used
/// where the IV is the big-endian titleID. Note that on a retail unit index0 is a retail keyY,
/// while on a dev-unit index0 is the dev common-key which is a normal-key.
/// (On retail for these keyYs, the hardware key-scrambler is used)
///
/// The titlekey is used to decrypt content downloaded from the CDN using 128-bit AES-CBC with
/// the content index (as big endian u16, padded with trailing zeroes) as the IV.
/// </remarks>
public byte[] TitleKey { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte Reserved1 { get; private set; }
/// <summary>
/// TicketID
/// </summary>
public ulong TicketID { get; private set; }
/// <summary>
/// ConsoleID
/// </summary>
public uint ConsoleID { get; private set; }
/// <summary>
/// TitleID
/// </summary>
public ulong TitleID { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public ushort Reserved2 { get; private set; }
/// <summary>
/// Ticket title version
/// </summary>
/// <remarks>
/// The Ticket Title Version is generally the same as the title version stored in the
/// Title Metadata. Although it doesn't have to match the TMD version to be valid.
/// </remarks>
public ushort TicketTitleVersion { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public ulong Reserved3 { get; private set; }
/// <summary>
/// License Type
/// </summary>
public byte LicenseType { get; private set; }
/// <summary>
/// Index to the common keyY used for this ticket, usually 0x1 for retail system titles;
/// see below.
/// </summary>
public byte CommonKeyYIndex { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved4 { get; private set; }
/// <summary>
/// eShop Account ID?
/// </summary>
public uint eShopAccountID { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte Reserved5 { get; private set; }
/// <summary>
/// Audit
/// </summary>
public byte Audit { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved6 { get; private set; }
/// <summary>
/// Limits
/// </summary>
/// <remarks>
/// In demos, the first u32 in the "Limits" section is 0x4, then the second u32 is the max-playcount.
/// </remarks>
public byte[] Limits { get; private set; }
/// <summary>
/// Content Index
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public byte[] ContentIndex { get; private set; }
/// <summary>
/// Read from a stream and get ticket, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="ticketSize">Ticket size from the header</param>
/// <returns>Ticket object, null on error</returns>
public static Ticket Read(BinaryReader reader, int ticketSize)
{
Ticket tk = new Ticket();
try
{
tk.SignatureType = (SignatureType)reader.ReadUInt32();
switch (tk.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
case SignatureType.RSA_4096_SHA256:
tk.SignatureSize = 0x200;
tk.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
case SignatureType.RSA_2048_SHA256:
tk.SignatureSize = 0x100;
tk.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
case SignatureType.ECDSA_SHA256:
tk.SignatureSize = 0x03C;
tk.PaddingSize = 0x40;
break;
}
tk.Signature = reader.ReadBytes(tk.SignatureSize);
reader.ReadBytes(tk.PaddingSize); // Padding
tk.Issuer = reader.ReadBytes(0x40);
tk.ECCPublicKey = reader.ReadBytes(0x3C);
tk.Version = reader.ReadByte();
tk.CaCrlVersion = reader.ReadByte();
tk.SignerCrlVersion = reader.ReadByte();
tk.TitleKey = reader.ReadBytes(0x10);
tk.Reserved1 = reader.ReadByte();
tk.TicketID = reader.ReadUInt64();
tk.ConsoleID = reader.ReadUInt32();
tk.TitleID = reader.ReadUInt64();
tk.Reserved2 = reader.ReadUInt16();
tk.TicketTitleVersion = reader.ReadUInt16();
tk.Reserved3 = reader.ReadUInt64();
tk.LicenseType = reader.ReadByte();
tk.CommonKeyYIndex = reader.ReadByte();
tk.Reserved4 = reader.ReadBytes(0x2A);
tk.eShopAccountID = reader.ReadUInt32();
tk.Reserved5 = reader.ReadByte();
tk.Audit = reader.ReadByte();
tk.Reserved6 = reader.ReadBytes(0x42);
tk.Limits = reader.ReadBytes(0x40);
if (ticketSize - 0x164 > 0)
tk.ContentIndex = reader.ReadBytes(ticketSize - 0x164);
return tk;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,214 @@
using System.IO;
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/Title_metadata
internal class TitleMetadata
{
/// <summary>
/// Signature Type
/// </summary>
public SignatureType SignatureType { get; private set; }
/// <summary>
/// Signature size
/// </summary>
public ushort SignatureSize { get; private set; }
/// <summary>
/// Padding size
/// </summary>
public byte PaddingSize { get; private set; }
/// <summary>
/// Signature
/// </summary>
public byte[] Signature { get; private set; }
/// <summary>
/// Signature Issuer
/// </summary>
public byte[] SignatureIssuer { get; private set; }
/// <summary>
/// Version
/// </summary>
public byte Version { get; private set; }
/// <summary>
/// CaCrlVersion
/// </summary>
public byte CaCrlVersion { get; private set; }
/// <summary>
/// SignerCrlVersion
/// </summary>
public byte SignerCrlVersion { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte Reserved1 { get; private set; }
/// <summary>
/// System Version
/// </summary>
public ulong SystemVersion { get; private set; }
/// <summary>
/// TitleID
/// </summary>
public ulong TitleID { get; private set; }
/// <summary>
/// Title Type
/// </summary>
public uint TitleType { get; private set; }
/// <summary>
/// Group ID
/// </summary>
public ushort GroupID { get; private set; }
/// <summary>
/// Save Data Size in Little Endian (Bytes) (Also SRL Public Save Data Size)
/// </summary>
public uint SaveDataSize { get; private set; }
/// <summary>
/// SRL Private Save Data Size in Little Endian (Bytes)
/// </summary>
public uint SRLPrivateSaveDataSize { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public uint Reserved2 { get; private set; }
/// <summary>
/// SRL Flag
/// </summary>
public byte SRLFlag { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved3 { get; private set; }
/// <summary>
/// Access Rights
/// </summary>
public uint AccessRights { get; private set; }
/// <summary>
/// Title Version
/// </summary>
public ushort TitleVersion { get; private set; }
/// <summary>
/// Content Count
/// </summary>
public ushort ContentCount { get; private set; }
/// <summary>
/// Boot Content
/// </summary>
public ushort BootContent { get; private set; }
/// <summary>
/// Padding
/// </summary>
public ushort Padding { get; private set; }
/// <summary>
/// SHA-256 Hash of the Content Info Records
/// </summary>
public byte[] SHA256HashContentInfoRecords { get; private set; }
/// <summary>
/// There are 64 of these records, usually only the first is used.
/// </summary>
public ContentInfoRecord[] ContentInfoRecords { get; private set; }
/// <summary>
/// There is one of these for each content contained in this title.
/// (Determined by "Content Count" in the TMD Header).
/// </summary>
public ContentChunkRecord[] ContentChunkRecords { get; private set; }
/// <summary>
/// Read from a stream and get ticket metadata, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="metadataSize">Metadata size from the header</param>
/// <returns>Title metadata object, null on error</returns>
public static TitleMetadata Read(BinaryReader reader, int metadataSize)
{
TitleMetadata tm = new TitleMetadata();
try
{
tm.SignatureType = (SignatureType)reader.ReadUInt32();
switch (tm.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
case SignatureType.RSA_4096_SHA256:
tm.SignatureSize = 0x200;
tm.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
case SignatureType.RSA_2048_SHA256:
tm.SignatureSize = 0x100;
tm.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
case SignatureType.ECDSA_SHA256:
tm.SignatureSize = 0x03C;
tm.PaddingSize = 0x40;
break;
}
tm.Signature = reader.ReadBytes(tm.SignatureSize);
reader.ReadBytes(tm.PaddingSize); // Padding
tm.SignatureIssuer = reader.ReadBytes(0x40);
tm.Version = reader.ReadByte();
tm.CaCrlVersion = reader.ReadByte();
tm.SignerCrlVersion = reader.ReadByte();
tm.Reserved1 = reader.ReadByte();
tm.SystemVersion = reader.ReadUInt64();
tm.TitleID = reader.ReadUInt64();
tm.TitleType = reader.ReadUInt32();
tm.GroupID = reader.ReadUInt16();
tm.SaveDataSize = reader.ReadUInt32();
tm.SRLPrivateSaveDataSize = reader.ReadUInt32();
tm.Reserved2 = reader.ReadUInt32();
tm.SRLFlag = reader.ReadByte();
tm.Reserved3 = reader.ReadBytes(0x31);
tm.AccessRights = reader.ReadUInt32();
tm.TitleVersion = reader.ReadUInt16();
tm.ContentCount = reader.ReadUInt16();
tm.BootContent = reader.ReadUInt16();
tm.Padding = reader.ReadUInt16();
tm.SHA256HashContentInfoRecords = reader.ReadBytes(0x20);
tm.ContentInfoRecords = new ContentInfoRecord[64];
for (int i = 0; i < 64; i++)
{
tm.ContentInfoRecords[i] = ContentInfoRecord.Read(reader);
}
tm.ContentChunkRecords = new ContentChunkRecord[tm.ContentCount];
for (int i = 0; i < tm.ContentCount; i++)
{
tm.ContentChunkRecords[i] = ContentChunkRecord.Read(reader);
}
return tm;
}
catch
{
return null;
}
}
}
}