From b34c329a825e4be87ecdb67db41ecb2c94a94a8b Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 12 Jun 2026 09:36:25 -0400 Subject: [PATCH] Update packages --- NDecrypt.Core/DSProcessor.cs | 5 +- NDecrypt.Core/NDecrypt.Core.csproj | 6 +- NDecrypt.Core/PartitionKeys.cs | 101 --- NDecrypt.Core/ThreeDSProcessor.cs | 1207 +--------------------------- NDecrypt/Features/BaseFeature.cs | 8 +- 5 files changed, 43 insertions(+), 1284 deletions(-) delete mode 100644 NDecrypt.Core/PartitionKeys.cs diff --git a/NDecrypt.Core/DSProcessor.cs b/NDecrypt.Core/DSProcessor.cs index 0379521..6841483 100644 --- a/NDecrypt.Core/DSProcessor.cs +++ b/NDecrypt.Core/DSProcessor.cs @@ -2,8 +2,9 @@ using System.IO; using System.Text; using SabreTools.Hashing; -using SabreTools.IO.Extensions; -using SabreTools.Serialization.Wrappers; +using SabreTools.Matching; +using SabreTools.Numerics.Extensions; +using SabreTools.Wrappers; namespace NDecrypt.Core { diff --git a/NDecrypt.Core/NDecrypt.Core.csproj b/NDecrypt.Core/NDecrypt.Core.csproj index c858e6d..44a7109 100644 --- a/NDecrypt.Core/NDecrypt.Core.csproj +++ b/NDecrypt.Core/NDecrypt.Core.csproj @@ -24,9 +24,9 @@ - - - + + + diff --git a/NDecrypt.Core/PartitionKeys.cs b/NDecrypt.Core/PartitionKeys.cs deleted file mode 100644 index 6e84147..0000000 --- a/NDecrypt.Core/PartitionKeys.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using SabreTools.Data.Models.N3DS; -using SabreTools.IO.Extensions; - -namespace NDecrypt.Core -{ - /// - /// Set of all keys associated with a partition - /// - internal class PartitionKeys - { - /// - /// Primary AES-CTR encryption key - /// - /// Used for both EXE-FS and ROM-FS - public byte[] NormalKey { get; private set; } - - /// - /// Secondary AES-CTR encryption key - /// - /// Used for only EXE-FS - public byte[] NormalKey2C { get; } - - /// - /// First 16 bytes of the RSA-2048 signature - /// - /// Used as an XOR value during key generation - private readonly byte[] KeyY; - - /// - /// Create a new set of keys for a given partition - /// - /// RSA-2048 signature from the partition - /// BitMasks from the partition or backup header - /// AES hardware constant to use - /// KeyX value to assign based on crypto method and development status - /// KeyX2C value to assign based on development status - public PartitionKeys(byte[]? signature, BitMasks masks, byte[] hardwareConstant, byte[] keyX, byte[] keyX0x2C) - { - // Validate inputs - if (signature is not null && signature.Length < 16) - throw new ArgumentOutOfRangeException(nameof(signature), $"{nameof(signature)} must be at least 16 bytes"); - - // Backup headers can't have a KeyY value set - KeyY = new byte[16]; - if (signature is not null) - Array.Copy(signature, KeyY, 16); - - // Special case for zero-key -#if NET20 || NET35 - if ((masks & BitMasks.FixedCryptoKey) > 0) -#else - if (masks.HasFlag(BitMasks.FixedCryptoKey)) -#endif - { - Console.WriteLine("Encryption Method: Zero Key"); - NormalKey = new byte[16]; - NormalKey2C = new byte[16]; - return; - } - - // Set the standard normal key values - NormalKey = ProcessKey(keyX, KeyY, hardwareConstant); - NormalKey2C = ProcessKey(keyX0x2C, KeyY, hardwareConstant); - } - - /// - /// Set RomFS values based on the bit masks - /// - /// BitMasks from the partition or backup header - /// AES hardware constant to use - /// KeyX2C value to assign based on development status - public void SetRomFSValues(BitMasks masks, byte[] hardwareConstant, byte[] keyX0x2C) - { - // NormalKey has a constant value for zero-key -#if NET20 || NET35 - if ((masks & BitMasks.FixedCryptoKey) > 0) -#else - if (masks.HasFlag(BitMasks.FixedCryptoKey)) -#endif - { - NormalKey = new byte[16]; - return; - } - - // Encrypting RomFS for partitions 1 and up always use Key0x2C - NormalKey = ProcessKey(keyX0x2C, KeyY, hardwareConstant); - } - - /// - /// Process a key with the standard processing steps - /// - private static byte[] ProcessKey(byte[] keyBase, byte[] keyY, byte[] hardwareConstant) - { - byte[] processed = keyBase.RotateLeft(2); - processed = processed.Xor(keyY); - processed = processed.Add(hardwareConstant); - return processed.RotateLeft(87); - } - } -} diff --git a/NDecrypt.Core/ThreeDSProcessor.cs b/NDecrypt.Core/ThreeDSProcessor.cs index e991126..bde7e84 100644 --- a/NDecrypt.Core/ThreeDSProcessor.cs +++ b/NDecrypt.Core/ThreeDSProcessor.cs @@ -1,399 +1,23 @@ using System; using System.IO; using System.Text; -using SabreTools.Data.Models.N3DS; -using SabreTools.IO.Encryption; -using SabreTools.IO.Extensions; -using SabreTools.Serialization.Wrappers; -using static SabreTools.Data.Models.N3DS.Constants; +using SabreTools.Security.Cryptography; +using SabreTools.Wrappers; namespace NDecrypt.Core { - // TODO: Strip this out when Serialization is updated public class ThreeDSProcessor : ICartProcessor { /// - /// AES Hardware Constant + /// Encryption settings for processing /// - /// TODO: Validate this value on assignment - public byte[] AESHardwareConstant { get; set; } = []; + private readonly N3DSEncryptionSettings _settings; - /// - /// KeyX 0x18 (New 3DS 9.3) - /// - public byte[] KeyX0x18 + public ThreeDSProcessor(N3DSEncryptionSettings settings) { - get; - set - { - // Ignore missing key data - if (value.Length == 0) - return; - - // Validate the key data - var cipher = AESCTR.CreateEncryptionCipher(value, 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(value, 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(value, 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(value, 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(value, 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(value, 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(value, 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(value, TestIV); - byte[] actual = cipher.ProcessBytes(TestPattern); - if (!actual.EqualsExactly(ExpectedDevKeyX0x2C)) - return; - - // Assign the validated value - field = value; - } - } = []; - - /// - /// Indicates if development images are expected - /// - private readonly bool _development; - - #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 - - public ThreeDSProcessor(bool development) - { - _development = development; + _settings = settings; } - #region Common - - /// - /// Get KeyX value for a crypto method and development status combination - /// - private byte[] GetKeyXForCryptoMethod(CryptoMethod method) - { - switch (method) - { - case CryptoMethod.Original: - Console.WriteLine("Encryption Method: Key 0x2C"); - return _development ? DevKeyX0x2C : KeyX0x2C; - - case CryptoMethod.Seven: - Console.WriteLine("Encryption Method: Key 0x25"); - return _development ? DevKeyX0x25 : KeyX0x25; - - case CryptoMethod.NineThree: - Console.WriteLine("Encryption Method: Key 0x18"); - return _development ? DevKeyX0x18 : KeyX0x18; - - case CryptoMethod.NineSix: - Console.WriteLine("Encryption Method: Key 0x1B"); - return _development ? DevKeyX0x1B : KeyX0x1B; - - // This should never happen - default: - Console.WriteLine("Encryption Method: UNSUPPORTED"); - return []; - } - } - - #endregion - #region Decrypt /// @@ -420,7 +44,19 @@ namespace NDecrypt.Core } // Decrypt all 8 NCCH partitions - DecryptAllPartitions(cart, force, reader, writer); + cart.DecryptAllPartitions(force, + reader, + writer, + _settings.Development, + _settings.AESHardwareConstant, + _settings.KeyX0x18, + _settings.DevKeyX0x18, + _settings.KeyX0x1B, + _settings.DevKeyX0x1B, + _settings.KeyX0x25, + _settings.DevKeyX0x25, + _settings.KeyX0x2C, + _settings.DevKeyX0x2C); return true; } catch @@ -431,391 +67,6 @@ namespace NDecrypt.Core } } - /// - /// Decrypt all partitions in the partition table of an NCSD header - /// - /// Cart representing the 3DS file - /// Indicates if the operation should be forced - /// Stream representing the input - /// Stream representing the output - private void DecryptAllPartitions(N3DS cart, bool force, Stream reader, Stream writer) - { - // Check the partitions table - if (cart.PartitionsTable is null || cart.Partitions is null) - { - Console.WriteLine("Invalid partitions table!"); - return; - } - - // Iterate over all 8 NCCH partitions - for (int p = 0; p < 8; p++) - { - var partition = cart.Partitions[p]; - if (partition is null || partition.MagicID != NCCHMagicNumber) - { - Console.WriteLine($"Partition {p} Not found... Skipping..."); - continue; - } - - // Check the partition has data - var partitionEntry = cart.PartitionsTable[p]; - if (partitionEntry is null || partitionEntry.Length == 0) - { - Console.WriteLine($"Partition {p} No data... Skipping..."); - continue; - } - - // Decrypt the partition, if possible - if (ShouldDecryptPartition(cart, p, force)) - DecryptPartition(cart, p, reader, writer); - } - } - - /// - /// Determine if the current partition should be decrypted - /// s - private static bool ShouldDecryptPartition(N3DS cart, 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 (cart.PossiblyDecrypted(index)) - { - Console.WriteLine($"Partition {index}: Already Decrypted?..."); - return false; - } - - // By default, it passes - return true; - } - - /// - /// Decrypt a single partition - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Stream representing the input - /// Stream representing the output - private void DecryptPartition(N3DS cart, int index, Stream reader, Stream writer) - { - // Determine the keys needed for this partition - PartitionKeys? keys = GetDecryptionKeys(cart, index); - if (keys == null) - { - Console.WriteLine($"Partition {index} could not generate keys. Skipping..."); - return; - } - - // Decrypt the parts of the partition - DecryptExtendedHeader(cart, index, keys, reader, writer); - DecryptExeFS(cart, index, keys, reader, writer); - DecryptRomFS(cart, index, keys, reader, writer); - - // Update the flags - UpdateDecryptCryptoAndMasks(cart, index, writer); - } - - /// - /// Determine the set of keys to be used for decryption - /// - /// Cart representing the 3DS file - /// Index of the partition - private PartitionKeys? GetDecryptionKeys(N3DS cart, int index) - { - // Get the partition - var partition = cart.Partitions?[index]; - if (partition?.Flags is null) - return null; - - // Get partition-specific values - byte[]? signature = partition.RSA2048Signature; - BitMasks masks = cart.GetBitMasks(index); - CryptoMethod method = cart.GetCryptoMethod(index); - - // Get the partition keys - byte[] keyX = GetKeyXForCryptoMethod(method); - byte[] keyX0x2C = _development ? DevKeyX0x2C : KeyX0x2C; - return new PartitionKeys(signature, masks, AESHardwareConstant, keyX, keyX0x2C); - } - - /// - /// Decrypt the extended header, if it exists - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private bool DecryptExtendedHeader(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - // Get required offsets - uint partitionOffset = cart.GetPartitionOffset(index); - if (partitionOffset == 0 || partitionOffset > reader.Length) - { - Console.WriteLine($"Partition {index} No Data... Skipping..."); - return false; - } - - uint extHeaderSize = cart.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, cart.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 - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private bool DecryptExeFS(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - // Validate the ExeFS - uint exeFsHeaderOffset = cart.GetExeFSOffset(index); - if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > reader.Length) - { - Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); - return false; - } - - uint exeFsSize = cart.GetExeFSSize(index); - if (exeFsSize == 0) - { - Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); - return false; - } - - // Decrypt the filename table - DecryptExeFSFilenameTable(cart, index, keys, reader, writer); - - // For all but the original crypto method, process each of the files in the table - if (cart.GetCryptoMethod(index) != CryptoMethod.Original) - DecryptExeFSFileEntries(cart, index, keys, reader, writer); - - // Get the ExeFS files offset - uint exeFsFilesOffset = exeFsHeaderOffset + cart.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 = cart.MediaUnitSize / 0x10; - byte[] exefsIVWithOffset = cart.ExeFSIV(index).Add(ctroffsetE); - var cipher = AESCTR.CreateDecryptionCipher(keys.NormalKey2C, exefsIVWithOffset); - - // Setup and perform the decryption - exeFsSize -= cart.MediaUnitSize; - AESCTR.PerformOperation(exeFsSize, - cipher, - reader, - writer, - s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}")); - - return true; - } - - /// - /// Decrypt the ExeFS Filename Table - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private void DecryptExeFSFilenameTable(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - // Get ExeFS offset - uint exeFsOffset = cart.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, cart.ExeFSIV(index)); - - // Process the filename table - byte[] readBytes = reader.ReadBytes((int)cart.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 - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private void DecryptExeFSFileEntries(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - if (cart.ExeFSHeaders is null || index < 0 || index > cart.ExeFSHeaders.Length) - { - Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); - return; - } - - // Reread the decrypted ExeFS header - uint exeFsHeaderOffset = cart.GetExeFSOffset(index); - reader.Seek(exeFsHeaderOffset, SeekOrigin.Begin); - cart.ExeFSHeaders[index] = SabreTools.Serialization.Readers.N3DS.ParseExeFSHeader(reader); - - // Get the ExeFS header - var exeFsHeader = cart.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 + cart.MediaUnitSize; - - // Loop through and process all headers - for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++) - { - // Only attempt to process code binary files - if (!cart.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 + cart.MediaUnitSize) / 0x10; - byte[] exefsIVWithOffsetForHeader = cart.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 - /// - /// Cart representing the 3DS file - /// Keys for the partition - /// Index of the partition - /// Stream representing the input - /// Stream representing the output - private bool DecryptRomFS(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - // Validate the RomFS - uint romFsOffset = cart.GetRomFSOffset(index); - if (romFsOffset == 0 || romFsOffset > reader.Length) - { - Console.WriteLine($"Partition {index} RomFS: No Data... Skipping..."); - return false; - } - - uint romFsSize = cart.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, cart.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 - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Stream representing the output - private static void UpdateDecryptCryptoAndMasks(N3DS cart, int index, Stream writer) - { - // Get required offsets - uint partitionOffset = cart.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 = cart.GetBitMasks(index); - flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF); - flag |= BitMasks.NoCrypto; - writer.Write((byte)flag); - writer.Flush(); - } - #endregion #region Encrypt @@ -844,7 +95,19 @@ namespace NDecrypt.Core } // Encrypt all 8 NCCH partitions - EncryptAllPartitions(cart, force, reader, writer); + cart.EncryptAllPartitions(force, + reader, + writer, + _settings.Development, + _settings.AESHardwareConstant, + _settings.KeyX0x18, + _settings.DevKeyX0x18, + _settings.KeyX0x1B, + _settings.DevKeyX0x1B, + _settings.KeyX0x25, + _settings.DevKeyX0x25, + _settings.KeyX0x2C, + _settings.DevKeyX0x2C); return true; } catch @@ -855,412 +118,6 @@ namespace NDecrypt.Core } } - /// - /// Encrypt all partitions in the partition table of an NCSD header - /// - /// Cart representing the 3DS file - /// Indicates if the operation should be forced - /// Stream representing the input - /// Stream representing the output - private void EncryptAllPartitions(N3DS cart, bool force, Stream reader, Stream writer) - { - // Check the partitions table - if (cart.PartitionsTable is null || cart.Partitions is null) - { - Console.WriteLine("Invalid partitions table!"); - return; - } - - // Iterate over all 8 NCCH partitions - for (int p = 0; p < 8; p++) - { - // Check the partition exists - var partition = cart.Partitions[p]; - if (partition is null || partition.MagicID != NCCHMagicNumber) - { - Console.WriteLine($"Partition {p} Not found... Skipping..."); - continue; - } - - // Check the partition has data - var partitionEntry = cart.PartitionsTable[p]; - if (partitionEntry is null || partitionEntry.Length == 0) - { - Console.WriteLine($"Partition {p} No data... Skipping..."); - continue; - } - - // Encrypt the partition, if possible - if (ShouldEncryptPartition(cart, p, force)) - EncryptPartition(cart, p, reader, writer); - } - } - - /// - /// Determine if the current partition should be encrypted - /// - private static bool ShouldEncryptPartition(N3DS cart, 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 (!cart.PossiblyDecrypted(index)) - { - Console.WriteLine($"Partition {index}: Already Encrypted?..."); - return false; - } - - // By default, it passes - return true; - } - - /// - /// Encrypt a single partition - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Stream representing the input - /// Stream representing the output - private void EncryptPartition(N3DS cart, int index, Stream reader, Stream writer) - { - // Determine the keys needed for this partition - PartitionKeys? keys = GetEncryptionKeys(cart, index); - if (keys == null) - { - Console.WriteLine($"Partition {index} could not generate keys. Skipping..."); - return; - } - - // Encrypt the parts of the partition - EncryptExtendedHeader(cart, index, keys, reader, writer); - EncryptExeFS(cart, index, keys, reader, writer); - EncryptRomFS(cart, index, keys, reader, writer); - - // Update the flags - UpdateEncryptCryptoAndMasks(cart, index, writer); - } - - /// - /// Determine the set of keys to be used for encryption - /// - /// Cart representing the 3DS file - /// Index of the partition - private PartitionKeys? GetEncryptionKeys(N3DS cart, int index) - { - // Get the partition - var partition = cart.Partitions?[index]; - if (partition is null) - return null; - - // Get the backup header - var backupHeader = cart.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(method); - byte[] keyX0x2C = _development ? DevKeyX0x2C : KeyX0x2C; - return new PartitionKeys(signature, masks, AESHardwareConstant, keyX, keyX0x2C); - } - - /// - /// Encrypt the extended header, if it exists - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private bool EncryptExtendedHeader(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - // Get required offsets - uint partitionOffset = cart.GetPartitionOffset(index); - if (partitionOffset == 0 || partitionOffset > reader.Length) - { - Console.WriteLine($"Partition {index} No Data... Skipping..."); - return false; - } - - uint extHeaderSize = cart.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, cart.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 - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private bool EncryptExeFS(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - if (cart.ExeFSHeaders is null || index < 0 || index > cart.ExeFSHeaders.Length) - { - Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping..."); - return false; - } - - // Get the ExeFS header - var exefsHeader = cart.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 = cart.BackupHeader; - if (backupHeader!.Flags!.CryptoMethod != CryptoMethod.Original) - EncryptExeFSFileEntries(cart, index, keys, reader, writer); - - // Encrypt the filename table - EncryptExeFSFilenameTable(cart, index, keys, reader, writer); - - // Get the ExeFS files offset - uint exeFsHeaderOffset = cart.GetExeFSOffset(index); - uint exeFsFilesOffset = exeFsHeaderOffset + cart.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 = cart.MediaUnitSize / 0x10; - byte[] exefsIVWithOffset = cart.ExeFSIV(index).Add(ctroffsetE); - var cipher = AESCTR.CreateEncryptionCipher(keys.NormalKey2C, exefsIVWithOffset); - - // Setup and perform the encryption - uint exeFsSize = cart.GetExeFSSize(index) - cart.MediaUnitSize; - AESCTR.PerformOperation(exeFsSize, - cipher, - reader, - writer, - s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}")); - - return true; - } - - /// - /// Encrypt the ExeFS Filename Table - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private void EncryptExeFSFilenameTable(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - // Get ExeFS offset - uint exeFsOffset = cart.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, cart.ExeFSIV(index)); - - // Process the filename table - byte[] readBytes = reader.ReadBytes((int)cart.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 - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private void EncryptExeFSFileEntries(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - // Get ExeFS offset - uint exeFsHeaderOffset = cart.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 + cart.MediaUnitSize; - - // If the header failed to read, log and return - var exeFsHeader = cart.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 (!cart.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 + cart.MediaUnitSize) / 0x10; - byte[] exefsIVWithOffsetForHeader = cart.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 - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Keys for the partition - /// Stream representing the input - /// Stream representing the output - private bool EncryptRomFS(N3DS cart, int index, PartitionKeys keys, Stream reader, Stream writer) - { - // Validate the RomFS - uint romFsOffset = cart.GetRomFSOffset(index); - if (romFsOffset == 0 || romFsOffset > reader.Length) - { - Console.WriteLine($"Partition {index} RomFS: No Data... Skipping..."); - return false; - } - - uint romFsSize = cart.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 = cart.BackupHeader; - keys.SetRomFSValues(backupHeader.Flags.BitMasks, - hardwareConstant: AESHardwareConstant, - keyX0x2C: _development ? DevKeyX0x2C : KeyX0x2C); - } - - // Create the RomFS AES cipher for this partition - var cipher = AESCTR.CreateEncryptionCipher(keys.NormalKey, cart.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 - /// - /// Cart representing the 3DS file - /// Index of the partition - /// Stream representing the output - private static void UpdateEncryptCryptoAndMasks(N3DS cart, int index, Stream writer) - { - // Get required offsets - uint partitionOffset = cart.GetPartitionOffset(index); - - // Get the backup header - var backupHeader = cart.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 = cart.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 Info diff --git a/NDecrypt/Features/BaseFeature.cs b/NDecrypt/Features/BaseFeature.cs index 56c7349..00960fe 100644 --- a/NDecrypt/Features/BaseFeature.cs +++ b/NDecrypt/Features/BaseFeature.cs @@ -4,7 +4,7 @@ using System.IO; using NDecrypt.Core; using SabreTools.CommandLine; using SabreTools.CommandLine.Inputs; -using SabreTools.IO.Extensions; +using SabreTools.Text.Extensions; namespace NDecrypt.Features { @@ -83,7 +83,7 @@ namespace NDecrypt.Features { // Set default values for tools _tools[FileType.NDS] = new DSProcessor(); - _tools[FileType.N3DS] = new ThreeDSProcessor(development: false); + _tools[FileType.N3DS] = new ThreeDSProcessor(new()); // Check the configuration path string? configPath = GetString(ConfigName, "config.json"); @@ -117,8 +117,9 @@ namespace NDecrypt.Features // Create the 3DS tool bool development = GetBoolean(DevelopmentName); - _tools[FileType.N3DS] = new ThreeDSProcessor(development) + var encryptionSettings = new SabreTools.Security.Cryptography.N3DSEncryptionSettings { + Development = development, AESHardwareConstant = config.AESHardwareConstant.FromHexString() ?? [], KeyX0x18 = config.KeyX0x18.FromHexString() ?? [], KeyX0x1B = config.KeyX0x1B.FromHexString() ?? [], @@ -129,6 +130,7 @@ namespace NDecrypt.Features DevKeyX0x25 = config.DevKeyX0x25.FromHexString() ?? [], DevKeyX0x2C = config.DevKeyX0x2C.FromHexString() ?? [], }; + _tools[FileType.N3DS] = new ThreeDSProcessor(encryptionSettings); } ///