15 Commits
0.3.0 ... 0.3.1

Author SHA1 Message Date
Matt Nadareski
6a094b2dd8 Bump version 2024-11-14 21:40:01 -05:00
Matt Nadareski
0e3d22020b Fix remaining offset and size issues 2024-11-14 21:39:20 -05:00
Matt Nadareski
695f0f44b6 Ensure ExeFS table doesn't finalize 2024-11-14 21:25:38 -05:00
Matt Nadareski
dc4bd15e04 Fix incorrect size being used 2024-11-14 21:17:35 -05:00
Matt Nadareski
ce54eb24e2 Fix more issues 2024-11-14 13:43:07 -05:00
Matt Nadareski
15961e7047 Wording change 2024-11-14 11:56:18 -05:00
Matt Nadareski
edd8ebc048 Update Serialization to 1.7.3 2024-11-14 11:44:41 -05:00
Matt Nadareski
23e9edbf69 Remove null assurances 2024-11-14 02:16:50 -05:00
Matt Nadareski
00ac5e1ca2 Split some steps for readability 2024-11-14 01:56:06 -05:00
Matt Nadareski
c79781a6d7 Keep fixing byte array math 2024-11-14 01:48:14 -05:00
Matt Nadareski
5aa50ee252 Fix IV and addition 2024-11-14 00:26:58 -05:00
Matt Nadareski
3fbaedd7d5 Reduce unnecessary methods 2024-11-13 21:15:44 -05:00
Matt Nadareski
bc31cb0f6a Start fixing issues, remove BigInteger 2024-11-13 21:11:26 -05:00
Matt Nadareski
3ef32748e9 Remove temp code 2024-11-13 14:51:16 -05:00
Matt Nadareski
41293ab7c5 Add byte array helpers 2024-11-13 14:41:32 -05:00
9 changed files with 1251 additions and 1561 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,28 @@
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
public static class CommonOperations
{
#region AES
/// <summary>
/// Create AES cipher and intialize
/// </summary>
/// <param name="key">BigInteger representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <param name="encrypt">True if cipher is created for encryption, false otherwise</param>
/// <returns>Initialized AES cipher</returns>
public static IBufferedCipher CreateAESCipher(BigInteger key, byte[] iv, bool encrypt)
{
return encrypt ? CreateAESEncryptionCipher(key, iv) : CreateAESDecryptionCipher(key, iv);
}
/// <summary>
/// Create AES decryption cipher and intialize
/// </summary>
/// <param name="key">BigInteger representation of 128-bit encryption key</param>
/// <param name="key">Byte array representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <returns>Initialized AES cipher</returns>
public static IBufferedCipher CreateAESDecryptionCipher(BigInteger key, byte[] iv)
public static IBufferedCipher CreateAESDecryptionCipher(byte[] key, byte[] iv)
{
var keyParam = new KeyParameter(TakeSixteen(key));
if (key.Length != 16)
throw new ArgumentOutOfRangeException(nameof(key));
var keyParam = new KeyParameter(key);
var cipher = CipherUtilities.GetCipher("AES/CTR");
cipher.Init(forEncryption: false, new ParametersWithIV(keyParam, iv));
return cipher;
@@ -42,12 +31,15 @@ namespace NDecrypt.Core
/// <summary>
/// Create AES encryption cipher and intialize
/// </summary>
/// <param name="key">BigInteger representation of 128-bit encryption key</param>
/// <param name="key">Byte array representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <returns>Initialized AES cipher</returns>
public static IBufferedCipher CreateAESEncryptionCipher(BigInteger key, byte[] iv)
public static IBufferedCipher CreateAESEncryptionCipher(byte[] key, byte[] iv)
{
var keyParam = new KeyParameter(TakeSixteen(key));
if (key.Length != 16)
throw new ArgumentOutOfRangeException(nameof(key));
var keyParam = new KeyParameter(key);
var cipher = CipherUtilities.GetCipher("AES/CTR");
cipher.Init(forEncryption: true, new ParametersWithIV(keyParam, iv));
return cipher;
@@ -132,31 +124,6 @@ namespace NDecrypt.Core
progress($"{blockCount + 1} / {blockCount + 1} MB... Done!\r\n");
}
/// <summary>
/// Get a 16-byte array representation of a BigInteger
/// </summary>
/// <param name="input">BigInteger value to convert</param>
/// <returns>16-byte array representing the BigInteger</returns>
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
@@ -167,275 +134,120 @@ namespace NDecrypt.Core
/// <param name="input">Byte array to add to</param>
/// <param name="add">Amount to add</param>
/// <returns>Byte array representing the new value</returns>
public static byte[] AddToByteArray(byte[] input, int add)
public static byte[] Add(byte[] input, uint add)
{
int len = input.Length;
Array.Reverse(input);
var bigint = new BigInteger(input);
byte[] addBytes = BitConverter.GetBytes(add);
Array.Reverse(addBytes);
byte[] paddedBytes = new byte[16];
Array.Copy(addBytes, 0, paddedBytes, 12, 4);
return Add(input, paddedBytes);
}
bigint += add;
var arr = bigint.ToByteArray();
Array.Reverse(arr);
/// <summary>
/// Add two numbers represented by byte arrays
/// </summary>
/// <param name="left">Byte array to add to</param>
/// <param name="right">Amount to add</param>
/// <returns>Byte array representing the new value</returns>
public static byte[] Add(byte[] left, byte[] right)
{
int addBytes = Math.Min(left.Length, right.Length);
int outLength = Math.Max(left.Length, right.Length);
if (arr.Length < len)
byte[] output = new byte[outLength];
uint carry = 0;
for (int i = addBytes - 1; i >= 0; i--)
{
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;
uint addValue = (uint)(left[i] + right[i]) + carry;
output[i] = (byte)addValue;
carry = addValue >> 8;
}
return arr;
if (outLength != addBytes && left.Length == outLength)
Array.Copy(left, addBytes, output, addBytes, outLength - addBytes);
else if (outLength != addBytes && right.Length == outLength)
Array.Copy(right, addBytes, output, addBytes, outLength - addBytes);
return output;
}
/// <summary>
/// Perform a rotate left on a BigInteger
/// Perform a rotate left on a byte array
/// </summary>
/// <param name="val">BigInteger value to rotate</param>
/// <param name="val">Byte array value to rotate</param>
/// <param name="r_bits">Number of bits to rotate</param>
/// <param name="max_bits">Maximum number of bits to rotate on</param>
/// <returns>Rotated BigInteger value</returns>
public static BigInteger RotateLeft(BigInteger val, int r_bits, int max_bits)
/// <returns>Rotated byte array value</returns>
public static byte[] RotateLeft(byte[] val, int r_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)));
}
byte[] output = new byte[val.Length];
Array.Copy(val, output, output.Length);
#endregion
// Shift by bytes
while (r_bits >= 8)
{
byte temp = output[0];
for (int i = 0; i < output.Length - 1; i++)
{
output[i] = output[i + 1];
}
#region Offsets
output[output.Length - 1] = temp;
r_bits -= 8;
}
/// <summary>
/// Get the offset of a partition ExeFS
/// </summary>
/// <returns>Offset to the ExeFS of the partition, 0 on error</returns>
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;
// Shift by bits
if (r_bits > 0)
{
byte bitMask = (byte)(8 - r_bits), carry, wrap = 0;
for (int i = 0; i < output.Length; i++)
{
carry = (byte)((255 << bitMask & output[i]) >> bitMask);
// Invalid partition table entry means no size is available
var entry = partitionsTable[index];
if (entry == null)
return 0;
// Make sure the first byte carries to the end
if (i == 0)
wrap = carry;
// Empty partitions array means no size is available
var partitions = cart.Partitions;
if (partitions == null)
return 0;
// Otherwise, move to the last byte
else
output[i - 1] |= carry;
// Invalid partition means no size is available
var header = partitions[index];
if (header == null)
return 0;
// Shift the current bits
output[i] <<= r_bits;
}
// If the offset is 0, return 0
uint exeFsOffsetMU = header.ExeFSOffsetInMediaUnits;
if (exeFsOffsetMU == 0)
return 0;
// Make sure the wrap happens
output[output.Length - 1] |= wrap;
}
// Return the adjusted offset
uint partitionOffsetMU = entry.Offset;
return (partitionOffsetMU + exeFsOffsetMU) * cart.MediaUnitSize();
return output;
}
/// <summary>
/// Get the offset of a partition ExeFS
/// XOR two numbers represented by byte arrays
/// </summary>
/// <returns>Offset to the ExeFS of the partition, 0 on error</returns>
public static uint GetExeFSOffset(NCCHHeader header,
PartitionTableEntry entry,
uint mediaUnitSize)
/// <param name="left">Byte array to XOR to</param>
/// <param name="right">Amount to XOR</param>
/// <returns>Byte array representing the new value</returns>
public static byte[] Xor(byte[] left, byte[] right)
{
// If the offset is 0, return 0
uint exeFsOffsetMU = header.ExeFSOffsetInMediaUnits;
if (exeFsOffsetMU == 0)
return 0;
int xorBytes = Math.Min(left.Length, right.Length);
int outLength = Math.Max(left.Length, right.Length);
// Return the adjusted offset
uint partitionOffsetMU = entry.Offset;
return (partitionOffsetMU + exeFsOffsetMU) * mediaUnitSize;
byte[] output = new byte[outLength];
for (int i = 0; i < xorBytes; i++)
{
output[i] = (byte)(left[i] ^ right[i]);
}
if (outLength != xorBytes && left.Length == outLength)
Array.Copy(left, xorBytes, output, xorBytes, outLength - xorBytes);
else if (outLength != xorBytes && right.Length == outLength)
Array.Copy(right, xorBytes, output, xorBytes, outLength - xorBytes);
return output;
}
/// <summary>
/// Get the offset of a partition
/// </summary>
/// <returns>Offset to the partition, 0 on error</returns>
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();
}
/// <summary>
/// Get the offset of a partition
/// </summary>
/// <returns>Offset to the partition, 0 on error</returns>
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;
}
/// <summary>
/// Get the offset of a partition RomFS
/// </summary>
/// <returns>Offset to the RomFS of the partition, 0 on error</returns>
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();
}
/// <summary>
/// Get the offset of a partition RomFS
/// </summary>
/// <returns>Offset to the RomFS of the partition, 0 on error</returns>
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
/// <summary>
/// Get the size of a partition ExeFS
/// </summary>
/// <returns>Size of the partition ExeFS in bytes, 0 on error</returns>
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());
}
/// <summary>
/// Get the size of a partition ExeFS
/// </summary>
/// <returns>Size of the partition ExeFS in bytes, 0 on error</returns>
public static uint GetExeFSSize(NCCHHeader header, uint mediaUnitSize)
=> header.ExeFSSizeInMediaUnits * mediaUnitSize;
/// <summary>
/// Get the size of a partition extended header
/// </summary>
/// <returns>Size of the partition extended header in bytes, 0 on error</returns>
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);
}
/// <summary>
/// Get the size of a partition extended header
/// </summary>
/// <returns>Size of the partition extended header in bytes, 0 on error</returns>
public static uint GetExtendedHeaderSize(NCCHHeader header)
=> header.ExtendedHeaderSizeInBytes;
/// <summary>
/// Get the size of a partition RomFS
/// </summary>
/// <returns>Size of the partition RomFS in bytes, 0 on error</returns>
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());
}
/// <summary>
/// Get the size of a partition RomFS
/// </summary>
/// <returns>Size of the partition RomFS in bytes, 0 on error</returns>
public static uint GetRomFSSize(NCCHHeader header, uint mediaUnitSize)
=> header.RomFSSizeInMediaUnits * mediaUnitSize;
#endregion
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Numerics;
using SabreTools.IO.Extensions;
using SabreTools.IO.Readers;
@@ -22,29 +21,29 @@ namespace NDecrypt.Core
/// <summary>
/// AES Hardware Constant
/// </summary>
public BigInteger AESHardwareConstant { get; private set; }
public byte[] AESHardwareConstant { get; private set; } = [];
#region Retail Keys
/// <summary>
/// KeyX 0x18 (New 3DS 9.3)
/// </summary>
public BigInteger KeyX0x18 { get; private set; }
public byte[] KeyX0x18 { get; private set; } = [];
/// <summary>
/// KeyX 0x1B (New 3DS 9.6)
/// </summary>
public BigInteger KeyX0x1B { get; private set; }
public byte[] KeyX0x1B { get; private set; } = [];
/// <summary>
/// KeyX 0x25 (> 7.x)
/// </summary>
public BigInteger KeyX0x25 { get; private set; }
public byte[] KeyX0x25 { get; private set; } = [];
/// <summary>
/// KeyX 0x2C (< 6.x)
/// </summary>
public BigInteger KeyX0x2C { get; private set; }
public byte[] KeyX0x2C { get; private set; } = [];
#endregion
@@ -53,22 +52,22 @@ namespace NDecrypt.Core
/// <summary>
/// Dev KeyX 0x18 (New 3DS 9.3)
/// </summary>
public BigInteger DevKeyX0x18 { get; private set; }
public byte[] DevKeyX0x18 { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x1B New 3DS 9.6)
/// </summary>
public BigInteger DevKeyX0x1B { get; private set; }
public byte[] DevKeyX0x1B { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x25 (> 7.x)
/// </summary>
public BigInteger DevKeyX0x25 { get; private set; }
public byte[] DevKeyX0x25 { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x2C (< 6.x)
/// </summary>
public BigInteger DevKeyX0x2C { get; private set; }
public byte[] DevKeyX0x2C { get; private set; } = [];
#endregion
@@ -103,10 +102,6 @@ namespace NDecrypt.Core
try
{
using var reader = new IniReader(keyfile);
// This is required to preserve sign for BigInteger
byte[] signByte = [0x00];
while (reader.ReadNextLine())
{
// Ignore comments in the file
@@ -117,28 +112,25 @@ namespace NDecrypt.Core
var kvp = reader.KeyValuePair!.Value;
byte[] value = StringToByteArray(kvp.Value);
Array.Reverse(value);
byte[] valueWithSign = [.. value, .. signByte];
switch (kvp.Key)
{
// Hardware constant
case "generator":
AESHardwareConstant = new BigInteger(value);
AESHardwareConstant = value;
break;
// Retail Keys
case "slot0x18KeyX":
KeyX0x18 = new BigInteger(valueWithSign);
KeyX0x18 = value;
break;
case "slot0x1BKeyX":
KeyX0x1B = new BigInteger(valueWithSign);
KeyX0x1B = value;
break;
case "slot0x25KeyX":
KeyX0x25 = new BigInteger(valueWithSign);
KeyX0x25 = value;
break;
case "slot0x2CKeyX":
KeyX0x2C = new BigInteger(valueWithSign);
KeyX0x2C = value;
break;
// Currently Unused KeyX
@@ -225,23 +217,29 @@ namespace NDecrypt.Core
{
using Stream reader = File.Open(keyfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// This is required to preserve sign for BigInteger
byte[] signByte = [0x00];
// Hardware constant
AESHardwareConstant = new BigInteger(reader.ReadBytes(16));
AESHardwareConstant = reader.ReadBytes(16);
Array.Reverse(AESHardwareConstant);
// Retail keys
KeyX0x18 = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
KeyX0x1B = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
KeyX0x25 = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
KeyX0x2C = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
KeyX0x18 = reader.ReadBytes(16);
Array.Reverse(KeyX0x18);
KeyX0x1B = reader.ReadBytes(16);
Array.Reverse(KeyX0x1B);
KeyX0x25 = reader.ReadBytes(16);
Array.Reverse(KeyX0x25);
KeyX0x2C = reader.ReadBytes(16);
Array.Reverse(KeyX0x2C);
// Development keys
DevKeyX0x18 = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
DevKeyX0x1B = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
DevKeyX0x25 = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
DevKeyX0x2C = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
DevKeyX0x18 = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x18);
DevKeyX0x1B = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x1B);
DevKeyX0x25 = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x25);
DevKeyX0x2C = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x2C);
}
catch
{

View File

@@ -1,152 +0,0 @@
using System;
using SabreTools.Models.N3DS;
namespace NDecrypt.Core
{
internal static class Extensions
{
#region Constants
// Setup Keys and IVs
public static byte[] PlainCounter = [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
public static byte[] ExefsCounter = [0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
public static byte[] RomfsCounter = [0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
#endregion
#region ExeFSFileHeader
/// <summary>
/// Determines if a file header represents a CODE block
/// </summary>
public static bool IsCodeBinary(this ExeFSFileHeader? header)
{
if (header == null)
return false;
return header.FileName == ".code\0\0\0";
}
#endregion
#region NCCHHeader
/// <summary>
/// Get the initial value for the plain counter
/// </summary>
public static byte[] PlainIV(this Cart cart, int partitionIndex)
{
if (cart.Partitions == null)
return [];
if (partitionIndex < 0 || partitionIndex >= cart.Partitions.Length)
return [];
var header = cart.Partitions[partitionIndex];
return PlainIV(header);
}
/// <summary>
/// Get the initial value for the plain counter
/// </summary>
public static byte[] PlainIV(this NCCHHeader? header)
{
if (header == null)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
return [.. partitionIdBytes, .. PlainCounter];
}
/// <summary>
/// Get the initial value for the ExeFS counter
/// </summary>
public static byte[] ExeFSIV(this Cart cart, int partitionIndex)
{
if (cart.Partitions == null)
return [];
if (partitionIndex < 0 || partitionIndex >= cart.Partitions.Length)
return [];
var header = cart.Partitions[partitionIndex];
return ExeFSIV(header);
}
/// <summary>
/// Get the initial value for the ExeFS counter
/// </summary>
public static byte[] ExeFSIV(this NCCHHeader? header)
{
if (header == null)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
return [.. partitionIdBytes, .. ExefsCounter];
}
/// <summary>
/// Get the initial value for the RomFS counter
/// </summary>
public static byte[] RomFSIV(this Cart cart, int partitionIndex)
{
if (cart.Partitions == null)
return [];
if (partitionIndex < 0 || partitionIndex >= cart.Partitions.Length)
return [];
var header = cart.Partitions[partitionIndex];
return RomFSIV(header);
}
/// <summary>
/// Get the initial value for the RomFS counter
/// </summary>
public static byte[] RomFSIV(this NCCHHeader? header)
{
if (header == null)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
return [.. partitionIdBytes, .. RomfsCounter];
}
#endregion
#region NCCHHeaderFlags
/// <summary>
/// Get if the NoCrypto bit is set
/// </summary>
public static bool PossblyDecrypted(this NCCHHeaderFlags flags)
{
if (flags == null)
return false;
return flags.BitMasks.HasFlag(BitMasks.NoCrypto);
}
#endregion
#region NCSDHeader
//// <summary>
/// Media Unit Size i.e. u32 MediaUnitSize = 0x200*2^flags[6];
/// </summary>
public static uint MediaUnitSize(this Cart cart)
{
return cart.Header.MediaUnitSize();
}
/// <summary>
/// Media Unit Size i.e. u32 MediaUnitSize = 0x200*2^flags[6];
/// </summary>
public static uint MediaUnitSize(this NCSDHeader? header)
{
if (header?.PartitionFlags == null)
return default;
return (uint)(0x200 * Math.Pow(2, header.PartitionFlags[(int)NCSDFlags.MediaUnitSize]));
}
#endregion
}
}

View File

@@ -9,7 +9,7 @@
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.3.0</VersionPrefix>
<VersionPrefix>0.3.1</VersionPrefix>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -46,8 +46,8 @@
<ItemGroup>
<PackageReference Include="SabreTools.Hashing" Version="1.4.0" />
<PackageReference Include="SabreTools.IO" Version="1.5.0" />
<PackageReference Include="SabreTools.Models" Version="1.5.0" />
<PackageReference Include="SabreTools.Serialization" Version="1.7.0" />
<PackageReference Include="SabreTools.Models" Version="1.5.1" />
<PackageReference Include="SabreTools.Serialization" Version="1.7.4" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,4 @@
using System;
using System.Numerics;
using Org.BouncyCastle.Crypto;
using SabreTools.Models.N3DS;
using static NDecrypt.Core.CommonOperations;
@@ -11,15 +10,15 @@ namespace NDecrypt.Core
/// </summary>
public class PartitionKeys
{
public BigInteger KeyX { get; private set; }
public byte[] KeyX { get; private set; }
public BigInteger KeyX2C { get; private set; }
public byte[] KeyX2C { get; }
public BigInteger KeyY { get; private set; }
public byte[] KeyY { get; }
public BigInteger NormalKey { get; private set; }
public byte[] NormalKey { get; private set; }
public BigInteger NormalKey2C { get; private set; }
public byte[] NormalKey2C { get; }
/// <summary>
/// Decryption args to use while processing
@@ -52,32 +51,28 @@ namespace NDecrypt.Core
_development = development;
// Set the standard KeyX values
KeyX = 0;
KeyX = new byte[16];
KeyX2C = development ? args.DevKeyX0x2C : args.KeyX0x2C;
// Backup headers can't have a KeyY value set
KeyY = new byte[16];
if (signature != null)
{
byte[] signature16 = new byte[16];
Array.Copy(signature, signature16, 16);
Array.Reverse(signature16);
KeyY = new BigInteger(signature16);
}
else
{
KeyY = new BigInteger(0);
}
Array.Copy(signature, KeyY, 16);
// Set the standard normal key values
NormalKey = 0x00;
NormalKey2C = RotateLeft((RotateLeft(KeyX2C, 2, 128) ^ KeyY) + args.AESHardwareConstant, 87, 128);
NormalKey = new byte[16];
NormalKey2C = RotateLeft(KeyX2C, 2);
NormalKey2C = Xor(NormalKey2C, KeyY);
NormalKey2C = Add(NormalKey2C, args.AESHardwareConstant);
NormalKey2C = RotateLeft(NormalKey2C, 87);
// Special case for zero-key
if (masks.HasFlag(BitMasks.FixedCryptoKey))
{
Console.WriteLine("Encryption Method: Zero Key");
NormalKey = 0x00;
NormalKey2C = 0x00;
NormalKey = new byte[16];
NormalKey2C = new byte[16];
return;
}
@@ -106,7 +101,10 @@ namespace NDecrypt.Core
}
// Set the normal key based on the new KeyX value
NormalKey = RotateLeft((RotateLeft(KeyX, 2, 128) ^ KeyY) + args.AESHardwareConstant, 87, 128);
NormalKey = RotateLeft(KeyX, 2);
NormalKey = Xor(NormalKey, KeyY);
NormalKey = Add(NormalKey, args.AESHardwareConstant);
NormalKey = RotateLeft(NormalKey, 87);
}
/// <summary>
@@ -117,13 +115,17 @@ namespace NDecrypt.Core
// NormalKey has a constant value for zero-key
if (masks.HasFlag(BitMasks.FixedCryptoKey))
{
NormalKey = 0x00;
NormalKey = new byte[16];
return;
}
// Encrypting RomFS for partitions 1 and up always use Key0x2C
KeyX = _development ? _decryptArgs.DevKeyX0x2C : _decryptArgs.KeyX0x2C;
NormalKey = RotateLeft((RotateLeft(KeyX, 2, 128) ^ KeyY) + _decryptArgs.AESHardwareConstant, 87, 128);
NormalKey = RotateLeft(KeyX, 2);
NormalKey = Xor(NormalKey, KeyY);
NormalKey = Add(NormalKey, _decryptArgs.AESHardwareConstant);
NormalKey = RotateLeft(NormalKey, 87);
}
}
}

View File

@@ -2,8 +2,9 @@
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.N3DS;
using SabreTools.Serialization.Wrappers;
using static NDecrypt.Core.CommonOperations;
using N3DSDeserializer = SabreTools.Serialization.Deserializers.N3DS;
using static SabreTools.Models.N3DS.Constants;
namespace NDecrypt.Core
{
@@ -47,8 +48,8 @@ namespace NDecrypt.Core
using var output = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DSDeserializer.DeserializeStream(input);
if (cart?.Header == null || cart?.CardInfoHeader?.InitialData?.BackupHeader == null)
var cart = N3DS.Create(input);
if (cart?.Model == null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
@@ -83,8 +84,8 @@ namespace NDecrypt.Core
using var output = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DSDeserializer.DeserializeStream(input);
if (cart?.Header == null || cart?.CardInfoHeader?.InitialData?.BackupHeader == null)
var cart = N3DS.Create(input);
if (cart?.Model == null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
@@ -111,10 +112,10 @@ namespace NDecrypt.Core
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptAllPartitions(Cart cart, bool force, Stream input, Stream output)
private void DecryptAllPartitions(N3DS cart, bool force, Stream input, Stream output)
{
// Check the partitions table
if (cart.Header?.PartitionsTable == null || cart.Partitions == null)
if (cart.PartitionsTable == null || cart.Partitions == null)
{
Console.WriteLine("Invalid partitions table!");
return;
@@ -123,13 +124,21 @@ namespace NDecrypt.Core
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
// Check the partition exists
if (cart.Partitions[p] == null)
var partition = cart.Partitions[p];
if (partition == null || partition.MagicID != NCCHMagicNumber)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Check the partition has data
var partitionEntry = cart.PartitionsTable[p];
if (partitionEntry == 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, input, output);
@@ -139,7 +148,7 @@ namespace NDecrypt.Core
/// <summary>
/// Determine if the current partition should be decrypted
/// </summary>s
private static bool ShouldDecryptPartition(Cart cart, int index, bool force)
private static bool ShouldDecryptPartition(N3DS cart, int index, bool force)
{
// If we're forcing the operation, tell the user
if (force)
@@ -148,7 +157,7 @@ namespace NDecrypt.Core
return true;
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (cart.Partitions![index]!.Flags!.PossblyDecrypted())
else if (cart.PossiblyDecrypted(index))
{
Console.WriteLine($"Partition {index}: Already Decrypted?...");
return false;
@@ -165,7 +174,7 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptPartition(Cart cart, int index, Stream input, Stream output)
private void DecryptPartition(N3DS cart, int index, Stream input, Stream output)
{
// Determine the keys needed for this partition
SetDecryptionKeys(cart, index);
@@ -184,19 +193,17 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
private void SetDecryptionKeys(Cart cart, int index)
private void SetDecryptionKeys(N3DS cart, int index)
{
// Get the partition
var partition = cart.Partitions?[index];
if (partition == null)
if (partition?.Flags == null)
return;
// Get partition-specific values
byte[]? rsaSignature = partition.RSA2048Signature;
// Set the header to use based on mode
BitMasks masks = partition.Flags!.BitMasks;
CryptoMethod method = partition.Flags!.CryptoMethod;
BitMasks masks = cart.GetBitMasks(index);
CryptoMethod method = cart.GetCryptoMethod(index);
// Get the partition keys
KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
@@ -209,20 +216,20 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExtendedHeader(Cart cart, int index, Stream input, Stream output)
private bool DecryptExtendedHeader(N3DS cart, int index, Stream input, Stream output)
{
// Get required offsets
uint partitionOffset = GetPartitionOffset(cart, index);
if (partitionOffset == 0)
uint partitionOffset = cart.GetPartitionOffset(index);
if (partitionOffset == 0 || partitionOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
Console.WriteLine($"Partition {index} No Data... Skipping...");
return false;
}
uint extHeaderSize = GetExtendedHeaderSize(cart, index);
uint extHeaderSize = cart.GetExtendedHeaderSize(index);
if (extHeaderSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Extended Header... Skipping...");
Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
return false;
}
@@ -230,7 +237,7 @@ namespace NDecrypt.Core
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Decrypting: ExHeader");
Console.WriteLine($"Partition {index}: Decrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, cart.PlainIV(index));
@@ -253,17 +260,17 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExeFS(Cart cart, int index, Stream input, Stream output)
private bool DecryptExeFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the ExeFS
uint exeFsOffset = GetExeFSOffset(cart, index);
if (exeFsOffset == 0)
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint exeFsSize = GetExeFSSize(cart, index);
uint exeFsSize = cart.GetExeFSSize(index);
if (exeFsSize == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
@@ -274,24 +281,28 @@ namespace NDecrypt.Core
DecryptExeFSFilenameTable(cart, index, input, output);
// For all but the original crypto method, process each of the files in the table
if (cart.Partitions![index]!.Flags!.CryptoMethod != CryptoMethod.Original)
if (cart.GetCryptoMethod(index) != CryptoMethod.Original)
DecryptExeFSFileEntries(cart, index, input, output);
// Get the ExeFS files offset
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Seek to the ExeFS
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
input.Seek(exeFsFilesOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
int ctroffsetE = (int)(cart.MediaUnitSize() / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(cart.ExeFSIV(index), ctroffsetE);
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = Add(cart.ExeFSIV(index), ctroffsetE);
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the decryption
exeFsSize -= cart.MediaUnitSize;
PerformAESOperation(exeFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting: {s}"));
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
return true;
}
@@ -303,11 +314,11 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFilenameTable(Cart cart, int index, Stream input, Stream output)
private void DecryptExeFSFilenameTable(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsOffset = GetExeFSOffset(cart, index);
if (exeFsOffset == 0)
uint exeFsOffset = cart.GetExeFSOffset(index);
if (exeFsOffset == 0 || exeFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
@@ -317,13 +328,15 @@ namespace NDecrypt.Core
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Decrypting: ExeFS Filename Table");
Console.WriteLine($"Partition {index} ExeFS: Decrypting - ExeFS Filename Table");
// Create the ExeFS AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
PerformAESOperation(cart.MediaUnitSize(), cipher, input, output, null);
byte[] readBytes = input.ReadBytes((int)cart.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.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
@@ -339,37 +352,41 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFileEntries(Cart cart, int index, Stream input, Stream output)
private void DecryptExeFSFileEntries(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsHeaderOffset = GetExeFSOffset(cart, index);
if (exeFsHeaderOffset == 0)
if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Get to the start of the files
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize();
input.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input);
// If the header failed to read, log and return
if (exefsHeader == null)
// Get the ExeFS header
var exeFsHeader = cart.ExeFSHeaders[index];
if (exeFsHeader?.FileHeaders == null)
{
Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping...");
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
}
foreach (var fileHeader in exefsHeader.FileHeaders!)
// Get the ExeFS offset
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Loop through and process all headers
for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
{
// Only decrypt a file if it's a code binary
if (fileHeader == null || !fileHeader.IsCodeBinary())
// Only attempt to process code binary files
if (!cart.IsCodeBinary(index, i))
continue;
// Get the file header
var fileHeader = exeFsHeader.FileHeaders[i];
if (fileHeader == null)
continue;
// Create the ExeFS AES ciphers for this partition
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize()) / 0x10;
byte[] exefsIVWithOffsetForHeader = AddToByteArray(cart.ExeFSIV(index), (int)ctroffset);
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize) / 0x10;
byte[] exefsIVWithOffsetForHeader = Add(cart.ExeFSIV(index), ctroffset);
var firstCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
@@ -378,13 +395,12 @@ namespace NDecrypt.Core
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
// Setup and perform the encryption
uint exeFsSize = GetExeFSSize(cart, index);
PerformAESOperation(exeFsSize,
PerformAESOperation(fileHeader.FileSize,
firstCipher,
secondCipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting: {fileHeader.FileName}...{s}"));
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
}
}
@@ -395,17 +411,17 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptRomFS(Cart cart, int index, Stream input, Stream output)
private bool DecryptRomFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the RomFS
uint romFsOffset = GetRomFSOffset(cart, index);
if (romFsOffset == 0)
uint romFsOffset = cart.GetRomFSOffset(index);
if (romFsOffset == 0 || romFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
uint romFsSize = GetRomFSSize(cart, index);
uint romFsSize = cart.GetRomFSSize(index);
if (romFsSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
@@ -424,7 +440,7 @@ namespace NDecrypt.Core
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Decrypting: {s}"));
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
return true;
}
@@ -435,10 +451,10 @@ namespace NDecrypt.Core
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="output">Stream representing the output</param>
private static void UpdateDecryptCryptoAndMasks(Cart cart, int index, Stream output)
private static void UpdateDecryptCryptoAndMasks(N3DS cart, int index, Stream output)
{
// Get required offsets
uint partitionOffset = GetPartitionOffset(cart, index);
uint partitionOffset = cart.GetPartitionOffset(index);
// Seek to the CryptoMethod location
output.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
@@ -451,7 +467,7 @@ namespace NDecrypt.Core
output.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
// Write the new BitMasks flag
BitMasks flag = cart.Partitions![index]!.Flags!.BitMasks;
BitMasks flag = cart.GetBitMasks(index);
flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag |= BitMasks.NoCrypto;
output.Write((byte)flag);
@@ -469,10 +485,10 @@ namespace NDecrypt.Core
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptAllPartitions(Cart cart, bool force, Stream input, Stream output)
private void EncryptAllPartitions(N3DS cart, bool force, Stream input, Stream output)
{
// Check the partitions table
if (cart.Header?.PartitionsTable == null || cart.Partitions == null)
if (cart.PartitionsTable == null || cart.Partitions == null)
{
Console.WriteLine("Invalid partitions table!");
return;
@@ -482,12 +498,21 @@ namespace NDecrypt.Core
for (int p = 0; p < 8; p++)
{
// Check the partition exists
if (cart.Partitions[p] == null)
var partition = cart.Partitions[p];
if (partition == null || partition.MagicID != NCCHMagicNumber)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Check the partition has data
var partitionEntry = cart.PartitionsTable[p];
if (partitionEntry == 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, input, output);
@@ -497,7 +522,7 @@ namespace NDecrypt.Core
/// <summary>
/// Determine if the current partition should be encrypted
/// </summary>
private static bool ShouldEncryptPartition(Cart cart, int index, bool force)
private static bool ShouldEncryptPartition(N3DS cart, int index, bool force)
{
// If we're forcing the operation, tell the user
if (force)
@@ -506,7 +531,7 @@ namespace NDecrypt.Core
return true;
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (!cart.Partitions![index]!.Flags!.PossblyDecrypted())
else if (!cart.PossiblyDecrypted(index))
{
Console.WriteLine($"Partition {index}: Already Encrypted?...");
return false;
@@ -523,7 +548,7 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptPartition(Cart cart, int index, Stream input, Stream output)
private void EncryptPartition(N3DS cart, int index, Stream input, Stream output)
{
// Determine the keys needed for this partition
SetEncryptionKeys(cart, index);
@@ -542,19 +567,21 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
private void SetEncryptionKeys(Cart cart, int index)
private void SetEncryptionKeys(N3DS cart, int index)
{
// Get the partition
var partition = cart.Partitions?[index];
if (partition == null)
return;
// Get the backup header
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == 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;
BitMasks masks = backupHeader.Flags.BitMasks;
CryptoMethod method = backupHeader.Flags.CryptoMethod;
// Get the partition keys
@@ -568,20 +595,20 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExtendedHeader(Cart cart, int index, Stream input, Stream output)
private bool EncryptExtendedHeader(N3DS cart, int index, Stream input, Stream output)
{
// Get required offsets
uint partitionOffset = GetPartitionOffset(cart, index);
if (partitionOffset == 0)
uint partitionOffset = cart.GetPartitionOffset(index);
if (partitionOffset == 0 || partitionOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
Console.WriteLine($"Partition {index} No Data... Skipping...");
return false;
}
uint extHeaderSize = GetExtendedHeaderSize(cart, index);
uint extHeaderSize = cart.GetExtendedHeaderSize(index);
if (extHeaderSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Extended Header... Skipping...");
Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
return false;
}
@@ -589,7 +616,7 @@ namespace NDecrypt.Core
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Encrypting: ExHeader");
Console.WriteLine($"Partition {index}: Encrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, cart.PlainIV(index));
@@ -612,46 +639,50 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExeFS(Cart cart, int index, Stream input, Stream output)
private bool EncryptExeFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the ExeFS
uint exeFsOffset = GetExeFSOffset(cart, index);
if (exeFsOffset == 0)
if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint exeFsSize = GetExeFSSize(cart, index);
if (exeFsSize == 0)
// Get the ExeFS header
var exefsHeader = cart.ExeFSHeaders[index];
if (exefsHeader == null)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
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.CardInfoHeader!.InitialData!.BackupHeader;
var backupHeader = cart.BackupHeader;
if (backupHeader!.Flags!.CryptoMethod != CryptoMethod.Original)
EncryptExeFSFileEntries(cart, index, input, output);
// Encrypt the filename table
EncryptExeFSFilenameTable(cart, index, input, output);
// Get the ExeFS files offset
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Seek to the ExeFS
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
input.Seek(exeFsFilesOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
int ctroffsetE = (int)(cart.MediaUnitSize() / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(cart.ExeFSIV(index), ctroffsetE);
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = Add(cart.ExeFSIV(index), ctroffsetE);
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the encryption
uint exeFsSize = cart.GetExeFSSize(index) - cart.MediaUnitSize;
PerformAESOperation(exeFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting: {s}"));
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
return true;
}
@@ -663,11 +694,11 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFilenameTable(Cart cart, int index, Stream input, Stream output)
private void EncryptExeFSFilenameTable(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsOffset = GetExeFSOffset(cart, index);
if (exeFsOffset == 0)
uint exeFsOffset = cart.GetExeFSOffset(index);
if (exeFsOffset == 0 || exeFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
@@ -677,13 +708,15 @@ namespace NDecrypt.Core
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Encrypting: ExeFS Filename Table");
Console.WriteLine($"Partition {index} ExeFS: Encrypting - ExeFS Filename Table");
// Create the ExeFS AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
PerformAESOperation(cart.MediaUnitSize(), cipher, input, output, null);
byte[] readBytes = input.ReadBytes((int)cart.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.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
@@ -699,37 +732,42 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFileEntries(Cart cart, int index, Stream input, Stream output)
private void EncryptExeFSFileEntries(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsHeaderOffset = GetExeFSOffset(cart, index);
if (exeFsHeaderOffset == 0)
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Get to the start of the files
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize();
input.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input);
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// If the header failed to read, log and return
if (exefsHeader == null)
var exeFsHeader = cart.ExeFSHeaders?[index];
if (exeFsHeader?.FileHeaders == null)
{
Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping...");
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
}
foreach (var fileHeader in exefsHeader.FileHeaders!)
// Loop through and process all headers
for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
{
// Only decrypt a file if it's a code binary
if (fileHeader == null || !fileHeader.IsCodeBinary())
// Only attempt to process code binary files
if (!cart.IsCodeBinary(index, i))
continue;
// Get the file header
var fileHeader = exeFsHeader.FileHeaders[i];
if (fileHeader == null)
continue;
// Create the ExeFS AES ciphers for this partition
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize()) / 0x10;
byte[] exefsIVWithOffsetForHeader = AddToByteArray(cart.ExeFSIV(index), (int)ctroffset);
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize) / 0x10;
byte[] exefsIVWithOffsetForHeader = Add(cart.ExeFSIV(index), ctroffset);
var firstCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
@@ -738,13 +776,12 @@ namespace NDecrypt.Core
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
// Setup and perform the encryption
uint exeFsSize = GetExeFSSize(cart, index);
PerformAESOperation(exeFsSize,
PerformAESOperation(fileHeader.FileSize,
firstCipher,
secondCipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting: {fileHeader.FileName}...{s}"));
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
}
}
@@ -755,17 +792,17 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptRomFS(Cart cart, int index, Stream input, Stream output)
private bool EncryptRomFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the RomFS
uint romFsOffset = GetRomFSOffset(cart, index);
if (romFsOffset == 0)
uint romFsOffset = cart.GetRomFSOffset(index);
if (romFsOffset == 0 || romFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
uint romFsSize = GetRomFSSize(cart, index);
uint romFsSize = cart.GetRomFSSize(index);
if (romFsSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
@@ -779,7 +816,7 @@ namespace NDecrypt.Core
// Force setting encryption keys for partitions 1 and above
if (index > 0)
{
var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader;
var backupHeader = cart.BackupHeader;
KeysMap[index].SetRomFSValues(backupHeader!.Flags!.BitMasks);
}
@@ -791,7 +828,7 @@ namespace NDecrypt.Core
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Encrypting: {s}"));
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
return true;
}
@@ -802,13 +839,15 @@ namespace NDecrypt.Core
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="output">Stream representing the output</param>
private static void UpdateEncryptCryptoAndMasks(Cart cart, int index, Stream output)
private static void UpdateEncryptCryptoAndMasks(N3DS cart, int index, Stream output)
{
// Get required offsets
uint partitionOffset = GetPartitionOffset(cart, index);
uint partitionOffset = cart.GetPartitionOffset(index);
// Get the backup header
var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader;
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == null)
return;
// Seek to the CryptoMethod location
output.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
@@ -816,7 +855,7 @@ namespace NDecrypt.Core
// 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;
byte cryptoMethod = index > 0 ? (byte)CryptoMethod.Original : (byte)backupHeader.Flags.CryptoMethod;
output.Write(cryptoMethod);
output.Flush();
@@ -824,9 +863,9 @@ namespace NDecrypt.Core
output.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
// Write the new BitMasks flag
BitMasks flag = cart.Partitions![index]!.Flags!.BitMasks;
BitMasks flag = cart.GetBitMasks(index);
flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF;
flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupHeader!.Flags!.BitMasks;
flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupHeader.Flags.BitMasks;
output.Write((byte)flag);
output.Flush();
}

View File

@@ -10,7 +10,7 @@
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.3.0</VersionPrefix>
<VersionPrefix>0.3.1</VersionPrefix>
<!-- Package Properties -->
<Title>NDecrypt</Title>

View File

@@ -98,7 +98,7 @@ namespace NDecrypt
// Create reusable tools
_tools[FileType.NDS] = new DSTool();
_tools[FileType.N3DS] = new ThreeDSTool(development, decryptArgs);
_tools[FileType.N3DSCIA] = new CIATool(development, decryptArgs);
//_tools[FileType.N3DSCIA] = new CIATool(development, decryptArgs);
for (int i = start; i < args.Length; i++)
{
@@ -225,7 +225,7 @@ More than one path can be specified at a time.");
FileType.NDSi => _tools[FileType.NDS],
FileType.iQueDS => _tools[FileType.NDS],
FileType.N3DS => _tools[FileType.N3DS],
FileType.N3DSCIA => _tools[FileType.N3DSCIA],
//FileType.N3DSCIA => _tools[FileType.N3DSCIA],
_ => null,
};
}
@@ -262,11 +262,11 @@ More than one path can be specified at a time.");
Console.WriteLine("File recognized as Nintendo 3DS");
return FileType.N3DS;
}
else if (filename.EndsWith(".cia", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as Nintendo 3DS CIA [CAUTION: NOT WORKING CURRENTLY]");
return FileType.N3DSCIA;
}
// else if (filename.EndsWith(".cia", StringComparison.OrdinalIgnoreCase))
// {
// Console.WriteLine("File recognized as Nintendo 3DS CIA [CAUTION: NOT WORKING CURRENTLY]");
// return FileType.N3DSCIA;
// }
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds");
return FileType.NULL;