From ead4f52f3c2dfe62d8cfb738b134dce86a28b141 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sun, 13 Oct 2024 22:49:31 -0400 Subject: [PATCH] Bring CIA up to par with 3DS --- NDecrypt.N3DS/CIATool.cs | 583 +++++++++++++++++++----------- NDecrypt.N3DS/CommonOperations.cs | 52 +++ NDecrypt.N3DS/ThreeDSTool.cs | 77 ++-- 3 files changed, 483 insertions(+), 229 deletions(-) diff --git a/NDecrypt.N3DS/CIATool.cs b/NDecrypt.N3DS/CIATool.cs index c8b97d8..de17cb1 100644 --- a/NDecrypt.N3DS/CIATool.cs +++ b/NDecrypt.N3DS/CIATool.cs @@ -161,15 +161,11 @@ namespace NDecrypt.N3DS // Get the table entry -- TODO: Fix this to get the real entry var tableEntry = new PartitionTableEntry(); - // Determine the Keys to be used - SetEncryptionKeys(ncchHeader, index, encrypt); - - // Process the extended header - ProcessExtendedHeader(ncchHeader, index, tableEntry, encrypt, input, output); - // If we're encrypting, encrypt the filesystems and update the flags if (encrypt) { + SetEncryptionKeys(ncchHeader, index); + EncryptExtendedHeader(ncchHeader, index, tableEntry, input, output); EncryptExeFS(ncchHeader, index, tableEntry, input, output); EncryptRomFS(ncchHeader, index, tableEntry, input, output); UpdateEncryptCryptoAndMasks(ncchHeader, index, tableEntry, output); @@ -178,228 +174,86 @@ namespace NDecrypt.N3DS // If we're decrypting, decrypt the filesystems and update the flags else { + SetDecryptionKeys(ncchHeader, index); + DecryptExtendedHeader(ncchHeader, index, tableEntry, input, output); DecryptExeFS(ncchHeader, index, tableEntry, input, output); DecryptRomFS(ncchHeader, index, tableEntry, input, output); UpdateDecryptCryptoAndMasks(ncchHeader, tableEntry, output); } } + #endregion + + #region Decrypt + /// - /// Determine the set of keys to be used for encryption or decryption + /// Determine the set of keys to be used for decryption /// /// NCCH header representing the partition /// Index of the partition - /// Indicates if the file should be encrypted or decrypted - private void SetEncryptionKeys(NCCHHeader ncchHeader, int index, bool encrypt) + private void SetDecryptionKeys(NCCHHeader ncchHeader, int index) { // Get partition-specific values byte[]? rsaSignature = ncchHeader.RSA2048Signature; - // TODO: Figure out what sane defaults for these values are // Set the header to use based on mode - BitMasks masks = BitMasks.NoCrypto; - CryptoMethod method = CryptoMethod.Original; - if (encrypt) - { - // TODO: Can we actually re-encrypt a CIA? - //masks = ciaHeader.BackupHeader.Flags.BitMasks; - //method = ciaHeader.BackupHeader.Flags.CryptoMethod; - } - else - { - masks = ncchHeader.Flags!.BitMasks; - method = ncchHeader.Flags.CryptoMethod; - } + BitMasks masks = ncchHeader.Flags!.BitMasks; + CryptoMethod method = ncchHeader.Flags.CryptoMethod; // Get the partition keys KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development); } /// - /// Process the extended header, if it exists + /// Decrypt the extended header, if it exists /// /// NCCH header representing the partition /// Index of the partition /// PartitionTableEntry header representing the partition - /// Indicates if the file should be encrypted or decrypted /// Stream representing the input /// Stream representing the output - private bool ProcessExtendedHeader(NCCHHeader ncchHeader, + private bool DecryptExtendedHeader(NCCHHeader ncchHeader, int index, PartitionTableEntry tableEntry, - bool encrypt, Stream input, Stream output) { - // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? - uint mediaUnitSize = 0x200; // mediaUnitSize; - - if (ncchHeader.ExtendedHeaderSizeInBytes > 0) + // Get required offsets + uint mediaUnitSize = 0x200; + uint partitionOffset = GetPartitionOffset(tableEntry, mediaUnitSize); + if (partitionOffset == 0) { - input.Seek((tableEntry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin); - output.Seek((tableEntry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin); - - Console.WriteLine($"Partition {index} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + ": ExHeader"); - - var cipher = CreateAESCipher(KeysMap[index].NormalKey2C, ncchHeader.PlainIV(), encrypt); - output.Write(cipher.ProcessBytes(input.ReadBytes(Constants.CXTExtendedDataHeaderLength))); - output.Flush(); - return true; - } - else - { - Console.WriteLine($"Partition {index} ExeFS: No Extended Header... Skipping..."); + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); return false; } - } - /// - /// Process the extended header, if it exists - /// - /// NCCH header representing the partition - /// Index of the partition - /// PartitionTableEntry header representing the partition - /// Indicates if the file should be encrypted or decrypted - /// Stream representing the input - /// Stream representing the output - private void ProcessExeFSFileEntries(NCCHHeader ncchHeader, - int index, - PartitionTableEntry tableEntry, - bool encrypt, - Stream input, - Stream output) - { - // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? - uint mediaUnitSize = 0x200; // mediaUnitSize; - - input.Seek((tableEntry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin); - var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input); - - // If the header failed to read, log and return - if (exefsHeader?.FileHeaders == null) + uint extHeaderSize = GetExtendedHeaderSize(ncchHeader); + if (extHeaderSize == 0) { - Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping..."); - return; + Console.WriteLine($"Partition {index} RomFS: No Extended Header... Skipping..."); + return false; } - foreach (var fileHeader in exefsHeader.FileHeaders) - { - // Only decrypt a file if it's a code binary - if (fileHeader == null || !fileHeader.IsCodeBinary()) - continue; + // Seek to the extended header + input.Seek(partitionOffset + 0x200, SeekOrigin.Begin); + output.Seek(partitionOffset + 0x200, SeekOrigin.Begin); - uint datalenM = ((fileHeader.FileSize) / (1024 * 1024)); - uint datalenB = ((fileHeader.FileSize) % (1024 * 1024)); - uint ctroffset = ((fileHeader.FileOffset + mediaUnitSize) / 0x10); + Console.WriteLine($"Partition {index} ExeFS: Decrypting: ExHeader"); - byte[] exefsIVWithOffsetForHeader = AddToByteArray(ncchHeader.ExeFSIV(), (int)ctroffset); + // Create the Plain AES cipher for this partition + var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, ncchHeader.PlainIV()); - var firstCipher = CreateAESCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader, encrypt); - var secondCipher = CreateAESCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader, !encrypt); + // Process the extended header + PerformAESOperation(Constants.CXTExtendedDataHeaderLength, cipher, input, output, null); - input.Seek((((tableEntry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin); - output.Seek((((tableEntry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin); - - if (datalenM > 0) - { - for (int i = 0; i < datalenM; i++) - { - output.Write(secondCipher.ProcessBytes(firstCipher.ProcessBytes(input.ReadBytes(1024 * 1024)))); - output.Flush(); - Console.Write($"\rPartition {index} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.FileName}... {i} / {datalenM + 1} mb..."); - } - } - - if (datalenB > 0) - { - output.Write(secondCipher.DoFinal(firstCipher.DoFinal(input.ReadBytes((int)datalenB)))); - output.Flush(); - } - - Console.Write($"\rPartition {index} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.FileName}... {datalenM + 1} / {datalenM + 1} mb... Done!\r\n"); - } - } - - /// - /// Process the ExeFS Filename Table - /// - /// NCCH header representing the partition - /// Index of the partition - /// PartitionTableEntry header representing the partition - /// Indicates if the file should be encrypted or decrypted - /// Stream representing the input - /// Stream representing the output - private void ProcessExeFSFilenameTable(NCCHHeader ncchHeader, - int index, - PartitionTableEntry tableEntry, - bool encrypt, - Stream input, - Stream output) - { - // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? - uint mediaUnitSize = 0x200; // mediaUnitSize; - - input.Seek((tableEntry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin); - output.Seek((tableEntry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin); - - Console.WriteLine($"Partition {index} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table"); - - var exeFSFilenameTable = CreateAESCipher(KeysMap[index].NormalKey2C, ncchHeader.ExeFSIV(), encrypt); - output.Write(exeFSFilenameTable.ProcessBytes(input.ReadBytes((int)mediaUnitSize))); +#if NET6_0_OR_GREATER + // In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer + input.Seek(0, SeekOrigin.Begin); +#endif output.Flush(); + return true; } - /// - /// Process the ExeFS, if it exists - /// - /// NCCH header representing the partition - /// Index of the partition - /// PartitionTableEntry header representing the partition - /// Indicates if the file should be encrypted or decrypted - /// Stream representing the input - /// Stream representing the output - private void ProcessExeFS(NCCHHeader ncchHeader, - int index, - PartitionTableEntry tableEntry, - bool encrypt, - Stream input, - Stream output) - { - // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? - uint mediaUnitSize = 0x200; // mediaUnitSize; - - int exefsSizeM = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * mediaUnitSize) / (1024 * 1024)); - int exefsSizeB = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * mediaUnitSize) % (1024 * 1024)); - int ctroffsetE = (int)(mediaUnitSize / 0x10); - - byte[] exefsIVWithOffset = AddToByteArray(ncchHeader.ExeFSIV(), ctroffsetE); - - var exeFS = CreateAESCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset, encrypt); - - input.Seek((tableEntry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * mediaUnitSize, SeekOrigin.Begin); - output.Seek((tableEntry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * mediaUnitSize, SeekOrigin.Begin); - if (exefsSizeM > 0) - { - for (int i = 0; i < exefsSizeM; i++) - { - output.Write(exeFS.ProcessBytes(input.ReadBytes(1024 * 1024))); - output.Flush(); - Console.Write($"\rPartition {index} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {i} / {exefsSizeM + 1} mb"); - } - } - if (exefsSizeB > 0) - { - output.Write(exeFS.DoFinal(input.ReadBytes(exefsSizeB))); - output.Flush(); - } - - Console.Write($"\rPartition {index} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {exefsSizeM + 1} / {exefsSizeM + 1} mb... Done!\r\n"); - } - - #endregion - - #region Decrypt - /// /// Decrypt the ExeFS, if it exists /// @@ -408,28 +262,156 @@ namespace NDecrypt.N3DS /// PartitionTableEntry header representing the partition /// Stream representing the input /// Stream representing the output - private void DecryptExeFS(NCCHHeader ncchHeader, + private bool DecryptExeFS(NCCHHeader ncchHeader, int index, PartitionTableEntry tableEntry, Stream input, Stream output) { - // If the ExeFS size is 0, we log and return - if (ncchHeader.ExeFSSizeInMediaUnits == 0) + // Validate the ExeFS + uint mediaUnitSize = 0x200; + uint exeFsOffset = GetExeFSOffset(ncchHeader, tableEntry, mediaUnitSize) - mediaUnitSize; + if (exeFsOffset == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return false; + } + + uint exeFsSize = GetExeFSSize(ncchHeader, mediaUnitSize); + if (exeFsSize == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return false; + } + + // Decrypt the filename table + DecryptExeFSFilenameTable(ncchHeader, index, tableEntry, input, output); + + // For all but the original crypto method, process each of the files in the table + if (ncchHeader.Flags!.CryptoMethod != CryptoMethod.Original) + DecryptExeFSFileEntries(ncchHeader, index, tableEntry, input, output); + + // Seek to the ExeFS + input.Seek(exeFsOffset, SeekOrigin.Begin); + output.Seek(exeFsOffset, SeekOrigin.Begin); + + // Create the ExeFS AES cipher for this partition + int ctroffsetE = (int)(mediaUnitSize / 0x10); + byte[] exefsIVWithOffset = AddToByteArray(ncchHeader.ExeFSIV(), ctroffsetE); + var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset); + + // Setup and perform the decryption + PerformAESOperation(exeFsSize - mediaUnitSize, + cipher, + input, + output, + (string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting: {s}")); + + return true; + } + + /// + /// Decrypt the ExeFS Filename Table + /// + /// NCCH header representing the partition + /// Index of the partition + /// PartitionTableEntry header representing the partition + /// Stream representing the input + /// Stream representing the output + private void DecryptExeFSFilenameTable(NCCHHeader ncchHeader, + int index, + PartitionTableEntry tableEntry, + Stream input, + Stream output) + { + // Get ExeFS offset + uint mediaUnitSize = 0x200; + uint exeFsOffset = GetExeFSOffset(ncchHeader, tableEntry, mediaUnitSize); + if (exeFsOffset == 0) { Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); return; } - // Decrypt the filename table - ProcessExeFSFilenameTable(ncchHeader, index, tableEntry, encrypt: false, input, output); + // Seek to the ExeFS header + input.Seek(exeFsOffset, SeekOrigin.Begin); + output.Seek(exeFsOffset, SeekOrigin.Begin); - // For all but the original crypto method, process each of the files in the table - if (ncchHeader.Flags!.CryptoMethod != CryptoMethod.Original) - ProcessExeFSFileEntries(ncchHeader, index, tableEntry, encrypt: false, input, output); + Console.WriteLine($"Partition {index} ExeFS: Decrypting: ExeFS Filename Table"); - // Decrypt the rest of the ExeFS - ProcessExeFS(ncchHeader, index, tableEntry, encrypt: false, input, output); + // Create the ExeFS AES cipher for this partition + var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, ncchHeader.ExeFSIV()); + + // Process the filename table + PerformAESOperation(mediaUnitSize, cipher, input, output, null); + +#if NET6_0_OR_GREATER + // In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer + input.Seek(0, SeekOrigin.Begin); +#endif + output.Flush(); + } + + /// + /// Decrypt the ExeFS file entries + /// + /// NCCH header representing the partition + /// Index of the partition + /// PartitionTableEntry header representing the partition + /// Stream representing the input + /// Stream representing the output + private void DecryptExeFSFileEntries(NCCHHeader ncchHeader, + int index, + PartitionTableEntry tableEntry, + Stream input, + Stream output) + { + // Get ExeFS offset + uint mediaUnitSize = 0x200; + uint exeFsHeaderOffset = GetExeFSOffset(ncchHeader, tableEntry, mediaUnitSize); + if (exeFsHeaderOffset == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return; + } + + // Get to the start of the files + uint exeFsFilesOffset = exeFsHeaderOffset + mediaUnitSize; + input.Seek(exeFsHeaderOffset, SeekOrigin.Begin); + var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input); + + // If the header failed to read, log and return + if (exefsHeader == null) + { + Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping..."); + return; + } + + foreach (var fileHeader in exefsHeader.FileHeaders!) + { + // Only decrypt a file if it's a code binary + if (fileHeader == null || !fileHeader.IsCodeBinary()) + continue; + + // Create the ExeFS AES ciphers for this partition + uint ctroffset = (fileHeader.FileOffset + mediaUnitSize) / 0x10; + byte[] exefsIVWithOffsetForHeader = AddToByteArray(ncchHeader.ExeFSIV(), (int)ctroffset); + var firstCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader); + var secondCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader); + + // Seek to the file entry + input.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin); + output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin); + + // Setup and perform the encryption + uint exeFsSize = GetExeFSSize(ncchHeader, mediaUnitSize); + PerformAESOperation(exeFsSize, + firstCipher, + secondCipher, + input, + output, + (string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting: {fileHeader.FileName}...{s}")); + } } /// @@ -440,7 +422,6 @@ namespace NDecrypt.N3DS /// PartitionTableEntry header representing the partition /// Stream representing the input /// Stream representing the output - /// TODO: See how much can be extracted into a common method with Encrypt private void DecryptRomFS(NCCHHeader ncchHeader, int index, PartitionTableEntry tableEntry, @@ -513,6 +494,77 @@ namespace NDecrypt.N3DS #region Encrypt + /// + /// Determine the set of keys to be used for encryption + /// + /// NCCH header representing the partition + /// Index of the partition + private void SetEncryptionKeys(NCCHHeader ncchHeader, int index) + { + // Get partition-specific values + byte[]? rsaSignature = ncchHeader.RSA2048Signature; + + // TODO: Figure out what sane defaults for these values are + // TODO: Can we actually re-encrypt a CIA? + + // Set the header to use based on mode + BitMasks masks = BitMasks.NoCrypto; // ciaHeader.BackupHeader.Flags.BitMasks; + CryptoMethod method = CryptoMethod.Original; // ciaHeader.BackupHeader.Flags.CryptoMethod; + + // Get the partition keys + KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development); + } + + /// + /// Encrypt the extended header, if it exists + /// + /// NCCH header representing the partition + /// Index of the partition + /// PartitionTableEntry header representing the partition + /// Stream representing the input + /// Stream representing the output + private bool EncryptExtendedHeader(NCCHHeader ncchHeader, + int index, + PartitionTableEntry tableEntry, + Stream input, + Stream output) + { + // Get required offsets + uint mediaUnitSize = 0x200; + uint partitionOffset = GetPartitionOffset(tableEntry, mediaUnitSize); + if (partitionOffset == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return false; + } + + uint extHeaderSize = GetExtendedHeaderSize(ncchHeader); + if (extHeaderSize == 0) + { + Console.WriteLine($"Partition {index} RomFS: No Extended Header... Skipping..."); + return false; + } + + // Seek to the extended header + input.Seek(partitionOffset + 0x200, SeekOrigin.Begin); + output.Seek(partitionOffset + 0x200, SeekOrigin.Begin); + + Console.WriteLine($"Partition {index} ExeFS: Encrypting: ExHeader"); + + // Create the Plain AES cipher for this partition + var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, ncchHeader.PlainIV()); + + // Process the extended header + PerformAESOperation(Constants.CXTExtendedDataHeaderLength, cipher, input, output, null); + +#if NET6_0_OR_GREATER + // In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer + input.Seek(0, SeekOrigin.Begin); +#endif + output.Flush(); + return true; + } + /// /// Encrypt the ExeFS, if it exists /// @@ -521,29 +573,157 @@ namespace NDecrypt.N3DS /// PartitionTableEntry header representing the partition /// Stream representing the input /// Stream representing the output - private void EncryptExeFS(NCCHHeader ncchHeader, + private bool EncryptExeFS(NCCHHeader ncchHeader, int index, PartitionTableEntry tableEntry, Stream input, Stream output) { - // If the ExeFS size is 0, we log and return - if (ncchHeader.ExeFSSizeInMediaUnits == 0) + // Validate the ExeFS + uint mediaUnitSize = 0x200; + uint exeFsOffset = GetExeFSOffset(ncchHeader, tableEntry, mediaUnitSize) - mediaUnitSize; + if (exeFsOffset == 0) { Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); - return; + return false; + } + + uint exeFsSize = GetExeFSSize(ncchHeader, mediaUnitSize); + if (exeFsSize == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return false; } // TODO: Determine how to figure out the original crypto method, if possible // For all but the original crypto method, process each of the files in the table //if (ciaHeader.BackupHeader.Flags.CryptoMethod != CryptoMethod.Original) - // ProcessExeFSFileEntries(ncchHeader, reader, writer); + // EncryptExeFSFileEntries(ncchHeader, index, tableEntry, reader, writer); // Encrypt the filename table - ProcessExeFSFilenameTable(ncchHeader, index, tableEntry, encrypt: true, input, output); + EncryptExeFSFilenameTable(ncchHeader, index, tableEntry, input, output); - // Encrypt the rest of the ExeFS - ProcessExeFS(ncchHeader, index, tableEntry, encrypt: true, input, output); + // Seek to the ExeFS + input.Seek(exeFsOffset, SeekOrigin.Begin); + output.Seek(exeFsOffset, SeekOrigin.Begin); + + // Create the ExeFS AES cipher for this partition + int ctroffsetE = (int)(mediaUnitSize / 0x10); + byte[] exefsIVWithOffset = AddToByteArray(ncchHeader.ExeFSIV(), ctroffsetE); + var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset); + + // Setup and perform the decryption + PerformAESOperation(exeFsSize - mediaUnitSize, + cipher, + input, + output, + (string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting: {s}")); + + return true; + } + + /// + /// Encrypt the ExeFS Filename Table + /// + /// NCCH header representing the partition + /// Index of the partition + /// PartitionTableEntry header representing the partition + /// Stream representing the input + /// Stream representing the output + private void EncryptExeFSFilenameTable(NCCHHeader ncchHeader, + int index, + PartitionTableEntry tableEntry, + Stream input, + Stream output) + { + // Get ExeFS offset + uint mediaUnitSize = 0x200; + uint exeFsOffset = GetExeFSOffset(ncchHeader, tableEntry, mediaUnitSize); + if (exeFsOffset == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return; + } + + // Seek to the ExeFS header + input.Seek(exeFsOffset, SeekOrigin.Begin); + output.Seek(exeFsOffset, SeekOrigin.Begin); + + Console.WriteLine($"Partition {index} ExeFS: Encrypting: ExeFS Filename Table"); + + // Create the ExeFS AES cipher for this partition + var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, ncchHeader.ExeFSIV()); + + // Process the filename table + PerformAESOperation(mediaUnitSize, cipher, input, output, null); + +#if NET6_0_OR_GREATER + // In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer + input.Seek(0, SeekOrigin.Begin); +#endif + output.Flush(); + } + + /// + /// Encrypt the ExeFS file entries + /// + /// NCCH header representing the partition + /// Index of the partition + /// PartitionTableEntry header representing the partition + /// Stream representing the input + /// Stream representing the output + private void EncryptExeFSFileEntries(NCCHHeader ncchHeader, + int index, + PartitionTableEntry tableEntry, + Stream input, + Stream output) + { + // Get ExeFS offset + uint mediaUnitSize = 0x200; + uint exeFsHeaderOffset = GetExeFSOffset(ncchHeader, tableEntry, mediaUnitSize); + if (exeFsHeaderOffset == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return; + } + + // Get to the start of the files + uint exeFsFilesOffset = exeFsHeaderOffset + mediaUnitSize; + input.Seek(exeFsHeaderOffset, SeekOrigin.Begin); + var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input); + + // If the header failed to read, log and return + if (exefsHeader == null) + { + Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping..."); + return; + } + + foreach (var fileHeader in exefsHeader.FileHeaders!) + { + // Only decrypt a file if it's a code binary + if (fileHeader == null || !fileHeader.IsCodeBinary()) + continue; + + // Create the ExeFS AES ciphers for this partition + uint ctroffset = (fileHeader.FileOffset + mediaUnitSize) / 0x10; + byte[] exefsIVWithOffsetForHeader = AddToByteArray(ncchHeader.ExeFSIV(), (int)ctroffset); + var firstCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader); + var secondCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader); + + // Seek to the file entry + input.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin); + output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin); + + // Setup and perform the encryption + uint exeFsSize = GetExeFSSize(ncchHeader, mediaUnitSize); + PerformAESOperation(exeFsSize, + firstCipher, + secondCipher, + input, + output, + (string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting: {fileHeader.FileName}...{s}")); + } } /// @@ -554,7 +734,6 @@ namespace NDecrypt.N3DS /// PartitionTableEntry header representing the partition /// Stream representing the input /// Stream representing the output - /// TODO: See how much can be extracted into a common method with Decrypt private void EncryptRomFS(NCCHHeader ncchHeader, int index, PartitionTableEntry tableEntry, diff --git a/NDecrypt.N3DS/CommonOperations.cs b/NDecrypt.N3DS/CommonOperations.cs index 7923d40..7b7e29c 100644 --- a/NDecrypt.N3DS/CommonOperations.cs +++ b/NDecrypt.N3DS/CommonOperations.cs @@ -237,6 +237,24 @@ namespace NDecrypt.N3DS return (partitionOffsetMU + exeFsOffsetMU) * cart.MediaUnitSize(); } + /// + /// Get the offset of a partition ExeFS + /// + /// Offset to the ExeFS of the partition, 0 on error + public static uint GetExeFSOffset(NCCHHeader header, + PartitionTableEntry entry, + uint mediaUnitSize) + { + // If the offset is 0, return 0 + uint exeFsOffsetMU = header.ExeFSOffsetInMediaUnits; + if (exeFsOffsetMU == 0) + return 0; + + // Return the adjusted offset + uint partitionOffsetMU = entry.Offset; + return (partitionOffsetMU + exeFsOffsetMU) * mediaUnitSize; + } + /// /// Get the offset of a partition /// @@ -258,6 +276,22 @@ namespace NDecrypt.N3DS return partitionOffsetMU * cart.MediaUnitSize(); } + /// + /// Get the offset of a partition + /// + /// Offset to the partition, 0 on error + public static uint GetPartitionOffset(PartitionTableEntry entry, + uint mediaUnitSize) + { + // Invalid partition table entry means no size is available + if (entry.Offset == 0) + return 0; + + // Return the adjusted offset + uint partitionOffsetMU = entry.Offset; + return partitionOffsetMU * mediaUnitSize; + } + /// /// Get the offset of a partition RomFS /// @@ -294,6 +328,24 @@ namespace NDecrypt.N3DS return (partitionOffsetMU + romFsOffsetMU) * cart.MediaUnitSize(); } + /// + /// Get the offset of a partition RomFS + /// + /// Offset to the RomFS of the partition, 0 on error + public static uint GetRomFSOffset(NCCHHeader header, + PartitionTableEntry entry, + uint mediaUnitSize) + { + // If the offset is 0, return 0 + uint romFsOffsetMU = header.RomFSOffsetInMediaUnits; + if (romFsOffsetMU == 0) + return 0; + + // Return the adjusted offset + uint partitionOffsetMU = entry.Offset; + return (partitionOffsetMU + romFsOffsetMU - 1) * mediaUnitSize; + } + #endregion #region Sizes diff --git a/NDecrypt.N3DS/ThreeDSTool.cs b/NDecrypt.N3DS/ThreeDSTool.cs index 5d3713c..ccdb3b1 100644 --- a/NDecrypt.N3DS/ThreeDSTool.cs +++ b/NDecrypt.N3DS/ThreeDSTool.cs @@ -147,12 +147,10 @@ namespace NDecrypt.N3DS /// Stream representing the output private void ProcessPartition(Cart cart, int index, bool encrypt, Stream input, Stream output) { - // Determine the Keys to be used - SetEncryptionKeys(cart, index, encrypt); - // If we're encrypting, encrypt the filesystems and update the flags if (encrypt) { + SetEncryptionKeys(cart, index); EncryptExtendedHeader(cart, index, input, output); EncryptExeFS(cart, index, input, output); EncryptRomFS(cart, index, input, output); @@ -162,6 +160,7 @@ namespace NDecrypt.N3DS // If we're decrypting, decrypt the filesystems and update the flags else { + SetDecryptionKeys(cart, index); DecryptExtendedHeader(cart, index, input, output); DecryptExeFS(cart, index, input, output); DecryptRomFS(cart, index, input, output); @@ -169,13 +168,16 @@ namespace NDecrypt.N3DS } } + #endregion + + #region Decrypt + /// - /// Determine the set of keys to be used for encryption or decryption + /// Determine the set of keys to be used for decryption /// /// Cart representing the 3DS file /// Index of the partition - /// Indicates if the file should be encrypted or decrypted - private void SetEncryptionKeys(Cart cart, int index, bool encrypt) + private void SetDecryptionKeys(Cart cart, int index) { // Get the partition var partition = cart.Partitions?[index]; @@ -186,28 +188,13 @@ namespace NDecrypt.N3DS byte[]? rsaSignature = partition.RSA2048Signature; // Set the header to use based on mode - BitMasks masks; - CryptoMethod method; - if (encrypt) - { - var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader; - masks = backupHeader!.Flags!.BitMasks; - method = backupHeader.Flags.CryptoMethod; - } - else - { - masks = partition.Flags!.BitMasks; - method = partition.Flags!.CryptoMethod; - } + BitMasks masks = partition.Flags!.BitMasks; + CryptoMethod method = partition.Flags!.CryptoMethod; // Get the partition keys KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development); } - #endregion - - #region Decrypt - /// /// Decrypt the extended header, if it exists /// @@ -261,7 +248,7 @@ namespace NDecrypt.N3DS /// Stream representing the output private bool DecryptExeFS(Cart cart, int index, Stream input, Stream output) { - // Get ExeFS offset + // Validate the ExeFS uint exeFsOffset = GetExeFSOffset(cart, index); if (exeFsOffset == 0) { @@ -269,6 +256,13 @@ namespace NDecrypt.N3DS return false; } + uint exeFsSize = GetExeFSSize(cart, index); + if (exeFsSize == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return false; + } + // Decrypt the filename table DecryptExeFSFilenameTable(cart, index, input, output); @@ -286,7 +280,6 @@ namespace NDecrypt.N3DS var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset); // Setup and perform the decryption - uint exeFsSize = GetExeFSSize(cart, index); PerformAESOperation(exeFsSize, cipher, input, @@ -462,6 +455,30 @@ namespace NDecrypt.N3DS #region Encrypt + /// + /// Determine the set of keys to be used for encryption + /// + /// Cart representing the 3DS file + /// Index of the partition + private void SetEncryptionKeys(Cart cart, int index) + { + // Get the partition + var partition = cart.Partitions?[index]; + if (partition == null) + return; + + // Get partition-specific values + byte[]? rsaSignature = partition.RSA2048Signature; + + // Set the header to use based on mode + var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader; + BitMasks masks = backupHeader!.Flags!.BitMasks; + CryptoMethod method = backupHeader.Flags.CryptoMethod; + + // Get the partition keys + KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development); + } + /// /// Encrypt the extended header, if it exists /// @@ -515,7 +532,7 @@ namespace NDecrypt.N3DS /// Stream representing the output private bool EncryptExeFS(Cart cart, int index, Stream input, Stream output) { - // Get ExeFS offset + // Validate the ExeFS uint exeFsOffset = GetExeFSOffset(cart, index); if (exeFsOffset == 0) { @@ -523,6 +540,13 @@ namespace NDecrypt.N3DS return false; } + uint exeFsSize = GetExeFSSize(cart, index); + if (exeFsSize == 0) + { + Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); + return false; + } + // For all but the original crypto method, process each of the files in the table var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader; if (backupHeader!.Flags!.CryptoMethod != CryptoMethod.Original) @@ -541,7 +565,6 @@ namespace NDecrypt.N3DS var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset); // Setup and perform the encryption - uint exeFsSize = GetExeFSSize(cart, index); PerformAESOperation(exeFsSize, cipher, input,