Configuration should not be a Core class

This commit is contained in:
Matt Nadareski
2026-03-22 00:04:51 -04:00
parent 0305d03b00
commit a518f1fd94
6 changed files with 264 additions and 242 deletions

View File

@@ -1,30 +1,71 @@
using System;
using System.IO;
using System.Text;
#if NETFRAMEWORK || NETSTANDARD2_0_OR_GREATER
using SabreTools.Hashing;
using SabreTools.IO.Extensions;
#endif
using SabreTools.Serialization.Wrappers;
namespace NDecrypt.Core
{
public class DSTool : ITool
{
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs _decryptArgs;
#region Properties
public DSTool(DecryptArgs decryptArgs)
/// <summary>
/// Blowfish Table
/// </summary>
public byte[] BlowfishTable
{
_decryptArgs = decryptArgs;
}
get;
set
{
// Ignore missing encryption data
if (value.Length == 0)
return;
// Validate the blowfish table data
byte[]? actual = HashTool.GetByteArrayHashArray(value, HashType.SHA512);
if (actual is null || !actual.EqualsExactly(ExpectedNitroSha512Hash))
return;
// Assign the validated value
field = value;
}
} = [];
#endregion
#region Internal Test Values
/// <summary>
/// Expected hash for NitroEncryptionData
/// </summary>
private static readonly byte[] ExpectedNitroSha512Hash =
[
0x1A, 0xD6, 0x40, 0x21, 0xFC, 0x3D, 0x1A, 0x9A,
0x9B, 0xC0, 0x88, 0x8E, 0x2E, 0x68, 0xDE, 0x4E,
0x8A, 0x60, 0x6B, 0x86, 0x63, 0x22, 0xD2, 0xC7,
0xC6, 0xD7, 0xD6, 0xCE, 0x65, 0xF5, 0xBA, 0xA7,
0xEA, 0x69, 0x63, 0x7E, 0xC9, 0xE4, 0x57, 0x7B,
0x01, 0xFD, 0xCE, 0xC2, 0x26, 0x3B, 0xD9, 0x0D,
0x84, 0x57, 0xC2, 0x00, 0xB8, 0x56, 0x9F, 0xE5,
0x56, 0xDA, 0x8D, 0xDE, 0x84, 0xB8, 0x8E, 0xE4,
];
#endregion
#region Encrypt
/// <inheritdoc/>
public bool EncryptFile(string input, string? output, bool force)
{
// If the blowfish table is not set, do not process
if (BlowfishTable.Length == 0)
{
Console.WriteLine("Error: Nitro encryption data not provided!");
return false;
}
try
{
// If the output is provided, copy the input file
@@ -52,7 +93,7 @@ namespace NDecrypt.Core
}
// Encrypt the secure area
nitro.EncryptSecureArea(_decryptArgs.NitroEncryptionData, force);
nitro.EncryptSecureArea(BlowfishTable, force);
// Write the encrypted secure area
using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
@@ -77,6 +118,13 @@ namespace NDecrypt.Core
/// <inheritdoc/>
public bool DecryptFile(string input, string? output, bool force)
{
// If the blowfish table is not set, do not process
if (BlowfishTable.Length == 0)
{
Console.WriteLine("Error: Nitro encryption data not provided!");
return false;
}
try
{
// If the output is provided, copy the input file
@@ -104,7 +152,7 @@ namespace NDecrypt.Core
}
// Decrypt the secure area
nitro.DecryptSecureArea(_decryptArgs.NitroEncryptionData, force);
nitro.DecryptSecureArea(BlowfishTable, force);
// Write the decrypted secure area
using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);

View File

@@ -22,7 +22,7 @@ namespace NDecrypt.Core
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs _decryptArgs;
private readonly ThreeDSDecryptArgs _decryptArgs;
/// <summary>
/// Indicates if development images are expected
@@ -37,11 +37,9 @@ namespace NDecrypt.Core
/// <param name="masks">BitMasks from the partition or backup header</param>
/// <param name="method">CryptoMethod from the partition or backup header</param>
/// <param name="development">Determine if development keys are used</param>
public PartitionKeys(DecryptArgs args, byte[]? signature, BitMasks masks, CryptoMethod method, bool development)
public PartitionKeys(ThreeDSDecryptArgs args, byte[]? signature, BitMasks masks, CryptoMethod method, bool development)
{
// Validate inputs
if (args.IsReady != true)
throw new InvalidOperationException($"{nameof(args)} must be initialized before use");
if (signature is not null && signature.Length < 16)
throw new ArgumentOutOfRangeException(nameof(signature), $"{nameof(signature)} must be at least 16 bytes");
@@ -51,7 +49,7 @@ namespace NDecrypt.Core
// Set the standard KeyX values
KeyX = new byte[16];
KeyX2C = development ? args.DevKeyX0x2C : args.KeyX0x2C;
KeyX2C = development ? _decryptArgs.DevKeyX0x2C : _decryptArgs.KeyX0x2C;
// Backup headers can't have a KeyY value set
KeyY = new byte[16];
@@ -63,7 +61,7 @@ namespace NDecrypt.Core
NormalKey2C = KeyX2C.RotateLeft(2);
NormalKey2C = NormalKey2C.Xor(KeyY);
NormalKey2C = NormalKey2C.Add(add: args.AESHardwareConstant);
NormalKey2C = NormalKey2C.Add(add: _decryptArgs.AESHardwareConstant);
NormalKey2C = NormalKey2C.RotateLeft(87);
// Special case for zero-key
@@ -84,22 +82,22 @@ namespace NDecrypt.Core
{
case CryptoMethod.Original:
Console.WriteLine("Encryption Method: Key 0x2C");
KeyX = development ? args.DevKeyX0x2C : args.KeyX0x2C;
KeyX = development ? _decryptArgs.DevKeyX0x2C : _decryptArgs.KeyX0x2C;
break;
case CryptoMethod.Seven:
Console.WriteLine("Encryption Method: Key 0x25");
KeyX = development ? args.DevKeyX0x25 : args.KeyX0x25;
KeyX = development ? _decryptArgs.DevKeyX0x25 : _decryptArgs.KeyX0x25;
break;
case CryptoMethod.NineThree:
Console.WriteLine("Encryption Method: Key 0x18");
KeyX = development ? args.DevKeyX0x18 : args.KeyX0x18;
KeyX = development ? _decryptArgs.DevKeyX0x18 : _decryptArgs.KeyX0x18;
break;
case CryptoMethod.NineSix:
Console.WriteLine("Encryption Method: Key 0x1B");
KeyX = development ? args.DevKeyX0x1B : args.KeyX0x1B;
KeyX = development ? _decryptArgs.DevKeyX0x1B : _decryptArgs.KeyX0x1B;
break;
// This should never happen
@@ -111,7 +109,7 @@ namespace NDecrypt.Core
// Set the normal key based on the new KeyX value
NormalKey = KeyX.RotateLeft(2);
NormalKey = NormalKey.Xor(KeyY);
NormalKey = NormalKey.Add(args.AESHardwareConstant);
NormalKey = NormalKey.Add(_decryptArgs.AESHardwareConstant);
NormalKey = NormalKey.RotateLeft(87);
}

View File

@@ -1,97 +1,205 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.IO.Encryption;
using SabreTools.IO.Extensions;
namespace NDecrypt.Core
{
public class DecryptArgs
/// <summary>
/// Nintendo 3DS encryption/decryption values
/// </summary>
public class ThreeDSDecryptArgs
{
#region Common Fields
/// <summary>
/// Represents if all of the keys have been initialized properly
/// </summary>
public bool? IsReady { get; private set; }
#endregion
#region DS-Specific Fields
/// <summary>
/// Blowfish Table
/// </summary>
public byte[] NitroEncryptionData { get; private set; } = [];
#endregion
#region 3DS-Specific Fields
/// <summary>
/// AES Hardware Constant
/// </summary>
public byte[] AESHardwareConstant { get; private set; } = [];
/// TODO: Validate this value on assignment
public byte[] AESHardwareConstant { get; set; } = [];
/// <summary>
/// KeyX 0x18 (New 3DS 9.3)
/// </summary>
public byte[] KeyX0x18 { get; private set; } = [];
public byte[] KeyX0x18
{
get;
set
{
// Ignore missing key data
if (value.Length == 0)
return;
// Validate the key data
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedKeyX0x18))
return;
// Assign the validated value
field = value;
}
} = [];
/// <summary>
/// Dev KeyX 0x18 (New 3DS 9.3)
/// </summary>
public byte[] DevKeyX0x18 { get; private set; } = [];
public byte[] DevKeyX0x18
{
get;
set
{
// Ignore missing key data
if (value.Length == 0)
return;
// Validate the key data
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedDevKeyX0x18))
return;
// Assign the validated value
field = value;
}
} = [];
/// <summary>
/// KeyX 0x1B (New 3DS 9.6)
/// </summary>
public byte[] KeyX0x1B { get; private set; } = [];
public byte[] KeyX0x1B
{
get;
set
{
// Ignore missing key data
if (value.Length == 0)
return;
// Validate the key data
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedKeyX0x1B))
return;
// Assign the validated value
field = value;
}
} = [];
/// <summary>
/// Dev KeyX 0x1B New 3DS 9.6)
/// </summary>
public byte[] DevKeyX0x1B { get; private set; } = [];
public byte[] DevKeyX0x1B
{
get;
set
{
// Ignore missing key data
if (value.Length == 0)
return;
// Validate the key data
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedDevKeyX0x1B))
return;
// Assign the validated value
field = value;
}
} = [];
/// <summary>
/// KeyX 0x25 (> 7.x)
/// </summary>
public byte[] KeyX0x25 { get; private set; } = [];
public byte[] KeyX0x25
{
get;
set
{
// Ignore missing key data
if (value.Length == 0)
return;
// Validate the key data
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedKeyX0x25))
return;
// Assign the validated value
field = value;
}
} = [];
/// <summary>
/// Dev KeyX 0x25 (> 7.x)
/// </summary>
public byte[] DevKeyX0x25 { get; private set; } = [];
public byte[] DevKeyX0x25
{
get;
set
{
// Ignore missing key data
if (value.Length == 0)
return;
// Validate the key data
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedDevKeyX0x25))
return;
// Assign the validated value
field = value;
}
} = [];
/// <summary>
/// KeyX 0x2C (< 6.x)
/// </summary>
public byte[] KeyX0x2C { get; private set; } = [];
public byte[] KeyX0x2C
{
get;
set
{
// Ignore missing key data
if (value.Length == 0)
return;
// Validate the key data
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedKeyX0x2C))
return;
// Assign the validated value
field = value;
}
} = [];
/// <summary>
/// Dev KeyX 0x2C (< 6.x)
/// </summary>
public byte[] DevKeyX0x2C { get; private set; } = [];
public byte[] DevKeyX0x2C
{
get;
set
{
// Ignore missing key data
if (value.Length == 0)
return;
#endregion
// Validate the key data
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedDevKeyX0x2C))
return;
// Assign the validated value
field = value;
}
} = [];
#region Internal Test Values
/// <summary>
/// Expected hash for NitroEncryptionData
/// </summary>
private static readonly byte[] ExpectedNitroSha512Hash =
[
0x1A, 0xD6, 0x40, 0x21, 0xFC, 0x3D, 0x1A, 0x9A,
0x9B, 0xC0, 0x88, 0x8E, 0x2E, 0x68, 0xDE, 0x4E,
0x8A, 0x60, 0x6B, 0x86, 0x63, 0x22, 0xD2, 0xC7,
0xC6, 0xD7, 0xD6, 0xCE, 0x65, 0xF5, 0xBA, 0xA7,
0xEA, 0x69, 0x63, 0x7E, 0xC9, 0xE4, 0x57, 0x7B,
0x01, 0xFD, 0xCE, 0xC2, 0x26, 0x3B, 0xD9, 0x0D,
0x84, 0x57, 0xC2, 0x00, 0xB8, 0x56, 0x9F, 0xE5,
0x56, 0xDA, 0x8D, 0xDE, 0x84, 0xB8, 0x8E, 0xE4,
];
/// <summary>
/// Initial value for key validation tests
/// </summary>
@@ -237,155 +345,5 @@ namespace NDecrypt.Core
];
#endregion
/// <summary>
/// Setup all of the necessary constants
/// </summary>
/// <param name="keyfile">Path to the keyfile</param>
public DecryptArgs(string? config)
{
if (config is null || !File.Exists(config))
{
IsReady = false;
return;
}
// Try to read the configuration file
var configObj = Configuration.Create(config);
if (configObj is null)
{
IsReady = false;
return;
}
// Set the fields from the configuration
NitroEncryptionData = configObj.NitroEncryptionData.FromHexString() ?? [];
AESHardwareConstant = configObj.AESHardwareConstant.FromHexString() ?? [];
KeyX0x18 = configObj.KeyX0x18.FromHexString() ?? [];
KeyX0x1B = configObj.KeyX0x1B.FromHexString() ?? [];
KeyX0x25 = configObj.KeyX0x25.FromHexString() ?? [];
KeyX0x2C = configObj.KeyX0x2C.FromHexString() ?? [];
DevKeyX0x18 = configObj.DevKeyX0x18.FromHexString() ?? [];
DevKeyX0x1B = configObj.DevKeyX0x1B.FromHexString() ?? [];
DevKeyX0x25 = configObj.DevKeyX0x25.FromHexString() ?? [];
DevKeyX0x2C = configObj.DevKeyX0x2C.FromHexString() ?? [];
IsReady = true;
ValidateKeys();
}
/// <summary>
/// Validate that all keys provided are going to be valid
/// </summary>
/// <remarks>Does not know what the keys are, just the result</remarks>
private void ValidateKeys()
{
// NitroEncryptionData
if (NitroEncryptionData.Length > 0)
{
byte[]? actual = HashTool.GetByteArrayHashArray(NitroEncryptionData, HashType.SHA512);
if (actual is null || !actual.EqualsExactly(ExpectedNitroSha512Hash))
{
Console.WriteLine($"NitroEncryptionData invalid value, disabling...");
NitroEncryptionData = [];
}
}
// KeyX0x18
if (KeyX0x18.Length > 0)
{
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedKeyX0x18))
{
Console.WriteLine($"KeyX0x18 invalid value, disabling...");
KeyX0x18 = [];
}
}
// DevKeyX0x18
if (DevKeyX0x18.Length > 0)
{
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedDevKeyX0x18))
{
Console.WriteLine($"DevKeyX0x18 invalid value, disabling...");
DevKeyX0x18 = [];
}
}
// KeyX0x1B
if (KeyX0x1B.Length > 0)
{
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedKeyX0x1B))
{
Console.WriteLine($"KeyX0x1B invalid value, disabling...");
KeyX0x1B = [];
}
}
// DevKeyX0x1B
if (DevKeyX0x1B.Length > 0)
{
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedDevKeyX0x1B))
{
Console.WriteLine($"DevKeyX0x1B invalid value, disabling...");
DevKeyX0x1B = [];
}
}
// KeyX0x25
if (KeyX0x25.Length > 0)
{
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedKeyX0x25))
{
Console.WriteLine($"KeyX0x25 invalid value, disabling...");
KeyX0x25 = [];
}
}
// DevKeyX0x25
if (DevKeyX0x25.Length > 0)
{
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedDevKeyX0x25))
{
Console.WriteLine($"DevKeyX0x25 invalid value, disabling...");
DevKeyX0x25 = [];
}
}
// KeyX0x2C
if (KeyX0x2C.Length > 0)
{
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedKeyX0x2C))
{
Console.WriteLine($"KeyX0x2C invalid value, disabling...");
KeyX0x2C = [];
}
}
// DevKeyX0x2C
if (DevKeyX0x2C.Length > 0)
{
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!actual.EqualsExactly(ExpectedDevKeyX0x2C))
{
Console.WriteLine($"DevKeyX0x2C invalid value, disabling...");
DevKeyX0x2C = [];
}
}
}
}
}

View File

@@ -14,7 +14,7 @@ namespace NDecrypt.Core
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs _decryptArgs;
private readonly ThreeDSDecryptArgs _decryptArgs;
/// <summary>
/// Indicates if development images are expected
@@ -26,7 +26,7 @@ namespace NDecrypt.Core
/// </summary>
private readonly PartitionKeys[] _keysMap = new PartitionKeys[8];
public ThreeDSTool(bool development, DecryptArgs decryptArgs)
public ThreeDSTool(bool development, ThreeDSDecryptArgs decryptArgs)
{
_development = development;
_decryptArgs = decryptArgs;
@@ -37,13 +37,6 @@ namespace NDecrypt.Core
/// <inheritdoc/>
public bool DecryptFile(string input, string? output, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
{
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
try
{
// If the output is provided, copy the input file
@@ -456,13 +449,6 @@ namespace NDecrypt.Core
/// <inheritdoc/>
public bool EncryptFile(string input, string? output, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
{
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
try
{
// If the output is provided, copy the input file

View File

@@ -1,7 +1,7 @@
using System.IO;
using Newtonsoft.Json;
namespace NDecrypt.Core
namespace NDecrypt
{
internal class Configuration
{

View File

@@ -4,6 +4,7 @@ using System.IO;
using NDecrypt.Core;
using SabreTools.CommandLine;
using SabreTools.CommandLine.Inputs;
using SabreTools.IO.Extensions;
namespace NDecrypt.Features
{
@@ -31,7 +32,7 @@ namespace NDecrypt.Features
/// <summary>
/// Mapping of reusable tools
/// </summary>
private readonly Dictionary<FileType, ITool> _tools = [];
private readonly Dictionary<FileType, ITool?> _tools = [];
protected BaseFeature(string name, string[] flags, string description, string? detailed = null)
: base(name, flags, description, detailed)
@@ -80,10 +81,41 @@ namespace NDecrypt.Features
/// </summary>
private void InitializeTools()
{
// Set default values for tools
_tools[FileType.NDS] = null;
_tools[FileType.N3DS] = null;
var decryptArgs = new DecryptArgs(GetString(ConfigName, "config.json"));
_tools[FileType.NDS] = new DSTool(decryptArgs);
_tools[FileType.N3DS] = new ThreeDSTool(GetBoolean(DevelopmentName), decryptArgs);
// Check the configuration path
string? configPath = GetString(ConfigName, "config.json");
if (configPath is null || !File.Exists(configPath))
return;
// Try to read the configuration file
var config = Configuration.Create(configPath);
if (config is null)
return;
// Create the DS tool
_tools[FileType.NDS] = new DSTool
{
BlowfishTable = config.NitroEncryptionData.FromHexString() ?? [],
};
// Create the 3DS tool
bool development = GetBoolean(DevelopmentName);
var decryptArgs = new ThreeDSDecryptArgs
{
AESHardwareConstant = config.AESHardwareConstant.FromHexString() ?? [],
KeyX0x18 = config.KeyX0x18.FromHexString() ?? [],
KeyX0x1B = config.KeyX0x1B.FromHexString() ?? [],
KeyX0x25 = config.KeyX0x25.FromHexString() ?? [],
KeyX0x2C = config.KeyX0x2C.FromHexString() ?? [],
DevKeyX0x18 = config.DevKeyX0x18.FromHexString() ?? [],
DevKeyX0x1B = config.DevKeyX0x1B.FromHexString() ?? [],
DevKeyX0x25 = config.DevKeyX0x25.FromHexString() ?? [],
DevKeyX0x2C = config.DevKeyX0x2C.FromHexString() ?? []
};
_tools[FileType.N3DS] = new ThreeDSTool(development, decryptArgs);
}
/// <summary>