diff --git a/NDecrypt/N3DS/Enums.cs b/NDecrypt/N3DS/Enums.cs index 2911ea5..70b869d 100644 --- a/NDecrypt/N3DS/Enums.cs +++ b/NDecrypt/N3DS/Enums.cs @@ -66,6 +66,24 @@ namespace NDecrypt.N3DS NewKeyYGenerator = 0x20, } + internal enum ContentIndex : ushort + { + /// + /// Main Content (.CXI for 3DS executable content/.CFA for 3DS Data Archives/.SRL for TWL content) + /// + MainContent = 0x0000, + + /// + /// Home Menu Manual (.CFA) + /// + HomeMenuManual = 0x0001, + + /// + /// DLP Child Container (.CFA) + /// + 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, + } } diff --git a/NDecrypt/N3DS/Headers/CIAHeader.cs b/NDecrypt/N3DS/Headers/CIAHeader.cs index 0a95c65..3a304c4 100644 --- a/NDecrypt/N3DS/Headers/CIAHeader.cs +++ b/NDecrypt/N3DS/Headers/CIAHeader.cs @@ -2,6 +2,7 @@ using System.IO; namespace NDecrypt.N3DS.Headers { + // https://www.3dbrew.org/wiki/CIA internal class CIAHeader { /// @@ -44,35 +45,25 @@ namespace NDecrypt.N3DS.Headers /// public long ContentSize { get; private set; } - /// - /// Content Index - /// - public byte[] ContentIndex { get; private set; } + #region Content Index /// /// Certificate chain /// /// - /// https://www.3dbrew.org/wiki/Certificates - /// https://www.3dbrew.org/wiki/Ticket#Certificate_Chain + /// https://www.3dbrew.org/wiki/CIA#Certificate_Chain /// - public byte[] CertificateChain { get; set; } + public Certificate[] CertificateChain { get; set; } /// /// Ticket /// - /// - /// https://www.3dbrew.org/wiki/Ticket#Structure - /// - public byte[] Ticket { get; set; } + public Ticket Ticket { get; set; } /// /// TMD file data /// - /// - /// https://www.3dbrew.org/wiki/Title_metadata - /// - public byte[] TMDFileData { get; set; } + public TitleMetadata TMDFileData { get; set; } /// /// Content file data @@ -87,6 +78,8 @@ namespace NDecrypt.N3DS.Headers /// public MetaFile MetaFileData { get; set; } + #endregion + /// /// Read from a stream and get a CIA header, if possible /// @@ -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) diff --git a/NDecrypt/N3DS/Headers/Certificate.cs b/NDecrypt/N3DS/Headers/Certificate.cs new file mode 100644 index 0000000..b9accbb --- /dev/null +++ b/NDecrypt/N3DS/Headers/Certificate.cs @@ -0,0 +1,137 @@ +using System.IO; + +namespace NDecrypt.N3DS.Headers +{ + // https://www.3dbrew.org/wiki/Certificates + internal class Certificate + { + /// + /// Signature Type + /// + public SignatureType SignatureType { get; private set; } + + /// + /// Signature size + /// + public ushort SignatureSize { get; private set; } + + /// + /// Padding size + /// + public byte PaddingSize { get; private set; } + + /// + /// Signature + /// + public byte[] Signature { get; private set; } + + /// + /// Issuer + /// + public byte[] Issuer { get; private set; } + + /// + /// Key Type + /// + public PublicKeyType KeyType { get; private set; } + + /// + /// Name + /// + public byte[] Name { get; private set; } + + /// + /// Expiration time as UNIX Timestamp, used at least for CTCert + /// + public uint ExpirationTime { get; private set; } + + // This contains the Public Key(i.e. Modulus & Public Exponent) + #region RSA + + /// + /// Modulus + /// + public byte[] Modulus { get; private set; } + + /// + /// Public Exponent + /// + public uint PublicExponent { get; private set; } + + #endregion + + // This contains the ECC public key, and is as follows: + #region ECC + + /// + /// Public Key + /// + public byte[] PublicKey { get; private set; } + + #endregion + + /// + /// Read from a stream and get certificate, if possible + /// + /// BinaryReader representing the input stream + /// Certificate object, null on error + 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; + } + } + } +} diff --git a/NDecrypt/N3DS/Headers/ContentChunkRecord.cs b/NDecrypt/N3DS/Headers/ContentChunkRecord.cs new file mode 100644 index 0000000..f8356cb --- /dev/null +++ b/NDecrypt/N3DS/Headers/ContentChunkRecord.cs @@ -0,0 +1,59 @@ +using System.IO; + +namespace NDecrypt.N3DS.Headers +{ + internal class ContentChunkRecord + { + /// + /// Content id + /// + public uint ContentId { get; private set; } + + /// + /// Content index + /// + /// + /// This does not apply to DLC. + /// + public ContentIndex ContentIndex { get; private set; } + + /// + /// Content type + /// + public TMDContentType ContentType { get; private set; } + + /// + /// Content size + /// + public ulong ContentSize { get; private set; } + + /// + /// SHA-256 hash + /// + public byte[] SHA256Hash { get; private set; } + + /// + /// Read from a stream and get content chunk record, if possible + /// + /// BinaryReader representing the input stream + /// Content chunk record object, null on error + 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; + } + } + } +} \ No newline at end of file diff --git a/NDecrypt/N3DS/Headers/ContentInfoRecord.cs b/NDecrypt/N3DS/Headers/ContentInfoRecord.cs new file mode 100644 index 0000000..617703c --- /dev/null +++ b/NDecrypt/N3DS/Headers/ContentInfoRecord.cs @@ -0,0 +1,44 @@ +using System.IO; + +namespace NDecrypt.N3DS.Headers +{ + internal class ContentInfoRecord + { + /// + /// Content index offset + /// + public ushort ContentIndexOffset { get; private set; } + + /// + /// Content command count [k] + /// + public ushort ContentCommandCount { get; private set; } + + /// + /// SHA-256 hash of the next k content records that have not been hashed yet + /// + public byte[] UnhashedContentRecordsSHA256Hash { get; private set; } + + /// + /// Read from a stream and get content info record, if possible + /// + /// BinaryReader representing the input stream + /// Content info record object, null on error + 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; + } + } + } +} \ No newline at end of file diff --git a/NDecrypt/N3DS/Headers/Ticket.cs b/NDecrypt/N3DS/Headers/Ticket.cs new file mode 100644 index 0000000..f9d9bd1 --- /dev/null +++ b/NDecrypt/N3DS/Headers/Ticket.cs @@ -0,0 +1,229 @@ +using System.IO; + +namespace NDecrypt.N3DS.Headers +{ + // https://www.3dbrew.org/wiki/Ticket + internal class Ticket + { + /// + /// Signature Type + /// + public SignatureType SignatureType { get; private set; } + + /// + /// Signature size + /// + public ushort SignatureSize { get; private set; } + + /// + /// Padding size + /// + public byte PaddingSize { get; private set; } + + /// + /// Signature + /// + public byte[] Signature { get; private set; } + + /// + /// Issuer + /// + public byte[] Issuer { get; private set; } + + /// + /// ECC PublicKey + /// + public byte[] ECCPublicKey { get; private set; } + + /// + /// Version (For 3DS this is always 1) + /// + public byte Version { get; private set; } + + /// + /// CaCrlVersion + /// + public byte CaCrlVersion { get; private set; } + + /// + /// SignerCrlVersion + /// + public byte SignerCrlVersion { get; private set; } + + /// + /// TitleKey (normal-key encrypted using one of the common keyYs; see below) + /// + /// + /// 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. + /// + public byte[] TitleKey { get; private set; } + + /// + /// Reserved + /// + public byte Reserved1 { get; private set; } + + /// + /// TicketID + /// + public ulong TicketID { get; private set; } + + /// + /// ConsoleID + /// + public uint ConsoleID { get; private set; } + + /// + /// TitleID + /// + public ulong TitleID { get; private set; } + + /// + /// Reserved + /// + public ushort Reserved2 { get; private set; } + + /// + /// Ticket title version + /// + /// + /// 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. + /// + public ushort TicketTitleVersion { get; private set; } + + /// + /// Reserved + /// + public ulong Reserved3 { get; private set; } + + /// + /// License Type + /// + public byte LicenseType { get; private set; } + + /// + /// Index to the common keyY used for this ticket, usually 0x1 for retail system titles; + /// see below. + /// + public byte CommonKeyYIndex { get; private set; } + + /// + /// Reserved + /// + public byte[] Reserved4 { get; private set; } + + /// + /// eShop Account ID? + /// + public uint eShopAccountID { get; private set; } + + /// + /// Reserved + /// + public byte Reserved5 { get; private set; } + + /// + /// Audit + /// + public byte Audit { get; private set; } + + /// + /// Reserved + /// + public byte[] Reserved6 { get; private set; } + + /// + /// Limits + /// + /// + /// In demos, the first u32 in the "Limits" section is 0x4, then the second u32 is the max-playcount. + /// + public byte[] Limits { get; private set; } + + /// + /// Content Index + /// + /// + /// 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 byte[] ContentIndex { get; private set; } + + /// + /// Read from a stream and get ticket, if possible + /// + /// BinaryReader representing the input stream + /// Ticket size from the header + /// Ticket object, null on error + 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; + } + } + } +} diff --git a/NDecrypt/N3DS/Headers/TitleMetadata.cs b/NDecrypt/N3DS/Headers/TitleMetadata.cs new file mode 100644 index 0000000..5162b01 --- /dev/null +++ b/NDecrypt/N3DS/Headers/TitleMetadata.cs @@ -0,0 +1,214 @@ +using System.IO; + +namespace NDecrypt.N3DS.Headers +{ + // https://www.3dbrew.org/wiki/Title_metadata + internal class TitleMetadata + { + /// + /// Signature Type + /// + public SignatureType SignatureType { get; private set; } + + /// + /// Signature size + /// + public ushort SignatureSize { get; private set; } + + /// + /// Padding size + /// + public byte PaddingSize { get; private set; } + + /// + /// Signature + /// + public byte[] Signature { get; private set; } + + /// + /// Signature Issuer + /// + public byte[] SignatureIssuer { get; private set; } + + /// + /// Version + /// + public byte Version { get; private set; } + + /// + /// CaCrlVersion + /// + public byte CaCrlVersion { get; private set; } + + /// + /// SignerCrlVersion + /// + public byte SignerCrlVersion { get; private set; } + + /// + /// Reserved + /// + public byte Reserved1 { get; private set; } + + /// + /// System Version + /// + public ulong SystemVersion { get; private set; } + + /// + /// TitleID + /// + public ulong TitleID { get; private set; } + + /// + /// Title Type + /// + public uint TitleType { get; private set; } + + /// + /// Group ID + /// + public ushort GroupID { get; private set; } + + /// + /// Save Data Size in Little Endian (Bytes) (Also SRL Public Save Data Size) + /// + public uint SaveDataSize { get; private set; } + + /// + /// SRL Private Save Data Size in Little Endian (Bytes) + /// + public uint SRLPrivateSaveDataSize { get; private set; } + + /// + /// Reserved + /// + public uint Reserved2 { get; private set; } + + /// + /// SRL Flag + /// + public byte SRLFlag { get; private set; } + + /// + /// Reserved + /// + public byte[] Reserved3 { get; private set; } + + /// + /// Access Rights + /// + public uint AccessRights { get; private set; } + + /// + /// Title Version + /// + public ushort TitleVersion { get; private set; } + + /// + /// Content Count + /// + public ushort ContentCount { get; private set; } + + /// + /// Boot Content + /// + public ushort BootContent { get; private set; } + + /// + /// Padding + /// + public ushort Padding { get; private set; } + + /// + /// SHA-256 Hash of the Content Info Records + /// + public byte[] SHA256HashContentInfoRecords { get; private set; } + + /// + /// There are 64 of these records, usually only the first is used. + /// + public ContentInfoRecord[] ContentInfoRecords { get; private set; } + + /// + /// There is one of these for each content contained in this title. + /// (Determined by "Content Count" in the TMD Header). + /// + public ContentChunkRecord[] ContentChunkRecords { get; private set; } + + /// + /// Read from a stream and get ticket metadata, if possible + /// + /// BinaryReader representing the input stream + /// Metadata size from the header + /// Title metadata object, null on error + 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; + } + } + } +} \ No newline at end of file