using System; using System.IO; using System.Numerics; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using SabreTools.IO.Extensions; using SabreTools.Models.N3DS; namespace NDecrypt.Core { internal static class CommonOperations { #region AES /// /// Create AES cipher and intialize /// /// BigInteger representation of 128-bit encryption key /// AES initial value for counter /// True if cipher is created for encryption, false otherwise /// Initialized AES cipher public static IBufferedCipher CreateAESCipher(BigInteger key, byte[] iv, bool encrypt) { return encrypt ? CreateAESEncryptionCipher(key, iv) : CreateAESDecryptionCipher(key, iv); } /// /// Create AES decryption cipher and intialize /// /// BigInteger representation of 128-bit encryption key /// AES initial value for counter /// Initialized AES cipher public static IBufferedCipher CreateAESDecryptionCipher(BigInteger key, byte[] iv) { var keyParam = new KeyParameter(TakeSixteen(key)); var cipher = CipherUtilities.GetCipher("AES/CTR"); cipher.Init(forEncryption: false, new ParametersWithIV(keyParam, iv)); return cipher; } /// /// Create AES encryption cipher and intialize /// /// BigInteger representation of 128-bit encryption key /// AES initial value for counter /// Initialized AES cipher public static IBufferedCipher CreateAESEncryptionCipher(BigInteger key, byte[] iv) { var keyParam = new KeyParameter(TakeSixteen(key)); var cipher = CipherUtilities.GetCipher("AES/CTR"); cipher.Init(forEncryption: true, new ParametersWithIV(keyParam, iv)); return cipher; } /// /// Perform an AES operation using an existing cipher /// public static void PerformAESOperation(uint size, IBufferedCipher cipher, Stream input, Stream output, Action? progress) { // Get MiB-aligned block count and extra byte count int blockCount = (int)((long)size / (1024 * 1024)); int extraBytes = (int)((long)size % (1024 * 1024)); // Process MiB-aligned data if (blockCount > 0) { for (int i = 0; i < blockCount; i++) { byte[] readBytes = input.ReadBytes(1024 * 1024); byte[] processedBytes = cipher.ProcessBytes(readBytes); output.Write(processedBytes); output.Flush(); progress?.Invoke($"{i} / {blockCount + 1} MB"); } } // Process additional data if (extraBytes > 0) { byte[] readBytes = input.ReadBytes(extraBytes); byte[] finalBytes = cipher.DoFinal(readBytes); output.Write(finalBytes); output.Flush(); } progress?.Invoke($"{blockCount + 1} / {blockCount + 1} MB... Done!\r\n"); } /// /// Perform an AES operation using two existing ciphers /// public static void PerformAESOperation(uint size, IBufferedCipher firstCipher, IBufferedCipher secondCipher, Stream input, Stream output, Action progress) { // Get MiB-aligned block count and extra byte count int blockCount = (int)((long)size / (1024 * 1024)); int extraBytes = (int)((long)size % (1024 * 1024)); // Process MiB-aligned data if (blockCount > 0) { for (int i = 0; i < blockCount; i++) { byte[] readBytes = input.ReadBytes(1024 * 1024); byte[] firstProcessedBytes = firstCipher.ProcessBytes(readBytes); byte[] secondProcessedBytes = secondCipher.ProcessBytes(firstProcessedBytes); output.Write(secondProcessedBytes); output.Flush(); progress($"{i} / {blockCount + 1} MB"); } } // Process additional data if (extraBytes > 0) { byte[] readBytes = input.ReadBytes(extraBytes); byte[] firstFinalBytes = firstCipher.DoFinal(readBytes); byte[] secondFinalBytes = secondCipher.DoFinal(firstFinalBytes); output.Write(secondFinalBytes); output.Flush(); } progress($"{blockCount + 1} / {blockCount + 1} MB... Done!\r\n"); } /// /// Get a 16-byte array representation of a BigInteger /// /// BigInteger value to convert /// 16-byte array representing the BigInteger private static byte[] TakeSixteen(BigInteger input) { var inputArr = input.ToByteArray(); var arr = new byte[16]; Array.Copy(inputArr, arr, Math.Min(inputArr.Length, 16)); Array.Reverse(arr); if (arr.Length < 16) { byte[] temp = new byte[16]; for (int i = 0; i < (16 - arr.Length); i++) temp[i] = 0x00; Array.Copy(arr, 0, temp, 16 - arr.Length, arr.Length); arr = temp; } return arr; } #endregion #region Byte Arrays /// /// Add an integer value to a number represented by a byte array /// /// Byte array to add to /// Amount to add /// Byte array representing the new value public static byte[] AddToByteArray(byte[] input, int add) { int len = input.Length; Array.Reverse(input); var bigint = new BigInteger(input); bigint += add; var arr = bigint.ToByteArray(); Array.Reverse(arr); if (arr.Length < len) { byte[] temp = new byte[len]; for (int i = 0; i < (len - arr.Length); i++) temp[i] = 0x00; Array.Copy(arr, 0, temp, len - arr.Length, arr.Length); arr = temp; } return arr; } /// /// Perform a rotate left on a BigInteger /// /// BigInteger value to rotate /// Number of bits to rotate /// Maximum number of bits to rotate on /// Rotated BigInteger value public static BigInteger RotateLeft(BigInteger val, int r_bits, int max_bits) { return (val << r_bits % max_bits) & (BigInteger.Pow(2, max_bits) - 1) | ((val & (BigInteger.Pow(2, max_bits) - 1)) >> (max_bits - (r_bits % max_bits))); } #endregion #region Offsets /// /// Get the offset of a partition ExeFS /// /// Offset to the ExeFS of the partition, 0 on error public static uint GetExeFSOffset(Cart cart, int index) { // Empty partitions table means no size is available var partitionsTable = cart.Header?.PartitionsTable; if (partitionsTable == null) return 0; // Invalid partition table entry means no size is available var entry = partitionsTable[index]; if (entry == null) return 0; // Empty partitions array means no size is available var partitions = cart.Partitions; if (partitions == null) return 0; // Invalid partition means no size is available var header = partitions[index]; if (header == null) return 0; // 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) * 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 /// /// Offset to the partition, 0 on error public static uint GetPartitionOffset(Cart cart, int index) { // Empty partitions table means no size is available var partitionsTable = cart.Header?.PartitionsTable; if (partitionsTable == null) return 0; // Invalid partition table entry means no size is available var entry = partitionsTable[index]; if (entry == null) return 0; // Return the adjusted offset uint partitionOffsetMU = entry.Offset; 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 /// /// Offset to the RomFS of the partition, 0 on error public static uint GetRomFSOffset(Cart cart, int index) { // Empty partitions table means no size is available var partitionsTable = cart.Header?.PartitionsTable; if (partitionsTable == null) return 0; // Invalid partition table entry means no size is available var entry = partitionsTable[index]; if (entry == null) return 0; // Empty partitions array means no size is available var partitions = cart.Partitions; if (partitions == null) return 0; // Invalid partition means no size is available var header = partitions[index]; if (header == null) return 0; // 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) * 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 /// /// Get the size of a partition ExeFS /// /// Size of the partition ExeFS in bytes, 0 on error public static uint GetExeFSSize(Cart cart, int index) { // Empty partitions array means no size is available var partitions = cart.Partitions; if (partitions == null) return 0; // Invalid partition header means no size is available var header = partitions[index]; if (header == null) return 0; // Return the adjusted size return GetExeFSSize(header, cart.MediaUnitSize()); } /// /// Get the size of a partition ExeFS /// /// Size of the partition ExeFS in bytes, 0 on error public static uint GetExeFSSize(NCCHHeader header, uint mediaUnitSize) => header.ExeFSSizeInMediaUnits * mediaUnitSize; /// /// Get the size of a partition extended header /// /// Size of the partition extended header in bytes, 0 on error public static uint GetExtendedHeaderSize(Cart cart, int index) { // Empty partitions array means no size is available var partitions = cart.Partitions; if (partitions == null) return 0; // Invalid partition header means no size is available var header = partitions[index]; if (header == null) return 0; // Return the adjusted size return GetExtendedHeaderSize(header); } /// /// Get the size of a partition extended header /// /// Size of the partition extended header in bytes, 0 on error public static uint GetExtendedHeaderSize(NCCHHeader header) => header.ExtendedHeaderSizeInBytes; /// /// Get the size of a partition RomFS /// /// Size of the partition RomFS in bytes, 0 on error public static uint GetRomFSSize(Cart cart, int index) { // Empty partitions array means no size is available var partitions = cart.Partitions; if (partitions == null) return 0; // Invalid partition header means no size is available var header = partitions[index]; if (header == null) return 0; // Return the adjusted size return GetRomFSSize(header, cart.MediaUnitSize()); } /// /// Get the size of a partition RomFS /// /// Size of the partition RomFS in bytes, 0 on error public static uint GetRomFSSize(NCCHHeader header, uint mediaUnitSize) => header.RomFSSizeInMediaUnits * mediaUnitSize; #endregion } }