diff --git a/SabreTools.Wrappers/N3DS.Encryption.cs b/SabreTools.Wrappers/N3DS.Encryption.cs
index d232466c..89461883 100644
--- a/SabreTools.Wrappers/N3DS.Encryption.cs
+++ b/SabreTools.Wrappers/N3DS.Encryption.cs
@@ -1,5 +1,7 @@
using System;
+using System.IO;
using SabreTools.Data.Models.N3DS;
+using SabreTools.IO.Encryption;
using SabreTools.IO.Extensions;
using static SabreTools.Data.Models.N3DS.Constants;
@@ -64,8 +66,1251 @@ namespace SabreTools.Wrappers
return [.. partitionIdBytes, .. RomfsCounter];
}
+ #region Common
+
+ ///
+ /// Get KeyX value for a crypto method and development status combination
+ ///
+ private static byte[] GetKeyXForCryptoMethod(EncryptionSettings settings, CryptoMethod method)
+ {
+ switch (method)
+ {
+ case CryptoMethod.Original:
+ Console.WriteLine("Encryption Method: Key 0x2C");
+ return settings.Development ? settings.DevKeyX0x2C : settings.KeyX0x2C;
+
+ case CryptoMethod.Seven:
+ Console.WriteLine("Encryption Method: Key 0x25");
+ return settings.Development ? settings.DevKeyX0x25 : settings.KeyX0x25;
+
+ case CryptoMethod.NineThree:
+ Console.WriteLine("Encryption Method: Key 0x18");
+ return settings.Development ? settings.DevKeyX0x18 : settings.KeyX0x18;
+
+ case CryptoMethod.NineSix:
+ Console.WriteLine("Encryption Method: Key 0x1B");
+ return settings.Development ? settings.DevKeyX0x1B : settings.KeyX0x1B;
+
+ // This should never happen
+ default:
+ Console.WriteLine("Encryption Method: UNSUPPORTED");
+ return [];
+ }
+ }
+
+ #endregion
+
+ #region Decrypt
+
+ ///
+ /// Decrypt all partitions in the partition table of an NCSD header
+ ///
+ /// Indicates if the operation should be forced
+ /// Stream representing the input
+ /// Stream representing the output
+ /// Indicates if development images are expected
+ /// AES Hardware Constant
+ /// KeyX 0x18 (New 3DS 9.3)
+ /// Dev KeyX 0x18 (New 3DS 9.3)
+ /// KeyX 0x1B (New 3DS 9.6)
+ /// Dev KeyX 0x1B New 3DS 9.6)
+ /// KeyX 0x25 (> 7.x)
+ /// Dev KeyX 0x25 (> 7.x)
+ /// KeyX 0x2C (< 6.x)
+ /// Dev KeyX 0x2C (< 6.x)
+ public void DecryptAllPartitions(bool force,
+ Stream reader,
+ Stream writer,
+ bool development = false,
+ byte[]? aesHardwareConstant = null,
+ byte[]? keyX0x18 = null,
+ byte[]? devKeyX0x18 = null,
+ byte[]? keyX0x1B = null,
+ byte[]? devKeyX0x1B = null,
+ byte[]? keyX0x25 = null,
+ byte[]? devKeyX0x25 = null,
+ byte[]? keyX0x2C = null,
+ byte[]? devKeyX0x2C = null)
+ {
+ // Check the partitions table
+ if (PartitionsTable is null || Partitions is null)
+ {
+ Console.WriteLine("Invalid partitions table!");
+ return;
+ }
+
+ // Create a new set of encryption settings
+ var settings = new EncryptionSettings
+ {
+ Development = development,
+ AESHardwareConstant = aesHardwareConstant ?? [],
+ KeyX0x18 = keyX0x18 ?? [],
+ DevKeyX0x18 = devKeyX0x18 ?? [],
+ KeyX0x1B = keyX0x1B ?? [],
+ DevKeyX0x1B = devKeyX0x1B ?? [],
+ KeyX0x25 = keyX0x25 ?? [],
+ DevKeyX0x25 = devKeyX0x25 ?? [],
+ KeyX0x2C = keyX0x2C ?? [],
+ DevKeyX0x2C = devKeyX0x2C ?? [],
+ };
+
+ // Iterate over all 8 NCCH partitions
+ for (int p = 0; p < 8; p++)
+ {
+ var partition = Partitions[p];
+ if (partition is null || partition.MagicID != NCCHMagicNumber)
+ {
+ Console.WriteLine($"Partition {p} Not found... Skipping...");
+ continue;
+ }
+
+ // Check the partition has data
+ var partitionEntry = PartitionsTable[p];
+ if (partitionEntry is null || partitionEntry.Length == 0)
+ {
+ Console.WriteLine($"Partition {p} No data... Skipping...");
+ continue;
+ }
+
+ // Decrypt the partition, if possible
+ if (ShouldDecryptPartition(p, force))
+ DecryptPartition(p, settings, reader, writer);
+ }
+ }
+
+ ///
+ /// Determine if the current partition should be decrypted
+ /// s
+ private bool ShouldDecryptPartition(int index, bool force)
+ {
+ // If we're forcing the operation, tell the user
+ if (force)
+ {
+ Console.WriteLine($"Partition {index} is not verified due to force flag being set.");
+ return true;
+ }
+ // If we're not forcing the operation, check if the 'NoCrypto' bit is set
+ else if (PossiblyDecrypted(index))
+ {
+ Console.WriteLine($"Partition {index}: Already Decrypted?...");
+ return false;
+ }
+
+ // By default, it passes
+ return true;
+ }
+
+ ///
+ /// Decrypt a single partition
+ ///
+ /// Index of the partition
+ /// Encryption settings
+ /// Stream representing the input
+ /// Stream representing the output
+ private void DecryptPartition(int index, EncryptionSettings settings, Stream reader, Stream writer)
+ {
+ // Determine the keys needed for this partition
+ PartitionKeys? keys = GetDecryptionKeys(index, settings);
+ if (keys == null)
+ {
+ Console.WriteLine($"Partition {index} could not generate keys. Skipping...");
+ return;
+ }
+
+ // Decrypt the parts of the partition
+ DecryptExtendedHeader(index, keys, reader, writer);
+ DecryptExeFS(index, keys, reader, writer);
+ DecryptRomFS(index, keys, reader, writer);
+
+ // Update the flags
+ UpdateDecryptCryptoAndMasks(index, writer);
+ }
+
+ ///
+ /// Determine the set of keys to be used for decryption
+ ///
+ /// Index of the partition
+ /// Encryption settings
+ private PartitionKeys? GetDecryptionKeys(int index, EncryptionSettings settings)
+ {
+ // Get the partition
+ var partition = Partitions?[index];
+ if (partition?.Flags is null)
+ return null;
+
+ // Get partition-specific values
+ byte[]? signature = partition.RSA2048Signature;
+ BitMasks masks = GetBitMasks(index);
+ CryptoMethod method = GetCryptoMethod(index);
+
+ // Get the partition keys
+ byte[] keyX = GetKeyXForCryptoMethod(settings, method);
+ byte[] keyX0x2C = settings.Development ? settings.DevKeyX0x2C : settings.KeyX0x2C;
+ return new PartitionKeys(signature, masks, settings.AESHardwareConstant, keyX, keyX0x2C);
+ }
+
+ ///
+ /// Decrypt the extended header, if it exists
+ ///
+ /// Index of the partition
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private bool DecryptExtendedHeader(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ // Get required offsets
+ uint partitionOffset = GetPartitionOffset(index);
+ if (partitionOffset == 0 || partitionOffset > reader.Length)
+ {
+ Console.WriteLine($"Partition {index} No Data... Skipping...");
+ return false;
+ }
+
+ uint extHeaderSize = GetExtendedHeaderSize(index);
+ if (extHeaderSize == 0)
+ {
+ Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
+ return false;
+ }
+
+ // Seek to the extended header
+ reader.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
+ writer.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
+
+ Console.WriteLine($"Partition {index}: Decrypting - ExHeader");
+
+ // Create the Plain AES cipher for this partition
+ var cipher = AESCTR.CreateDecryptionCipher(keys.NormalKey2C, PlainIV(index));
+
+ // Process the extended header
+ AESCTR.PerformOperation(CXTExtendedDataHeaderLength, cipher, reader, writer, 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
+ reader.Seek(0, SeekOrigin.Begin);
+#endif
+ writer.Flush();
+ return true;
+ }
+
+ ///
+ /// Decrypt the ExeFS, if it exists
+ ///
+ /// Index of the partition
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private bool DecryptExeFS(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ // Validate the ExeFS
+ uint exeFsHeaderOffset = GetExeFSOffset(index);
+ if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > reader.Length)
+ {
+ Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
+ return false;
+ }
+
+ uint exeFsSize = GetExeFSSize(index);
+ if (exeFsSize == 0)
+ {
+ Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
+ return false;
+ }
+
+ // Decrypt the filename table
+ DecryptExeFSFilenameTable(index, keys, reader, writer);
+
+ // For all but the original crypto method, process each of the files in the table
+ if (GetCryptoMethod(index) != CryptoMethod.Original)
+ DecryptExeFSFileEntries(index, keys, reader, writer);
+
+ // Get the ExeFS files offset
+ uint exeFsFilesOffset = exeFsHeaderOffset + MediaUnitSize;
+
+ // Seek to the ExeFS
+ reader.Seek(exeFsFilesOffset, SeekOrigin.Begin);
+ writer.Seek(exeFsFilesOffset, SeekOrigin.Begin);
+
+ // Create the ExeFS AES cipher for this partition
+ uint ctroffsetE = MediaUnitSize / 0x10;
+ byte[] exefsIVWithOffset = ExeFSIV(index).Add(ctroffsetE);
+ var cipher = AESCTR.CreateDecryptionCipher(keys.NormalKey2C, exefsIVWithOffset);
+
+ // Setup and perform the decryption
+ exeFsSize -= MediaUnitSize;
+ AESCTR.PerformOperation(exeFsSize,
+ cipher,
+ reader,
+ writer,
+ s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
+
+ return true;
+ }
+
+ ///
+ /// Decrypt the ExeFS Filename Table
+ ///
+ /// Index of the partition
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private void DecryptExeFSFilenameTable(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ // Get ExeFS offset
+ uint exeFsOffset = GetExeFSOffset(index);
+ if (exeFsOffset == 0 || exeFsOffset > reader.Length)
+ {
+ Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
+ return;
+ }
+
+ // Seek to the ExeFS header
+ reader.Seek(exeFsOffset, SeekOrigin.Begin);
+ writer.Seek(exeFsOffset, SeekOrigin.Begin);
+
+ Console.WriteLine($"Partition {index} ExeFS: Decrypting - ExeFS Filename Table");
+
+ // Create the ExeFS AES cipher for this partition
+ var cipher = AESCTR.CreateDecryptionCipher(keys.NormalKey2C, ExeFSIV(index));
+
+ // Process the filename table
+ byte[] readBytes = reader.ReadBytes((int)MediaUnitSize);
+ byte[] processedBytes = cipher.ProcessBytes(readBytes);
+ writer.Write(processedBytes);
+
+#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
+ reader.Seek(0, SeekOrigin.Begin);
+#endif
+ writer.Flush();
+ }
+
+ ///
+ /// Decrypt the ExeFS file entries
+ ///
+ /// Index of the partition
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private void DecryptExeFSFileEntries(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ if (ExeFSHeaders is null || index < 0 || index > ExeFSHeaders.Length)
+ {
+ Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
+ return;
+ }
+
+ // Reread the decrypted ExeFS header
+ uint exeFsHeaderOffset = GetExeFSOffset(index);
+ reader.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
+ ExeFSHeaders[index] = Serialization.Readers.N3DS.ParseExeFSHeader(reader);
+
+ // Get the ExeFS header
+ var exeFsHeader = ExeFSHeaders[index];
+ if (exeFsHeader?.FileHeaders is null)
+ {
+ Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
+ return;
+ }
+
+ // Get the ExeFS files offset
+ uint exeFsFilesOffset = exeFsHeaderOffset + MediaUnitSize;
+
+ // Loop through and process all headers
+ for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
+ {
+ // Only attempt to process code binary files
+ if (!IsCodeBinary(index, i))
+ continue;
+
+ // Get the file header
+ var fileHeader = exeFsHeader.FileHeaders[i];
+ if (fileHeader is null)
+ continue;
+
+ // Create the ExeFS AES ciphers for this partition
+ uint ctroffset = (fileHeader.FileOffset + MediaUnitSize) / 0x10;
+ byte[] exefsIVWithOffsetForHeader = ExeFSIV(index).Add(ctroffset);
+ var firstCipher = AESCTR.CreateDecryptionCipher(keys.NormalKey, exefsIVWithOffsetForHeader);
+ var secondCipher = AESCTR.CreateEncryptionCipher(keys.NormalKey2C, exefsIVWithOffsetForHeader);
+
+ // Seek to the file entry
+ reader.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
+ writer.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
+
+ // Setup and perform the encryption
+ AESCTR.PerformOperation(fileHeader.FileSize,
+ firstCipher,
+ secondCipher,
+ reader,
+ writer,
+ s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
+ }
+ }
+
+ ///
+ /// Decrypt the RomFS, if it exists
+ ///
+ /// Keys for the partition
+ /// Index of the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private bool DecryptRomFS(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ // Validate the RomFS
+ uint romFsOffset = GetRomFSOffset(index);
+ if (romFsOffset == 0 || romFsOffset > reader.Length)
+ {
+ Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
+ return false;
+ }
+
+ uint romFsSize = GetRomFSSize(index);
+ if (romFsSize == 0)
+ {
+ Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
+ return false;
+ }
+
+ // Seek to the RomFS
+ reader.Seek(romFsOffset, SeekOrigin.Begin);
+ writer.Seek(romFsOffset, SeekOrigin.Begin);
+
+ // Create the RomFS AES cipher for this partition
+ var cipher = AESCTR.CreateDecryptionCipher(keys.NormalKey, RomFSIV(index));
+
+ // Setup and perform the decryption
+ AESCTR.PerformOperation(romFsSize,
+ cipher,
+ reader,
+ writer,
+ s => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
+
+ return true;
+ }
+
+ ///
+ /// Update the CryptoMethod and BitMasks for the decrypted partition
+ ///
+ /// Index of the partition
+ /// Stream representing the output
+ private void UpdateDecryptCryptoAndMasks(int index, Stream writer)
+ {
+ // Get required offsets
+ uint partitionOffset = GetPartitionOffset(index);
+
+ // Seek to the CryptoMethod location
+ writer.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
+
+ // Write the new CryptoMethod
+ writer.Write((byte)CryptoMethod.Original);
+ writer.Flush();
+
+ // Seek to the BitMasks location
+ writer.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
+
+ // Write the new BitMasks flag
+ BitMasks flag = GetBitMasks(index);
+ flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
+ flag |= BitMasks.NoCrypto;
+ writer.Write((byte)flag);
+ writer.Flush();
+ }
+
+ #endregion
+
+ #region Encrypt
+
+ ///
+ /// Encrypt all partitions in the partition table of an NCSD header
+ ///
+ /// Indicates if the operation should be forced
+ /// Stream representing the input
+ /// Stream representing the output
+ /// Indicates if development images are expected
+ /// AES Hardware Constant
+ /// KeyX 0x18 (New 3DS 9.3)
+ /// Dev KeyX 0x18 (New 3DS 9.3)
+ /// KeyX 0x1B (New 3DS 9.6)
+ /// Dev KeyX 0x1B New 3DS 9.6)
+ /// KeyX 0x25 (> 7.x)
+ /// Dev KeyX 0x25 (> 7.x)
+ /// KeyX 0x2C (< 6.x)
+ /// Dev KeyX 0x2C (< 6.x)
+ public void EncryptAllPartitions(bool force,
+ Stream reader,
+ Stream writer,
+ bool development = false,
+ byte[]? aesHardwareConstant = null,
+ byte[]? keyX0x18 = null,
+ byte[]? devKeyX0x18 = null,
+ byte[]? keyX0x1B = null,
+ byte[]? devKeyX0x1B = null,
+ byte[]? keyX0x25 = null,
+ byte[]? devKeyX0x25 = null,
+ byte[]? keyX0x2C = null,
+ byte[]? devKeyX0x2C = null)
+ {
+ // Check the partitions table
+ if (PartitionsTable is null || Partitions is null)
+ {
+ Console.WriteLine("Invalid partitions table!");
+ return;
+ }
+
+ // Create a new set of encryption settings
+ var settings = new EncryptionSettings
+ {
+ Development = development,
+ AESHardwareConstant = aesHardwareConstant ?? [],
+ KeyX0x18 = keyX0x18 ?? [],
+ DevKeyX0x18 = devKeyX0x18 ?? [],
+ KeyX0x1B = keyX0x1B ?? [],
+ DevKeyX0x1B = devKeyX0x1B ?? [],
+ KeyX0x25 = keyX0x25 ?? [],
+ DevKeyX0x25 = devKeyX0x25 ?? [],
+ KeyX0x2C = keyX0x2C ?? [],
+ DevKeyX0x2C = devKeyX0x2C ?? [],
+ };
+
+ // Iterate over all 8 NCCH partitions
+ for (int p = 0; p < 8; p++)
+ {
+ // Check the partition exists
+ var partition = Partitions[p];
+ if (partition is null || partition.MagicID != NCCHMagicNumber)
+ {
+ Console.WriteLine($"Partition {p} Not found... Skipping...");
+ continue;
+ }
+
+ // Check the partition has data
+ var partitionEntry = PartitionsTable[p];
+ if (partitionEntry is null || partitionEntry.Length == 0)
+ {
+ Console.WriteLine($"Partition {p} No data... Skipping...");
+ continue;
+ }
+
+ // Encrypt the partition, if possible
+ if (ShouldEncryptPartition(p, force))
+ EncryptPartition(p, settings, reader, writer);
+ }
+ }
+
+ ///
+ /// Determine if the current partition should be encrypted
+ ///
+ private bool ShouldEncryptPartition(int index, bool force)
+ {
+ // If we're forcing the operation, tell the user
+ if (force)
+ {
+ Console.WriteLine($"Partition {index} is not verified due to force flag being set.");
+ return true;
+ }
+ // If we're not forcing the operation, check if the 'NoCrypto' bit is set
+ else if (!PossiblyDecrypted(index))
+ {
+ Console.WriteLine($"Partition {index}: Already Encrypted?...");
+ return false;
+ }
+
+ // By default, it passes
+ return true;
+ }
+
+ ///
+ /// Encrypt a single partition
+ ///
+ /// Index of the partition
+ /// Encryption settings
+ /// Stream representing the input
+ /// Stream representing the output
+ private void EncryptPartition(int index, EncryptionSettings settings, Stream reader, Stream writer)
+ {
+ // Determine the keys needed for this partition
+ PartitionKeys? keys = GetEncryptionKeys(index, settings);
+ if (keys == null)
+ {
+ Console.WriteLine($"Partition {index} could not generate keys. Skipping...");
+ return;
+ }
+
+ // Encrypt the parts of the partition
+ EncryptExtendedHeader(index, keys, reader, writer);
+ EncryptExeFS(index, keys, reader, writer);
+ EncryptRomFS(index, settings, keys, reader, writer);
+
+ // Update the flags
+ UpdateEncryptCryptoAndMasks(index, writer);
+ }
+
+ ///
+ /// Determine the set of keys to be used for encryption
+ ///
+ /// Index of the partition
+ /// Encryption settings
+ private PartitionKeys? GetEncryptionKeys(int index, EncryptionSettings settings)
+ {
+ // Get the partition
+ var partition = Partitions?[index];
+ if (partition is null)
+ return null;
+
+ // Get the backup header
+ var backupHeader = BackupHeader;
+ if (backupHeader?.Flags is null)
+ return null;
+
+ // Get partition-specific values
+ byte[]? signature = partition.RSA2048Signature;
+ BitMasks masks = backupHeader.Flags.BitMasks;
+ CryptoMethod method = backupHeader.Flags.CryptoMethod;
+
+ // Get the partition keys
+ byte[] keyX = GetKeyXForCryptoMethod(settings, method);
+ byte[] keyX0x2C = settings.Development ? settings.DevKeyX0x2C : settings.KeyX0x2C;
+ return new PartitionKeys(signature, masks, settings.AESHardwareConstant, keyX, keyX0x2C);
+ }
+
+ ///
+ /// Encrypt the extended header, if it exists
+ ///
+ /// Index of the partition
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private bool EncryptExtendedHeader(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ // Get required offsets
+ uint partitionOffset = GetPartitionOffset(index);
+ if (partitionOffset == 0 || partitionOffset > reader.Length)
+ {
+ Console.WriteLine($"Partition {index} No Data... Skipping...");
+ return false;
+ }
+
+ uint extHeaderSize = GetExtendedHeaderSize(index);
+ if (extHeaderSize == 0)
+ {
+ Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
+ return false;
+ }
+
+ // Seek to the extended header
+ reader.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
+ writer.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
+
+ Console.WriteLine($"Partition {index}: Encrypting - ExHeader");
+
+ // Create the Plain AES cipher for this partition
+ var cipher = AESCTR.CreateEncryptionCipher(keys.NormalKey2C, PlainIV(index));
+
+ // Process the extended header
+ AESCTR.PerformOperation(CXTExtendedDataHeaderLength, cipher, reader, writer, 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
+ reader.Seek(0, SeekOrigin.Begin);
+#endif
+ writer.Flush();
+ return true;
+ }
+
+ ///
+ /// Encrypt the ExeFS, if it exists
+ ///
+ /// Index of the partition
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private bool EncryptExeFS(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ if (ExeFSHeaders is null || index < 0 || index > ExeFSHeaders.Length)
+ {
+ Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
+ return false;
+ }
+
+ // Get the ExeFS header
+ var exefsHeader = ExeFSHeaders[index];
+ if (exefsHeader is null)
+ {
+ Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
+ return false;
+ }
+
+ // For all but the original crypto method, process each of the files in the table
+ var backupHeader = BackupHeader;
+ if (backupHeader!.Flags!.CryptoMethod != CryptoMethod.Original)
+ EncryptExeFSFileEntries(index, keys, reader, writer);
+
+ // Encrypt the filename table
+ EncryptExeFSFilenameTable(index, keys, reader, writer);
+
+ // Get the ExeFS files offset
+ uint exeFsHeaderOffset = GetExeFSOffset(index);
+ uint exeFsFilesOffset = exeFsHeaderOffset + MediaUnitSize;
+
+ // Seek to the ExeFS
+ reader.Seek(exeFsFilesOffset, SeekOrigin.Begin);
+ writer.Seek(exeFsFilesOffset, SeekOrigin.Begin);
+
+ // Create the ExeFS AES cipher for this partition
+ uint ctroffsetE = MediaUnitSize / 0x10;
+ byte[] exefsIVWithOffset = ExeFSIV(index).Add(ctroffsetE);
+ var cipher = AESCTR.CreateEncryptionCipher(keys.NormalKey2C, exefsIVWithOffset);
+
+ // Setup and perform the encryption
+ uint exeFsSize = GetExeFSSize(index) - MediaUnitSize;
+ AESCTR.PerformOperation(exeFsSize,
+ cipher,
+ reader,
+ writer,
+ s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
+
+ return true;
+ }
+
+ ///
+ /// Encrypt the ExeFS Filename Table
+ ///
+ /// Index of the partition
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private void EncryptExeFSFilenameTable(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ // Get ExeFS offset
+ uint exeFsOffset = GetExeFSOffset(index);
+ if (exeFsOffset == 0 || exeFsOffset > reader.Length)
+ {
+ Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
+ return;
+ }
+
+ // Seek to the ExeFS header
+ reader.Seek(exeFsOffset, SeekOrigin.Begin);
+ writer.Seek(exeFsOffset, SeekOrigin.Begin);
+
+ Console.WriteLine($"Partition {index} ExeFS: Encrypting - ExeFS Filename Table");
+
+ // Create the ExeFS AES cipher for this partition
+ var cipher = AESCTR.CreateEncryptionCipher(keys.NormalKey2C, ExeFSIV(index));
+
+ // Process the filename table
+ byte[] readBytes = reader.ReadBytes((int)MediaUnitSize);
+ byte[] processedBytes = cipher.ProcessBytes(readBytes);
+ writer.Write(processedBytes);
+
+#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
+ reader.Seek(0, SeekOrigin.Begin);
+#endif
+ writer.Flush();
+ }
+
+ ///
+ /// Encrypt the ExeFS file entries
+ ///
+ /// Index of the partition
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private void EncryptExeFSFileEntries(int index, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ // Get ExeFS offset
+ uint exeFsHeaderOffset = GetExeFSOffset(index);
+ if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > reader.Length)
+ {
+ Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
+ return;
+ }
+
+ // Get to the start of the files
+ uint exeFsFilesOffset = exeFsHeaderOffset + MediaUnitSize;
+
+ // If the header failed to read, log and return
+ var exeFsHeader = ExeFSHeaders?[index];
+ if (exeFsHeader?.FileHeaders is null)
+ {
+ Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
+ return;
+ }
+
+ // Loop through and process all headers
+ for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
+ {
+ // Only attempt to process code binary files
+ if (!IsCodeBinary(index, i))
+ continue;
+
+ // Get the file header
+ var fileHeader = exeFsHeader.FileHeaders[i];
+ if (fileHeader is null)
+ continue;
+
+ // Create the ExeFS AES ciphers for this partition
+ uint ctroffset = (fileHeader.FileOffset + MediaUnitSize) / 0x10;
+ byte[] exefsIVWithOffsetForHeader = ExeFSIV(index).Add(ctroffset);
+ var firstCipher = AESCTR.CreateEncryptionCipher(keys.NormalKey, exefsIVWithOffsetForHeader);
+ var secondCipher = AESCTR.CreateDecryptionCipher(keys.NormalKey2C, exefsIVWithOffsetForHeader);
+
+ // Seek to the file entry
+ reader.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
+ writer.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
+
+ // Setup and perform the encryption
+ AESCTR.PerformOperation(fileHeader.FileSize,
+ firstCipher,
+ secondCipher,
+ reader,
+ writer,
+ s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
+ }
+ }
+
+ ///
+ /// Encrypt the RomFS, if it exists
+ ///
+ /// Index of the partition
+ /// Encryption settings
+ /// Keys for the partition
+ /// Stream representing the input
+ /// Stream representing the output
+ private bool EncryptRomFS(int index, EncryptionSettings settings, PartitionKeys keys, Stream reader, Stream writer)
+ {
+ // Validate the RomFS
+ uint romFsOffset = GetRomFSOffset(index);
+ if (romFsOffset == 0 || romFsOffset > reader.Length)
+ {
+ Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
+ return false;
+ }
+
+ uint romFsSize = GetRomFSSize(index);
+ if (romFsSize == 0)
+ {
+ Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
+ return false;
+ }
+
+ // Seek to the RomFS
+ reader.Seek(romFsOffset, SeekOrigin.Begin);
+ writer.Seek(romFsOffset, SeekOrigin.Begin);
+
+ // Force setting encryption keys for partitions 1 and above
+ if (index > 0)
+ {
+ var backupHeader = BackupHeader;
+ keys.SetRomFSValues(backupHeader.Flags.BitMasks,
+ settings.AESHardwareConstant,
+ settings.Development ? settings.DevKeyX0x2C : settings.KeyX0x2C);
+ }
+
+ // Create the RomFS AES cipher for this partition
+ var cipher = AESCTR.CreateEncryptionCipher(keys.NormalKey, RomFSIV(index));
+
+ // Setup and perform the decryption
+ AESCTR.PerformOperation(romFsSize,
+ cipher,
+ reader,
+ writer,
+ s => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
+
+ return true;
+ }
+
+ ///
+ /// Update the CryptoMethod and BitMasks for the encrypted partition
+ ///
+ /// Index of the partition
+ /// Stream representing the output
+ private void UpdateEncryptCryptoAndMasks(int index, Stream writer)
+ {
+ // Get required offsets
+ uint partitionOffset = GetPartitionOffset(index);
+
+ // Get the backup header
+ var backupHeader = BackupHeader;
+ if (backupHeader?.Flags is null)
+ return;
+
+ // Seek to the CryptoMethod location
+ writer.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
+
+ // Write the new CryptoMethod
+ // - For partitions 1 and up, set crypto-method to 0x00
+ // - If partition 0, restore crypto-method from backup flags
+ byte cryptoMethod = index > 0 ? (byte)CryptoMethod.Original : (byte)backupHeader.Flags.CryptoMethod;
+ writer.Write(cryptoMethod);
+ writer.Flush();
+
+ // Seek to the BitMasks location
+ writer.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
+
+ // Write the new BitMasks flag
+ BitMasks flag = GetBitMasks(index);
+ flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF;
+ flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupHeader.Flags.BitMasks;
+ writer.Write((byte)flag);
+ writer.Flush();
+ }
+
+ #endregion
+
#region Helper Classes
+ ///
+ /// Encryption settings to be passed around during processing
+ ///
+ private class EncryptionSettings
+ {
+ ///
+ /// Indicates if development images are expected
+ ///
+ public bool Development { get; set; }
+
+ ///
+ /// AES Hardware Constant
+ ///
+ /// TODO: Validate this value on assignment
+ public byte[] AESHardwareConstant { get; set; } = [];
+
+ ///
+ /// KeyX 0x18 (New 3DS 9.3)
+ ///
+ public byte[] KeyX0x18
+ {
+ get;
+ set
+ {
+ // Ignore missing key data
+ if (value.Length == 0)
+ return;
+
+ // Validate the key data
+ var cipher = AESCTR.CreateEncryptionCipher(KeyX0x18, TestIV);
+ byte[] actual = cipher.ProcessBytes(TestPattern);
+ if (!actual.EqualsExactly(ExpectedKeyX0x18))
+ return;
+
+ // Assign the validated value
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// Dev KeyX 0x18 (New 3DS 9.3)
+ ///
+ public byte[] DevKeyX0x18
+ {
+ get;
+ set
+ {
+ // Ignore missing key data
+ if (value.Length == 0)
+ return;
+
+ // Validate the key data
+ var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x18, TestIV);
+ byte[] actual = cipher.ProcessBytes(TestPattern);
+ if (!actual.EqualsExactly(ExpectedDevKeyX0x18))
+ return;
+
+ // Assign the validated value
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// KeyX 0x1B (New 3DS 9.6)
+ ///
+ public byte[] KeyX0x1B
+ {
+ get;
+ set
+ {
+ // Ignore missing key data
+ if (value.Length == 0)
+ return;
+
+ // Validate the key data
+ var cipher = AESCTR.CreateEncryptionCipher(KeyX0x1B, TestIV);
+ byte[] actual = cipher.ProcessBytes(TestPattern);
+ if (!actual.EqualsExactly(ExpectedKeyX0x1B))
+ return;
+
+ // Assign the validated value
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// Dev KeyX 0x1B New 3DS 9.6)
+ ///
+ public byte[] DevKeyX0x1B
+ {
+ get;
+ set
+ {
+ // Ignore missing key data
+ if (value.Length == 0)
+ return;
+
+ // Validate the key data
+ var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x1B, TestIV);
+ byte[] actual = cipher.ProcessBytes(TestPattern);
+ if (!actual.EqualsExactly(ExpectedDevKeyX0x1B))
+ return;
+
+ // Assign the validated value
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// KeyX 0x25 (> 7.x)
+ ///
+ public byte[] KeyX0x25
+ {
+ get;
+ set
+ {
+ // Ignore missing key data
+ if (value.Length == 0)
+ return;
+
+ // Validate the key data
+ var cipher = AESCTR.CreateEncryptionCipher(KeyX0x25, TestIV);
+ byte[] actual = cipher.ProcessBytes(TestPattern);
+ if (!actual.EqualsExactly(ExpectedKeyX0x25))
+ return;
+
+ // Assign the validated value
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// Dev KeyX 0x25 (> 7.x)
+ ///
+ public byte[] DevKeyX0x25
+ {
+ get;
+ set
+ {
+ // Ignore missing key data
+ if (value.Length == 0)
+ return;
+
+ // Validate the key data
+ var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x25, TestIV);
+ byte[] actual = cipher.ProcessBytes(TestPattern);
+ if (!actual.EqualsExactly(ExpectedDevKeyX0x25))
+ return;
+
+ // Assign the validated value
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// KeyX 0x2C (< 6.x)
+ ///
+ public byte[] KeyX0x2C
+ {
+ get;
+ set
+ {
+ // Ignore missing key data
+ if (value.Length == 0)
+ return;
+
+ // Validate the key data
+ var cipher = AESCTR.CreateEncryptionCipher(KeyX0x2C, TestIV);
+ byte[] actual = cipher.ProcessBytes(TestPattern);
+ if (!actual.EqualsExactly(ExpectedKeyX0x2C))
+ return;
+
+ // Assign the validated value
+ field = value;
+ }
+ } = [];
+
+ ///
+ /// Dev KeyX 0x2C (< 6.x)
+ ///
+ public byte[] DevKeyX0x2C
+ {
+ get;
+ set
+ {
+ // Ignore missing key data
+ if (value.Length == 0)
+ return;
+
+ // Validate the key data
+ var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x2C, TestIV);
+ byte[] actual = cipher.ProcessBytes(TestPattern);
+ if (!actual.EqualsExactly(ExpectedDevKeyX0x2C))
+ return;
+
+ // Assign the validated value
+ field = value;
+ }
+ } = [];
+
+ #region Internal Test Values
+
+ ///
+ /// Initial value for key validation tests
+ ///
+ private static readonly byte[] TestIV =
+ [
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+ ];
+
+ ///
+ /// Pattern to use for key validation tests
+ ///
+ private static readonly byte[] TestPattern =
+ [
+ 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
+ 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
+ 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
+ 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
+ 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
+ 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
+ 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
+ 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
+ ];
+
+ ///
+ /// Expected output value for KeyX0x18
+ ///
+ private static readonly byte[] ExpectedKeyX0x18 =
+ [
+ 0x06, 0xF1, 0xB2, 0x3B, 0x12, 0xAD, 0x80, 0xC1,
+ 0x13, 0xC6, 0x18, 0x3D, 0x27, 0xB8, 0xB9, 0x95,
+ 0x49, 0x73, 0x59, 0x82, 0xEF, 0xFE, 0x16, 0x48,
+ 0x91, 0x2A, 0x89, 0x55, 0x9A, 0xDC, 0x3C, 0xA0,
+ 0x84, 0x46, 0x14, 0xE0, 0x16, 0x59, 0x8E, 0x4F,
+ 0xC2, 0x6C, 0x52, 0xA4, 0x7D, 0xAD, 0x4F, 0x23,
+ 0xF1, 0xC6, 0x99, 0x44, 0x39, 0xB7, 0x42, 0xF0,
+ 0x1F, 0xBB, 0x02, 0xF6, 0x0A, 0x8A, 0xC2, 0x9A,
+ ];
+
+ ///
+ /// Expected output value for DevKeyX0x18
+ ///
+ private static readonly byte[] ExpectedDevKeyX0x18 =
+ [
+ 0x99, 0x6E, 0x3C, 0x54, 0x97, 0x3C, 0xEA, 0xE8,
+ 0xBA, 0xAE, 0x18, 0x5C, 0x93, 0x27, 0x65, 0x50,
+ 0xF6, 0x6D, 0x67, 0xD7, 0xEF, 0xBD, 0x7C, 0xCB,
+ 0x8A, 0xC1, 0x1A, 0x54, 0xFC, 0x3B, 0x8B, 0x3A,
+ 0x0E, 0xE5, 0xEF, 0x27, 0x4A, 0x73, 0x7E, 0x0A,
+ 0x2E, 0x2E, 0x9D, 0xAF, 0x6C, 0x03, 0xF2, 0x91,
+ 0xC4, 0xFA, 0x73, 0xFD, 0x6B, 0xA0, 0x07, 0xD4,
+ 0x75, 0x5B, 0x6F, 0x2E, 0x8B, 0x68, 0x4C, 0xD1,
+ ];
+
+ ///
+ /// Expected output value for KeyX0x1B
+ ///
+ private static readonly byte[] ExpectedKeyX0x1B =
+ [
+ 0x0A, 0xE4, 0x79, 0x02, 0x1B, 0xFA, 0x25, 0x4B,
+ 0x2D, 0x92, 0x4F, 0xA8, 0x41, 0x59, 0xCE, 0x10,
+ 0x09, 0xE6, 0x08, 0x61, 0x23, 0xC7, 0xD2, 0x30,
+ 0x84, 0x37, 0xD5, 0x49, 0x42, 0x94, 0xB2, 0x70,
+ 0x6A, 0xF3, 0x75, 0xB0, 0x1F, 0x4F, 0xA1, 0xCE,
+ 0x03, 0xA2, 0x6A, 0x19, 0x5D, 0x32, 0x0D, 0xB5,
+ 0x79, 0xCD, 0xFD, 0xF0, 0xDE, 0x49, 0x26, 0x2D,
+ 0x29, 0x36, 0x30, 0x69, 0x8B, 0x45, 0xE1, 0xFC,
+ ];
+
+ ///
+ /// Expected output value for DevKeyX0x1B
+ ///
+ private static readonly byte[] ExpectedDevKeyX0x1B =
+ [
+ 0x16, 0x4F, 0xD9, 0x58, 0xC9, 0x20, 0xB3, 0xED,
+ 0xC4, 0xEB, 0x57, 0x39, 0x10, 0xEF, 0xA8, 0xCC,
+ 0xE5, 0x49, 0xBF, 0x52, 0x10, 0xA9, 0xCC, 0xE1,
+ 0x65, 0x3B, 0x2D, 0x51, 0x45, 0xFB, 0x60, 0x52,
+ 0x3E, 0x29, 0xEB, 0xEB, 0x3F, 0xF2, 0x76, 0x08,
+ 0x00, 0x05, 0x7F, 0x64, 0x29, 0x4A, 0x17, 0x22,
+ 0x56, 0x7F, 0x49, 0x94, 0x1A, 0x8C, 0x56, 0x35,
+ 0x38, 0xBE, 0xA4, 0x2E, 0x58, 0xD3, 0x81, 0x8C,
+ ];
+
+ ///
+ /// Expected output value for KeyX0x25
+ ///
+ private static readonly byte[] ExpectedKeyX0x25 =
+ [
+ 0x37, 0xBC, 0x73, 0xD6, 0xEE, 0x73, 0xE0, 0x94,
+ 0x42, 0x84, 0x74, 0xE5, 0xD8, 0xFB, 0x5F, 0x65,
+ 0xF4, 0xCF, 0x2E, 0xC1, 0x43, 0x48, 0x6C, 0xAA,
+ 0xC8, 0xF9, 0x96, 0xE6, 0x33, 0xDD, 0xE7, 0xBF,
+ 0xD2, 0x21, 0x89, 0x39, 0x13, 0xD1, 0xEC, 0xCA,
+ 0x1D, 0x5D, 0x1F, 0x77, 0x95, 0xD2, 0x8B, 0x27,
+ 0x92, 0x79, 0xC5, 0x1D, 0x72, 0xA7, 0x28, 0x57,
+ 0x41, 0x0E, 0x46, 0xB8, 0x80, 0x7B, 0x7C, 0x0D,
+ ];
+
+ ///
+ /// Expected output value for DevKeyX0x25
+ ///
+ private static readonly byte[] ExpectedDevKeyX0x25 =
+ [
+ 0x71, 0x65, 0x30, 0xF2, 0x68, 0xEC, 0x65, 0x0A,
+ 0x8C, 0x9E, 0xC5, 0x5A, 0xFA, 0x37, 0x8E, 0xDA,
+ 0x7B, 0x58, 0x3B, 0x66, 0x7C, 0x9D, 0x16, 0xD9,
+ 0x2D, 0x8F, 0xCF, 0x04, 0x66, 0x7F, 0x27, 0x41,
+ 0xBF, 0x5F, 0x1E, 0x11, 0x4C, 0xD6, 0xB9, 0x0A,
+ 0xC5, 0x42, 0xCF, 0x2B, 0x87, 0x6B, 0xD4, 0x72,
+ 0x4D, 0x9C, 0x29, 0x2E, 0xF8, 0xB0, 0x6F, 0x22,
+ 0x35, 0x5B, 0x96, 0x83, 0xD1, 0xE4, 0x5E, 0xDB,
+ ];
+
+ ///
+ /// Expected output value for KeyX0x2C
+ ///
+ private static readonly byte[] ExpectedKeyX0x2C =
+ [
+ 0xAE, 0x44, 0x20, 0xDB, 0xA5, 0x96, 0xDC, 0xF3,
+ 0xD8, 0x23, 0x9E, 0x3C, 0x44, 0x73, 0x3D, 0xCD,
+ 0x07, 0xD5, 0xF8, 0xD0, 0xC6, 0xB3, 0x5A, 0x80,
+ 0xB5, 0x5A, 0x55, 0x30, 0x5D, 0x4A, 0xBE, 0x61,
+ 0xBF, 0xEF, 0x64, 0x17, 0x28, 0xD6, 0x26, 0x52,
+ 0x42, 0x4D, 0x8F, 0x1C, 0xBC, 0x63, 0xD3, 0x91,
+ 0x7D, 0xA6, 0x4F, 0xAF, 0x26, 0x38, 0x60, 0xEE,
+ 0x79, 0x92, 0x2F, 0xD8, 0xCA, 0x4E, 0xE7, 0xEC,
+ ];
+
+ ///
+ /// Expected output value for DevKeyX0x2C
+ ///
+ private static readonly byte[] ExpectedDevKeyX0x2C =
+ [
+ 0x5F, 0x73, 0xD5, 0x9A, 0x67, 0xFF, 0x8C, 0x12,
+ 0x31, 0x58, 0x0B, 0x58, 0x46, 0xFE, 0x05, 0x16,
+ 0x92, 0xE4, 0x84, 0x06, 0x18, 0x9B, 0x58, 0x91,
+ 0xE7, 0xF8, 0xCD, 0xA9, 0x95, 0xAC, 0x07, 0xCD,
+ 0x43, 0x20, 0x7A, 0x8C, 0xCC, 0xAB, 0x48, 0x50,
+ 0x29, 0x2F, 0x96, 0x73, 0xB0, 0xD9, 0xE5, 0xCB,
+ 0xE6, 0x9A, 0x0D, 0xF7, 0xD0, 0x1E, 0xC2, 0xEC,
+ 0xC1, 0xE2, 0x8E, 0xEE, 0x89, 0xB9, 0xB1, 0x97,
+ ];
+
+ #endregion
+ }
+
///
/// Set of all keys associated with a partition
///