22 Commits

Author SHA1 Message Date
Matt Nadareski
7ec0517866 Add editorconfig, fix issues 2026-01-25 17:20:06 -05:00
Matt Nadareski
d94b83aca6 Add default config path (fixes #25) 2025-12-15 15:52:58 -05:00
Matt Nadareski
e63792070a Update Serialization to 2.2.1 2025-11-25 07:53:34 -05:00
Matt Nadareski
2bc04c9e15 Bump version 2025-11-24 10:16:24 -05:00
Matt Nadareski
632b8d8c53 Add support for .NET 10 2025-11-24 10:07:12 -05:00
Matt Nadareski
eada29c89b Update packages 2025-11-06 08:19:40 -05:00
Matt Nadareski
818ca1ea03 Update rolling tag 2025-10-26 20:23:36 -04:00
Matt Nadareski
eceb4ba22e Update packages 2025-10-07 12:56:23 -04:00
Matt Nadareski
8627455ed7 Maintain consistent comments 2025-10-06 09:01:09 -04:00
Matt Nadareski
a7cc4ba4ed Clean up after the previous commit 2025-10-06 08:59:20 -04:00
Matt Nadareski
07676f4dcc Use CommandLine library for executable 2025-10-06 08:57:26 -04:00
Matt Nadareski
1a69113af7 Update Serialization to 2.0.1 2025-10-05 17:16:12 -04:00
Matt Nadareski
92adfa17df Add another note 2025-09-30 19:34:42 -04:00
Matt Nadareski
c48ac6e4cc Clean up the usings 2025-09-30 18:20:01 -04:00
Matt Nadareski
4e778bc837 Sync back fixes from IO 2025-09-30 18:09:17 -04:00
Matt Nadareski
ccd04cb15e Remove an unneeded BouncyCastle use 2025-09-30 17:05:50 -04:00
Matt Nadareski
97c17bb9dc Require exact versions for build 2025-09-30 11:23:44 -04:00
Matt Nadareski
2447007900 Update Serialization to 2.0.0 2025-09-29 07:47:32 -04:00
Matt Nadareski
f79a5fa246 Update packages 2025-09-24 11:21:54 -04:00
Matt Nadareski
16aeb6cafe Update Serialization to 1.9.5 2025-09-21 12:51:30 -04:00
Matt Nadareski
c4d812c426 Update Serialization to 1.9.4 2025-09-21 09:45:41 -04:00
Matt Nadareski
383572d5b5 Update packages 2025-09-20 22:45:45 -04:00
23 changed files with 779 additions and 904 deletions

167
.editorconfig Normal file
View File

@@ -0,0 +1,167 @@
# top-most EditorConfig file
root = true
# C# files
[*.cs]
# Indentation and spacing
charset = utf-8
indent_size = 4
indent_style = space
tab_width = 4
trim_trailing_whitespace = true
# New line preferences
end_of_line = lf
insert_final_newline = true
max_line_length = unset
# using directive preferences
csharp_using_directive_placement = outside_namespace
dotnet_diagnostic.IDE0005.severity = error
# Code-block preferences
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_top_level_statements = false
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_inlined_variable_declaration = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
dotnet_diagnostic.IDE0001.severity = warning
dotnet_diagnostic.IDE0002.severity = warning
dotnet_diagnostic.IDE0004.severity = warning
dotnet_diagnostic.IDE0010.severity = error
dotnet_diagnostic.IDE0051.severity = warning
dotnet_diagnostic.IDE0052.severity = warning
dotnet_diagnostic.IDE0072.severity = warning
dotnet_diagnostic.IDE0080.severity = warning
dotnet_diagnostic.IDE0100.severity = error
dotnet_diagnostic.IDE0110.severity = error
dotnet_diagnostic.IDE0120.severity = warning
dotnet_diagnostic.IDE0121.severity = warning
dotnet_diagnostic.IDE0240.severity = error
dotnet_diagnostic.IDE0241.severity = error
dotnet_style_coalesce_expression = true
dotnet_style_namespace_match_folder = false
dotnet_style_null_propagation = true
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_compound_assignment = true
csharp_style_prefer_simple_property_accessors = true
dotnet_style_prefer_simplified_interpolation = true
dotnet_style_prefer_simplified_boolean_expressions = true
csharp_style_prefer_unbound_generic_type_in_nameof = true
# Field preferences
dotnet_diagnostic.IDE0044.severity = warning
dotnet_style_readonly_field = true
# Language keyword vs. framework types preferences
dotnet_diagnostic.IDE0049.severity = error
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_style_prefer_readonly_struct = true
dotnet_diagnostic.IDE0036.severity = warning
dotnet_diagnostic.IDE0040.severity = error
dotnet_diagnostic.IDE0380.severity = error
dotnet_style_require_accessibility_modifiers = always
# New-line preferences
dotnet_diagnostic.IDE2000.severity = warning
dotnet_diagnostic.IDE2002.severity = warning
dotnet_diagnostic.IDE2003.severity = warning
dotnet_diagnostic.IDE2004.severity = warning
dotnet_diagnostic.IDE2005.severity = warning
dotnet_diagnostic.IDE2006.severity = warning
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
dotnet_style_allow_multiple_blank_lines_experimental = false
dotnet_style_allow_statement_immediately_after_block_experimental = false
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
dotnet_diagnostic.IDE0280.severity = error
# Parentheses preferences
dotnet_diagnostic.IDE0047.severity = warning
dotnet_diagnostic.IDE0048.severity = warning
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = always_for_clarity
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Pattern-matching preferences
dotnet_diagnostic.IDE0019.severity = warning
dotnet_diagnostic.IDE0020.severity = warning
dotnet_diagnostic.IDE0038.severity = warning
dotnet_diagnostic.IDE0066.severity = none
dotnet_diagnostic.IDE0083.severity = warning
dotnet_diagnostic.IDE0260.severity = warning
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# var preferences
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = true
# .NET formatting options
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
# C# formatting options
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = false
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false

View File

@@ -1,40 +1,48 @@
name: Build and Test
on:
push:
branches: [ "master" ]
push:
branches: ["master"]
jobs:
build:
runs-on: ubuntu-latest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Run tests
run: dotnet test
- name: Run publish script
run: ./publish-nix.sh -d
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: "*.nupkg,*.snupkg,*.zip"
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Run tests
run: dotnet test
- name: Run publish script
run: ./publish-nix.sh -d
- name: Update rolling tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -f rolling
git push origin :refs/tags/rolling || true
git push origin rolling --force
- name: Upload to rolling
uses: ncipollo/release-action@v1.20.0
with:
allowUpdates: True
artifacts: "*.nupkg,*.snupkg,*.zip"
body: "Last built commit: ${{ github.sha }}"
name: "Rolling Release"
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True

View File

@@ -3,18 +3,18 @@ name: Build PR
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Build
run: dotnet build
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Build
run: dotnet build

2
.vscode/launch.json vendored
View File

@@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/NDecrypt/bin/Debug/net9.0/NDecrypt.dll",
"program": "${workspaceFolder}/NDecrypt/bin/Debug/net10.0/NDecrypt.dll",
"args": [],
"cwd": "${workspaceFolder}/NDecrypt",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console

View File

@@ -1,253 +0,0 @@
using System;
using System.IO;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using SabreTools.IO.Extensions;
namespace NDecrypt.Core
{
public static class CommonOperations
{
#region AES
/// <summary>
/// Create AES decryption cipher and intialize
/// </summary>
/// <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(byte[] key, byte[] iv)
{
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;
}
/// <summary>
/// Create AES encryption cipher and intialize
/// </summary>
/// <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(byte[] key, byte[] iv)
{
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;
}
/// <summary>
/// Perform an AES operation using an existing cipher
/// </summary>
public static void PerformAESOperation(uint size,
IBufferedCipher cipher,
Stream input,
Stream output,
Action<string>? 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");
}
/// <summary>
/// Perform an AES operation using two existing ciphers
/// </summary>
public static void PerformAESOperation(uint size,
IBufferedCipher firstCipher,
IBufferedCipher secondCipher,
Stream input,
Stream output,
Action<string> 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");
}
#endregion
#region Byte Arrays
/// <summary>
/// Add an integer value to a number represented by a byte array
/// </summary>
/// <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[] Add(byte[] input, uint add)
{
byte[] addBytes = BitConverter.GetBytes(add);
Array.Reverse(addBytes);
byte[] paddedBytes = new byte[16];
Array.Copy(addBytes, 0, paddedBytes, 12, 4);
return Add(input, paddedBytes);
}
/// <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);
byte[] output = new byte[outLength];
uint carry = 0;
for (int i = addBytes - 1; i >= 0; i--)
{
uint addValue = (uint)(left[i] + right[i]) + carry;
output[i] = (byte)addValue;
carry = addValue >> 8;
}
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 byte array
/// </summary>
/// <param name="val">Byte array value to rotate</param>
/// <param name="r_bits">Number of bits to rotate</param>
/// <returns>Rotated byte array value</returns>
public static byte[] RotateLeft(byte[] val, int r_bits)
{
byte[] output = new byte[val.Length];
Array.Copy(val, output, output.Length);
// 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];
}
output[output.Length - 1] = temp;
r_bits -= 8;
}
// 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);
// Make sure the first byte carries to the end
if (i == 0)
wrap = carry;
// Otherwise, move to the last byte
else
output[i - 1] |= carry;
// Shift the current bits
output[i] <<= r_bits;
}
// Make sure the wrap happens
output[output.Length - 1] |= wrap;
}
return output;
}
/// <summary>
/// XOR two numbers represented by byte arrays
/// </summary>
/// <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)
{
int xorBytes = Math.Min(left.Length, right.Length);
int outLength = Math.Max(left.Length, right.Length);
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;
}
#endregion
}
}

View File

@@ -86,4 +86,4 @@ namespace NDecrypt.Core
}
}
}
}
}

View File

@@ -1,7 +1,9 @@
using System;
using System.IO;
using System.Text;
#if NETFRAMEWORK || NETSTANDARD2_0_OR_GREATER
using SabreTools.IO.Extensions;
#endif
using SabreTools.Serialization.Wrappers;
namespace NDecrypt.Core
@@ -26,7 +28,7 @@ namespace NDecrypt.Core
try
{
// If the output is provided, copy the input file
if (output != null)
if (output is not null)
File.Copy(input, output, overwrite: true);
else
output = input;
@@ -36,14 +38,14 @@ namespace NDecrypt.Core
// Deserialize the cart information
var nitro = Nitro.Create(reader);
if (nitro == null)
if (nitro is null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
}
// Ensure the secure area was read
if (nitro.SecureArea == null)
if (nitro.SecureArea is null)
{
Console.WriteLine("Error: Invalid secure area!");
return false;
@@ -78,7 +80,7 @@ namespace NDecrypt.Core
try
{
// If the output is provided, copy the input file
if (output != null)
if (output is not null)
File.Copy(input, output, overwrite: true);
else
output = input;
@@ -88,14 +90,14 @@ namespace NDecrypt.Core
// Deserialize the cart information
var nitro = Nitro.Create(reader);
if (nitro == null)
if (nitro is null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
}
// Ensure the secure area was read
if (nitro.SecureArea == null)
if (nitro.SecureArea is null)
{
Console.WriteLine("Error: Invalid secure area!");
return false;
@@ -134,7 +136,7 @@ namespace NDecrypt.Core
// Deserialize the cart information
var cart = Nitro.Create(input);
if (cart?.Model == null)
if (cart?.Model is null)
return "Error: Not a DS/DSi cart image!";
// Get a string builder for the status
@@ -143,7 +145,7 @@ namespace NDecrypt.Core
// Get the encryption status
bool? decrypted = cart.CheckIfDecrypted(out _);
if (decrypted == null)
if (decrypted is null)
sb.Append("Empty");
else if (decrypted == true)
sb.Append("Decrypted");

View File

@@ -1,7 +1,8 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.IO.Encryption;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
namespace NDecrypt.Core
{
@@ -243,7 +244,7 @@ namespace NDecrypt.Core
/// <param name="keyfile">Path to the keyfile</param>
public DecryptArgs(string? config)
{
if (config == null || !File.Exists(config))
if (config is null || !File.Exists(config))
{
IsReady = false;
return;
@@ -251,7 +252,7 @@ namespace NDecrypt.Core
// Try to read the configuration file
var configObj = Configuration.Create(config);
if (configObj == null)
if (configObj is null)
{
IsReady = false;
return;
@@ -282,9 +283,8 @@ namespace NDecrypt.Core
// NitroEncryptionData
if (NitroEncryptionData.Length > 0)
{
using var hasher = System.Security.Cryptography.SHA512.Create();
byte[] actual = hasher.ComputeHash(NitroEncryptionData);
if (!Extensions.EqualsExactly(ExpectedNitroSha512Hash, actual))
byte[]? actual = HashTool.GetByteArrayHashArray(NitroEncryptionData, HashType.SHA512);
if (actual is null || !actual.EqualsExactly(ExpectedNitroSha512Hash))
{
Console.WriteLine($"NitroEncryptionData invalid value, disabling...");
NitroEncryptionData = [];
@@ -294,9 +294,9 @@ namespace NDecrypt.Core
// KeyX0x18
if (KeyX0x18.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x18, TestIV);
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x18, actual))
if (!actual.EqualsExactly(ExpectedKeyX0x18))
{
Console.WriteLine($"KeyX0x18 invalid value, disabling...");
KeyX0x18 = [];
@@ -306,9 +306,9 @@ namespace NDecrypt.Core
// DevKeyX0x18
if (DevKeyX0x18.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x18, TestIV);
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x18, actual))
if (!actual.EqualsExactly(ExpectedDevKeyX0x18))
{
Console.WriteLine($"DevKeyX0x18 invalid value, disabling...");
DevKeyX0x18 = [];
@@ -318,9 +318,9 @@ namespace NDecrypt.Core
// KeyX0x1B
if (KeyX0x1B.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x1B, TestIV);
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x1B, actual))
if (!actual.EqualsExactly(ExpectedKeyX0x1B))
{
Console.WriteLine($"KeyX0x1B invalid value, disabling...");
KeyX0x1B = [];
@@ -330,9 +330,9 @@ namespace NDecrypt.Core
// DevKeyX0x1B
if (DevKeyX0x1B.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x1B, TestIV);
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x1B, actual))
if (!actual.EqualsExactly(ExpectedDevKeyX0x1B))
{
Console.WriteLine($"DevKeyX0x1B invalid value, disabling...");
DevKeyX0x1B = [];
@@ -342,9 +342,9 @@ namespace NDecrypt.Core
// KeyX0x25
if (KeyX0x25.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x25, TestIV);
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x25, actual))
if (!actual.EqualsExactly(ExpectedKeyX0x25))
{
Console.WriteLine($"KeyX0x25 invalid value, disabling...");
KeyX0x25 = [];
@@ -354,9 +354,9 @@ namespace NDecrypt.Core
// DevKeyX0x25
if (DevKeyX0x25.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x25, TestIV);
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x25, actual))
if (!actual.EqualsExactly(ExpectedDevKeyX0x25))
{
Console.WriteLine($"DevKeyX0x25 invalid value, disabling...");
DevKeyX0x25 = [];
@@ -366,9 +366,9 @@ namespace NDecrypt.Core
// KeyX0x2C
if (KeyX0x2C.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x2C, TestIV);
var cipher = AESCTR.CreateEncryptionCipher(KeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x2C, actual))
if (!actual.EqualsExactly(ExpectedKeyX0x2C))
{
Console.WriteLine($"KeyX0x2C invalid value, disabling...");
KeyX0x2C = [];
@@ -378,9 +378,9 @@ namespace NDecrypt.Core
// DevKeyX0x2C
if (DevKeyX0x2C.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x2C, TestIV);
var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x2C, actual))
if (!actual.EqualsExactly(ExpectedDevKeyX0x2C))
{
Console.WriteLine($"DevKeyX0x2C invalid value, disabling...");
DevKeyX0x2C = [];
@@ -388,4 +388,4 @@ namespace NDecrypt.Core
}
}
}
}
}

View File

@@ -10,7 +10,7 @@
/// <param name="force">Indicates if the operation should be forced</param>
/// <returns>True if the file could be encrypted, false otherwise</returns>
/// <remarks>If an output filename is not provided, the input file will be overwritten</remarks>
bool EncryptFile(string input, string? output, bool force);
public bool EncryptFile(string input, string? output, bool force);
/// <summary>
/// Attempts to decrypt an input file
@@ -20,13 +20,13 @@
/// <param name="force">Indicates if the operation should be forced</param>
/// <returns>True if the file could be decrypted, false otherwise</returns>
/// <remarks>If an output filename is not provided, the input file will be overwritten</remarks>
bool DecryptFile(string input, string? output, bool force);
public bool DecryptFile(string input, string? output, bool force);
/// <summary>
/// Attempts to get information on an input file
/// </summary>
/// <param name="filename">Name of the file get information on</param>
/// <returns>String representing the info, null on error</returns>
string? GetInformation(string filename);
public string? GetInformation(string filename);
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<IncludeSymbols>true</IncludeSymbols>
@@ -11,7 +11,7 @@
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.4.2</VersionPrefix>
<VersionPrefix>0.5.0</VersionPrefix>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -23,27 +23,10 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<!-- Support All Frameworks -->
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net4`))">
<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`netcoreapp`)) OR $(TargetFramework.StartsWith(`net5`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net6`)) OR $(TargetFramework.StartsWith(`net7`)) OR $(TargetFramework.StartsWith(`net8`)) OR $(TargetFramework.StartsWith(`net9`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(RuntimeIdentifier.StartsWith(`osx-arm`))">
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.NetCore" Version="1.9.0" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="SabreTools.Hashing" Version="1.5.0" />
<PackageReference Include="SabreTools.IO" Version="1.7.2" />
<PackageReference Include="SabreTools.Models" Version="1.7.1" />
<PackageReference Include="SabreTools.Serialization" Version="1.9.1" />
<PackageReference Include="SabreTools.Hashing" Version="[1.6.0]" />
<PackageReference Include="SabreTools.IO" Version="[1.9.0]" />
<PackageReference Include="SabreTools.Serialization" Version="[2.2.1]" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,6 @@
using System;
using Org.BouncyCastle.Crypto;
using SabreTools.Models.N3DS;
using static NDecrypt.Core.CommonOperations;
using SabreTools.Data.Models.N3DS;
using SabreTools.IO.Extensions;
namespace NDecrypt.Core
{
@@ -43,8 +42,8 @@ namespace NDecrypt.Core
// Validate inputs
if (args.IsReady != true)
throw new InvalidOperationException($"{nameof(args)} must be initialized before use");
if (signature != null && signature.Length < 16)
throw new DataLengthException($"{nameof(signature)} must be at least 16 bytes");
if (signature is not null && signature.Length < 16)
throw new ArgumentOutOfRangeException(nameof(signature), $"{nameof(signature)} must be at least 16 bytes");
// Set fields for future use
_decryptArgs = args;
@@ -56,16 +55,16 @@ namespace NDecrypt.Core
// Backup headers can't have a KeyY value set
KeyY = new byte[16];
if (signature != null)
if (signature is not null)
Array.Copy(signature, KeyY, 16);
// Set the standard normal key values
NormalKey = new byte[16];
NormalKey2C = RotateLeft(KeyX2C, 2);
NormalKey2C = Xor(NormalKey2C, KeyY);
NormalKey2C = Add(NormalKey2C, args.AESHardwareConstant);
NormalKey2C = RotateLeft(NormalKey2C, 87);
NormalKey2C = KeyX2C.RotateLeft(2);
NormalKey2C = NormalKey2C.Xor(KeyY);
NormalKey2C = NormalKey2C.Add(add: args.AESHardwareConstant);
NormalKey2C = NormalKey2C.RotateLeft(87);
// Special case for zero-key
#if NET20 || NET35
@@ -105,10 +104,10 @@ namespace NDecrypt.Core
}
// Set the normal key based on the new KeyX value
NormalKey = RotateLeft(KeyX, 2);
NormalKey = Xor(NormalKey, KeyY);
NormalKey = Add(NormalKey, args.AESHardwareConstant);
NormalKey = RotateLeft(NormalKey, 87);
NormalKey = KeyX.RotateLeft(2);
NormalKey = NormalKey.Xor(KeyY);
NormalKey = NormalKey.Add(args.AESHardwareConstant);
NormalKey = NormalKey.RotateLeft(87);
}
/// <summary>
@@ -130,10 +129,10 @@ namespace NDecrypt.Core
// Encrypting RomFS for partitions 1 and up always use Key0x2C
KeyX = _development ? _decryptArgs.DevKeyX0x2C : _decryptArgs.KeyX0x2C;
NormalKey = RotateLeft(KeyX, 2);
NormalKey = Xor(NormalKey, KeyY);
NormalKey = Add(NormalKey, _decryptArgs.AESHardwareConstant);
NormalKey = RotateLeft(NormalKey, 87);
NormalKey = KeyX.RotateLeft(2);
NormalKey = NormalKey.Xor(KeyY);
NormalKey = NormalKey.Add(_decryptArgs.AESHardwareConstant);
NormalKey = NormalKey.RotateLeft(87);
}
}
}
}

View File

@@ -1,11 +1,11 @@
using System;
using System.IO;
using System.Text;
using SabreTools.Data.Models.N3DS;
using SabreTools.IO.Encryption;
using SabreTools.IO.Extensions;
using SabreTools.Models.N3DS;
using SabreTools.Serialization.Wrappers;
using static NDecrypt.Core.CommonOperations;
using static SabreTools.Models.N3DS.Constants;
using static SabreTools.Data.Models.N3DS.Constants;
namespace NDecrypt.Core
{
@@ -47,7 +47,7 @@ namespace NDecrypt.Core
try
{
// If the output is provided, copy the input file
if (output != null)
if (output is not null)
File.Copy(input, output, overwrite: true);
else
output = input;
@@ -58,7 +58,7 @@ namespace NDecrypt.Core
// Deserialize the cart information
var cart = N3DS.Create(reader);
if (cart?.Model == null)
if (cart?.Model is null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
@@ -86,7 +86,7 @@ namespace NDecrypt.Core
private void DecryptAllPartitions(N3DS cart, bool force, Stream reader, Stream writer)
{
// Check the partitions table
if (cart.PartitionsTable == null || cart.Partitions == null)
if (cart.PartitionsTable is null || cart.Partitions is null)
{
Console.WriteLine("Invalid partitions table!");
return;
@@ -96,7 +96,7 @@ namespace NDecrypt.Core
for (int p = 0; p < 8; p++)
{
var partition = cart.Partitions[p];
if (partition == null || partition.MagicID != NCCHMagicNumber)
if (partition is null || partition.MagicID != NCCHMagicNumber)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
@@ -104,7 +104,7 @@ namespace NDecrypt.Core
// Check the partition has data
var partitionEntry = cart.PartitionsTable[p];
if (partitionEntry == null || partitionEntry.Length == 0)
if (partitionEntry is null || partitionEntry.Length == 0)
{
Console.WriteLine($"Partition {p} No data... Skipping...");
continue;
@@ -168,7 +168,7 @@ namespace NDecrypt.Core
{
// Get the partition
var partition = cart.Partitions?[index];
if (partition?.Flags == null)
if (partition?.Flags is null)
return;
// Get partition-specific values
@@ -211,10 +211,10 @@ namespace NDecrypt.Core
Console.WriteLine($"Partition {index}: Decrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESDecryptionCipher(_keysMap[index].NormalKey2C, cart.PlainIV(index));
var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey2C, cart.PlainIV(index));
// Process the extended header
PerformAESOperation(Constants.CXTExtendedDataHeaderLength, cipher, reader, writer, null);
AESCTR.PerformOperation(Constants.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
@@ -264,16 +264,16 @@ namespace NDecrypt.Core
// Create the ExeFS AES cipher for this partition
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = Add(cart.ExeFSIV(index), ctroffsetE);
var cipher = CreateAESDecryptionCipher(_keysMap[index].NormalKey2C, exefsIVWithOffset);
byte[] exefsIVWithOffset = cart.ExeFSIV(index).Add(ctroffsetE);
var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the decryption
exeFsSize -= cart.MediaUnitSize;
PerformAESOperation(exeFsSize,
AESCTR.PerformOperation(exeFsSize,
cipher,
reader,
writer,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
return true;
}
@@ -302,7 +302,7 @@ namespace NDecrypt.Core
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));
var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
byte[] readBytes = reader.ReadBytes((int)cart.MediaUnitSize);
@@ -325,7 +325,7 @@ namespace NDecrypt.Core
/// <param name="writer">Stream representing the output</param>
private void DecryptExeFSFileEntries(N3DS cart, int index, Stream reader, Stream writer)
{
if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
if (cart.ExeFSHeaders is null || index < 0 || index > cart.ExeFSHeaders.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
@@ -334,11 +334,11 @@ namespace NDecrypt.Core
// Reread the decrypted ExeFS header
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
reader.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
cart.ExeFSHeaders[index] = SabreTools.Serialization.Deserializers.N3DS.ParseExeFSHeader(reader);
cart.ExeFSHeaders[index] = SabreTools.Serialization.Readers.N3DS.ParseExeFSHeader(reader);
// Get the ExeFS header
var exeFsHeader = cart.ExeFSHeaders[index];
if (exeFsHeader?.FileHeaders == null)
if (exeFsHeader?.FileHeaders is null)
{
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
@@ -356,26 +356,26 @@ namespace NDecrypt.Core
// Get the file header
var fileHeader = exeFsHeader.FileHeaders[i];
if (fileHeader == null)
if (fileHeader is null)
continue;
// Create the ExeFS AES ciphers for this partition
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);
byte[] exefsIVWithOffsetForHeader = cart.ExeFSIV(index).Add(ctroffset);
var firstCipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = AESCTR.CreateEncryptionCipher(_keysMap[index].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
PerformAESOperation(fileHeader.FileSize,
AESCTR.PerformOperation(fileHeader.FileSize,
firstCipher,
secondCipher,
reader,
writer,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
}
}
@@ -408,14 +408,14 @@ namespace NDecrypt.Core
writer.Seek(romFsOffset, SeekOrigin.Begin);
// Create the RomFS AES cipher for this partition
var cipher = CreateAESDecryptionCipher(_keysMap[index].NormalKey, cart.RomFSIV(index));
var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey, cart.RomFSIV(index));
// Setup and perform the decryption
PerformAESOperation(romFsSize,
AESCTR.PerformOperation(romFsSize,
cipher,
reader,
writer,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
s => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
return true;
}
@@ -466,7 +466,7 @@ namespace NDecrypt.Core
try
{
// If the output is provided, copy the input file
if (output != null)
if (output is not null)
File.Copy(input, output, overwrite: true);
else
output = input;
@@ -477,7 +477,7 @@ namespace NDecrypt.Core
// Deserialize the cart information
var cart = N3DS.Create(reader);
if (cart?.Model == null)
if (cart?.Model is null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
@@ -505,7 +505,7 @@ namespace NDecrypt.Core
private void EncryptAllPartitions(N3DS cart, bool force, Stream reader, Stream writer)
{
// Check the partitions table
if (cart.PartitionsTable == null || cart.Partitions == null)
if (cart.PartitionsTable is null || cart.Partitions is null)
{
Console.WriteLine("Invalid partitions table!");
return;
@@ -516,7 +516,7 @@ namespace NDecrypt.Core
{
// Check the partition exists
var partition = cart.Partitions[p];
if (partition == null || partition.MagicID != NCCHMagicNumber)
if (partition is null || partition.MagicID != NCCHMagicNumber)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
@@ -524,7 +524,7 @@ namespace NDecrypt.Core
// Check the partition has data
var partitionEntry = cart.PartitionsTable[p];
if (partitionEntry == null || partitionEntry.Length == 0)
if (partitionEntry is null || partitionEntry.Length == 0)
{
Console.WriteLine($"Partition {p} No data... Skipping...");
continue;
@@ -588,12 +588,12 @@ namespace NDecrypt.Core
{
// Get the partition
var partition = cart.Partitions?[index];
if (partition == null)
if (partition is null)
return;
// Get the backup header
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == null)
if (backupHeader?.Flags is null)
return;
// Get partition-specific values
@@ -636,10 +636,10 @@ namespace NDecrypt.Core
Console.WriteLine($"Partition {index}: Encrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESEncryptionCipher(_keysMap[index].NormalKey2C, cart.PlainIV(index));
var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey2C, cart.PlainIV(index));
// Process the extended header
PerformAESOperation(Constants.CXTExtendedDataHeaderLength, cipher, reader, writer, null);
AESCTR.PerformOperation(Constants.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
@@ -658,7 +658,7 @@ namespace NDecrypt.Core
/// <param name="writer">Stream representing the output</param>
private bool EncryptExeFS(N3DS cart, int index, Stream reader, Stream writer)
{
if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
if (cart.ExeFSHeaders is null || index < 0 || index > cart.ExeFSHeaders.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
@@ -666,7 +666,7 @@ namespace NDecrypt.Core
// Get the ExeFS header
var exefsHeader = cart.ExeFSHeaders[index];
if (exefsHeader == null)
if (exefsHeader is null)
{
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return false;
@@ -690,16 +690,16 @@ namespace NDecrypt.Core
// Create the ExeFS AES cipher for this partition
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = Add(cart.ExeFSIV(index), ctroffsetE);
var cipher = CreateAESEncryptionCipher(_keysMap[index].NormalKey2C, exefsIVWithOffset);
byte[] exefsIVWithOffset = cart.ExeFSIV(index).Add(ctroffsetE);
var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the encryption
uint exeFsSize = cart.GetExeFSSize(index) - cart.MediaUnitSize;
PerformAESOperation(exeFsSize,
AESCTR.PerformOperation(exeFsSize,
cipher,
reader,
writer,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
return true;
}
@@ -728,7 +728,7 @@ namespace NDecrypt.Core
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));
var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
byte[] readBytes = reader.ReadBytes((int)cart.MediaUnitSize);
@@ -764,7 +764,7 @@ namespace NDecrypt.Core
// If the header failed to read, log and return
var exeFsHeader = cart.ExeFSHeaders?[index];
if (exeFsHeader?.FileHeaders == null)
if (exeFsHeader?.FileHeaders is null)
{
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
@@ -779,26 +779,26 @@ namespace NDecrypt.Core
// Get the file header
var fileHeader = exeFsHeader.FileHeaders[i];
if (fileHeader == null)
if (fileHeader is null)
continue;
// Create the ExeFS AES ciphers for this partition
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);
byte[] exefsIVWithOffsetForHeader = cart.ExeFSIV(index).Add(ctroffset);
var firstCipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = AESCTR.CreateDecryptionCipher(_keysMap[index].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
PerformAESOperation(fileHeader.FileSize,
AESCTR.PerformOperation(fileHeader.FileSize,
firstCipher,
secondCipher,
reader,
writer,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
}
}
@@ -838,14 +838,14 @@ namespace NDecrypt.Core
}
// Create the RomFS AES cipher for this partition
var cipher = CreateAESEncryptionCipher(_keysMap[index].NormalKey, cart.RomFSIV(index));
var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey, cart.RomFSIV(index));
// Setup and perform the decryption
PerformAESOperation(romFsSize,
AESCTR.PerformOperation(romFsSize,
cipher,
reader,
writer,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
s => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
return true;
}
@@ -863,7 +863,7 @@ namespace NDecrypt.Core
// Get the backup header
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == null)
if (backupHeader?.Flags is null)
return;
// Seek to the CryptoMethod location
@@ -901,7 +901,7 @@ namespace NDecrypt.Core
// Deserialize the cart information
var cart = N3DS.Create(input);
if (cart?.Model == null)
if (cart?.Model is null)
return "Error: Not a 3DS cart image!";
// Get a string builder for the status

View File

@@ -1,16 +1,5 @@
namespace NDecrypt
{
/// <summary>
/// Functionality to use from the program
/// </summary>
internal enum Feature
{
NULL,
Decrypt,
Encrypt,
Info,
}
/// <summary>
/// Type of the detected file
/// </summary>
@@ -23,4 +12,4 @@ namespace NDecrypt
N3DS,
iQue3DS,
}
}
}

View File

@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.IO;
using NDecrypt.Core;
using SabreTools.CommandLine;
using SabreTools.CommandLine.Inputs;
namespace NDecrypt.Features
{
internal abstract class BaseFeature : Feature
{
#region Common Inputs
protected const string ConfigName = "config";
protected readonly StringInput ConfigString = new(ConfigName, ["-c", "--config"], "Path to config.json");
protected const string DevelopmentName = "development";
protected readonly FlagInput DevelopmentFlag = new(DevelopmentName, ["-d", "--development"], "Enable using development keys, if available");
protected const string ForceName = "force";
protected readonly FlagInput ForceFlag = new(ForceName, ["-f", "--force"], "Force operation by avoiding sanity checks");
protected const string HashName = "hash";
protected readonly FlagInput HashFlag = new(HashName, "--hash", "Output size and hashes to a companion file");
protected const string OverwriteName = "overwrite";
protected readonly FlagInput OverwriteFlag = new(OverwriteName, ["-o", "--overwrite"], "Overwrite input files instead of creating new ones");
#endregion
/// <summary>
/// Mapping of reusable tools
/// </summary>
private readonly Dictionary<FileType, ITool> _tools = [];
protected BaseFeature(string name, string[] flags, string description, string? detailed = null)
: base(name, flags, description, detailed)
{
}
/// <inheritdoc/>
public override bool Execute()
{
// Initialize required pieces
InitializeTools();
for (int i = 0; i < Inputs.Count; i++)
{
if (File.Exists(Inputs[i]))
{
ProcessFile(Inputs[i]);
}
else if (Directory.Exists(Inputs[i]))
{
foreach (string file in Directory.GetFiles(Inputs[i], "*", SearchOption.AllDirectories))
{
ProcessFile(file);
}
}
else
{
Console.WriteLine($"{Inputs[i]} is not a file or folder. Please check your spelling and formatting and try again.");
}
}
return true;
}
/// <inheritdoc/>
public override bool VerifyInputs() => Inputs.Count > 0;
/// <summary>
/// Process a single file path
/// </summary>
/// <param name="input">File path to process</param>
protected abstract void ProcessFile(string input);
/// <summary>
/// Initialize the tools to be used by the feature
/// </summary>
private void InitializeTools()
{
var decryptArgs = new DecryptArgs(GetString(ConfigName, "config.json"));
_tools[FileType.NDS] = new DSTool(decryptArgs);
_tools[FileType.N3DS] = new ThreeDSTool(GetBoolean(DevelopmentName), decryptArgs);
}
/// <summary>
/// Derive the encryption tool to be used for the given file
/// </summary>
/// <param name="filename">Filename to derive the tool from</param>
protected ITool? DeriveTool(string filename)
{
if (!File.Exists(filename))
{
Console.WriteLine($"{filename} does not exist! Skipping...");
return null;
}
FileType type = DetermineFileType(filename);
return type switch
{
FileType.NDS => _tools[FileType.NDS],
FileType.NDSi => _tools[FileType.NDS],
FileType.iQueDS => _tools[FileType.NDS],
FileType.N3DS => _tools[FileType.N3DS],
_ => null,
};
}
/// <summary>
/// Derive an output filename from the input, if possible
/// </summary>
/// <param name="filename">Name of the input file to derive from</param>
/// <param name="extension">Preferred extension set by the feature implementation</param>
/// <returns>Output filename based on the input</returns>
protected static string GetOutputFile(string filename, string extension)
{
// Empty filenames are passed back
if (filename.Length == 0)
return filename;
// TODO: Replace the suffix instead of just appending
// TODO: Ensure that the input and output aren't the same
// If the extension does not include a leading period
#if NETCOREAPP || NETSTANDARD2_0_OR_GREATER
if (!extension.StartsWith('.'))
#else
if (!extension.StartsWith("."))
#endif
extension = $".{extension}";
// Append the extension and return
return $"{filename}{extension}";
}
/// <summary>
/// Write out the hashes of a file to a named file
/// </summary>
/// <param name="filename">Filename to get hashes for/param>
protected static void WriteHashes(string filename)
{
// If the file doesn't exist, don't try anything
if (!File.Exists(filename))
return;
// Get the hash string from the file
string? hashString = HashingHelper.GetInfo(filename);
if (hashString is null)
return;
// Open the output file and write the hashes
using var fs = File.Open(Path.GetFullPath(filename) + ".hash", FileMode.Create, FileAccess.Write, FileShare.None);
using var sw = new StreamWriter(fs);
sw.Write(hashString);
}
/// <summary>
/// Determine the file type from the filename extension
/// </summary>
/// <param name="filename">Filename to derive the type from</param>
/// <returns>FileType value, if possible</returns>
private static FileType DetermineFileType(string filename)
{
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".nds.dec", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area decrypted
|| filename.EndsWith(".nds.enc", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area encrypted
|| filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo DS");
return FileType.NDS;
}
else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as Nintendo DSi");
return FileType.NDSi;
}
else if (filename.EndsWith(".ids", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as iQue DS");
return FileType.iQueDS;
}
else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".3ds.dec", StringComparison.OrdinalIgnoreCase) // Decrypted carts/images
|| filename.EndsWith(".3ds.enc", StringComparison.OrdinalIgnoreCase) // Encrypted carts/images
|| filename.EndsWith(".cci", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo 3DS");
return FileType.N3DS;
}
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds, *.cci");
return FileType.NULL;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
namespace NDecrypt.Features
{
internal sealed class DecryptFeature : BaseFeature
{
#region Feature Definition
public const string DisplayName = "decrypt";
private static readonly string[] _flags = ["d", "decrypt"];
private const string _description = "Decrypt the input files";
#endregion
public DecryptFeature()
: base(DisplayName, _flags, _description)
{
RequiresInputs = true;
Add(ConfigString);
Add(DevelopmentFlag);
Add(ForceFlag);
Add(HashFlag);
// TODO: Include this when enabled
// Add(OverwriteFlag);
}
/// <inheritdoc/>
protected override void ProcessFile(string input)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(input);
if (tool is null)
return;
// Derive the output filename, if required
string? output = null;
if (!GetBoolean(OverwriteName))
output = GetOutputFile(input, ".dec");
Console.WriteLine($"Processing {input}");
if (!tool.DecryptFile(input, output, GetBoolean(ForceName)))
{
Console.WriteLine("Decryption failed!");
return;
}
// Output the file hashes, if expected
if (GetBoolean(HashName))
WriteHashes(input);
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
namespace NDecrypt.Features
{
internal sealed class EncryptFeature : BaseFeature
{
#region Feature Definition
public const string DisplayName = "encrypt";
private static readonly string[] _flags = ["e", "encrypt"];
private const string _description = "Encrypt the input files";
#endregion
public EncryptFeature()
: base(DisplayName, _flags, _description)
{
RequiresInputs = true;
Add(ConfigString);
Add(DevelopmentFlag);
Add(ForceFlag);
Add(HashFlag);
// TODO: Include this when enabled
// Add(OverwriteFlag);
}
/// <inheritdoc/>
protected override void ProcessFile(string input)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(input);
if (tool is null)
return;
// Derive the output filename, if required
string? output = null;
if (!GetBoolean(OverwriteName))
output = GetOutputFile(input, ".enc");
Console.WriteLine($"Processing {input}");
if (!tool.EncryptFile(input, output, GetBoolean(ForceName)))
{
Console.WriteLine("Encryption failed!");
return;
}
// Output the file hashes, if expected
if (GetBoolean(HashName))
WriteHashes(input);
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
namespace NDecrypt.Features
{
internal sealed class InfoFeature : BaseFeature
{
#region Feature Definition
public const string DisplayName = "info";
private static readonly string[] _flags = ["i", "info"];
private const string _description = "Output file information";
#endregion
public InfoFeature()
: base(DisplayName, _flags, _description)
{
RequiresInputs = true;
Add(HashFlag);
}
/// <inheritdoc/>
protected override void ProcessFile(string input)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(input);
if (tool is null)
return;
Console.WriteLine($"Processing {input}");
string? infoString = tool.GetInformation(input);
infoString ??= "There was a problem getting file information!";
Console.WriteLine(infoString);
// Output the file hashes, if expected
if (GetBoolean(HashName))
WriteHashes(input);
}
}
}

View File

@@ -19,15 +19,15 @@ namespace NDecrypt
// Get the file information, if possible
HashType[] hashTypes = [HashType.CRC32, HashType.MD5, HashType.SHA1, HashType.SHA256];
var hashDict = HashTool.GetFileHashesAndSize(input, hashTypes, out long size);
if (hashDict == null)
if (hashDict is null)
return null;
// Get the results
return $"Size: {size}\n"
+ $"CRC-32: {(hashDict.ContainsKey(HashType.CRC32) ? hashDict[HashType.CRC32] : string.Empty)}\n"
+ $"MD5: {(hashDict.ContainsKey(HashType.MD5) ? hashDict[HashType.MD5] : string.Empty)}\n"
+ $"SHA-1: {(hashDict.ContainsKey(HashType.SHA1) ? hashDict[HashType.SHA1] : string.Empty)}\n"
+ $"CSHA-256: {(hashDict.ContainsKey(HashType.SHA256) ? hashDict[HashType.SHA256] : string.Empty)}\n";
+ $"CRC-32: {(hashDict.TryGetValue(HashType.CRC32, out string? value) ? value : string.Empty)}\n"
+ $"MD5: {(hashDict.TryGetValue(HashType.MD5, out string? value1) ? value1 : string.Empty)}\n"
+ $"SHA-1: {(hashDict.TryGetValue(HashType.SHA1, out string? value2) ? value2 : string.Empty)}\n"
+ $"SHA-256: {(hashDict.TryGetValue(HashType.SHA256, out string? value3) ? value3 : string.Empty)}\n";
}
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
@@ -11,7 +11,7 @@
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.4.2</VersionPrefix>
<VersionPrefix>0.5.0</VersionPrefix>
<!-- Package Properties -->
<Title>NDecrypt</Title>
@@ -31,11 +31,11 @@
<PropertyGroup Condition="$(TargetFramework.StartsWith(`netcoreapp`)) OR $(TargetFramework.StartsWith(`net5`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net6`)) OR $(TargetFramework.StartsWith(`net7`)) OR $(TargetFramework.StartsWith(`net8`)) OR $(TargetFramework.StartsWith(`net9`))">
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net6`)) OR $(TargetFramework.StartsWith(`net7`)) OR $(TargetFramework.StartsWith(`net8`)) OR $(TargetFramework.StartsWith(`net9`)) OR $(TargetFramework.StartsWith(`net10`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(RuntimeIdentifier.StartsWith(`osx-arm`))">
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
@@ -43,7 +43,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.CommandLine" Version="[1.4.0]" />
</ItemGroup>
</Project>

View File

@@ -1,263 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
#if NET20 || NET35 || NET40 || NET452
using System.Reflection;
#endif
namespace NDecrypt
{
/// <summary>
/// Set of options for the test executable
/// </summary>
internal sealed class Options
{
#region Properties
/// <summary>
/// Feature to process input files with
/// </summary>
public Feature Feature { get; private set; }
/// <summary>
/// Path to config.json
/// </summary>
public string? ConfigPath { get; private set; }
/// <summary>
/// Enable using development keys, if available
/// </summary>
public bool Development { get; private set; } = false;
/// <summary>
/// Force operation by avoiding sanity checks
/// </summary>
public bool Force { get; private set; } = false;
/// <summary>
/// Set of input paths to use for operations
/// </summary>
public List<string> InputPaths { get; private set; } = [];
/// <summary>
/// Output size and hashes to a companion file
/// </summary>
public bool OutputHashes { get; private set; } = false;
/// <summary>
/// Enable overwriting of the original file
/// </summary>
/// TODO: Change this to default false when hooked up
public bool Overwrite { get; private set; } = true;
#endregion
/// <summary>
/// Parse commandline arguments into an Options object
/// </summary>
public static Options? ParseOptions(string[] args)
{
// If we have invalid arguments
if (args == null || args.Length < 2)
{
Console.WriteLine("Not enough arguments");
return null;
}
// Create an Options object
var options = new Options();
// Derive the feature
switch (args[0])
{
case "-?":
case "-h":
case "--help":
return null;
case "d":
case "decrypt":
options.Feature = Feature.Decrypt;
break;
case "e":
case "encrypt":
options.Feature = Feature.Encrypt;
break;
case "i":
case "info":
options.Feature = Feature.Info;
break;
default:
Console.WriteLine($"Invalid operation: {args[0]}");
return null;
}
// Parse the options and paths
for (int index = 1; index < args.Length; index++)
{
string arg = args[index];
switch (arg)
{
case "-?":
case "-h":
case "--help":
return null;
case "-c":
case "--config":
if (index == args.Length - 1)
{
Console.WriteLine("Invalid config path: no additional arguments found!");
continue;
}
index++;
options.ConfigPath = args[index];
if (string.IsNullOrEmpty(options.ConfigPath))
Console.WriteLine($"Invalid config path: null or empty path found!");
options.ConfigPath = Path.GetFullPath(options.ConfigPath);
if (!File.Exists(options.ConfigPath))
{
Console.WriteLine($"Invalid config path: file {options.ConfigPath} not found!");
options.ConfigPath = null;
}
break;
case "-d":
case "--development":
options.Development = true;
break;
case "-f":
case "--force":
options.Force = true;
break;
case "--hash":
options.OutputHashes = true;
break;
case "-o":
case "--overwrite":
options.Overwrite = true;
break;
default:
options.InputPaths.Add(arg);
break;
}
}
// Validate we have any input paths to work on
if (options.InputPaths.Count == 0)
{
Console.WriteLine("At least one path is required!");
return null;
}
// Derive the config path based on the runtime folder if not already set
options.ConfigPath = DeriveConfigFile(options.ConfigPath);
return options;
}
/// <summary>
/// Display help text
/// </summary>
/// <param name="err">Additional error text to display, can be null to ignore</param>
public static void DisplayHelp(string? err = null)
{
if (!string.IsNullOrEmpty(err))
Console.WriteLine($"Error: {err}");
Console.WriteLine("Cart Image Encrypt/Decrypt Tool");
Console.WriteLine();
Console.WriteLine("NDecrypt <operation> [options] <path> ...");
Console.WriteLine();
Console.WriteLine("Operations:");
Console.WriteLine("e, encrypt Encrypt the input files");
Console.WriteLine("d, decrypt Decrypt the input files");
Console.WriteLine("i, info Output file information");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine("-?, -h, --help Display this help text and quit");
Console.WriteLine("-c, --config <path> Path to config.json");
Console.WriteLine("-d, --development Enable using development keys, if available");
Console.WriteLine("-f, --force Force operation by avoiding sanity checks");
Console.WriteLine("--hash Output size and hashes to a companion file");
// Console.WriteLine("-o, --overwrite Overwrite input files instead of creating new ones"); // TODO: Print this when enabled
Console.WriteLine();
Console.WriteLine("<path> can be any file or folder that contains uncompressed items.");
Console.WriteLine("More than one path can be specified at a time.");
}
#region Helpers
/// <summary>
/// Derive the full path to the config file, if possible
/// </summary>
private static string? DeriveConfigFile(string? config)
{
// If a path is passed in
if (!string.IsNullOrEmpty(config))
{
config = Path.GetFullPath(config);
if (File.Exists(config))
return config;
}
// Derive the keyfile path, if possible
return GetFileLocation("config.json");
}
/// <summary>
/// Search for a file in local and config directories
/// </summary>
/// <param name="filename">Filename to check in local and config directories</param>
/// <returns>The full path to the file if found, null otherwise</returns>
/// <remarks>
/// This method looks in the following locations:
/// - %HOME%/.config/ndecrypt
/// - Assembly location directory
/// - Process runtime directory
/// </remarks>
private static string? GetFileLocation(string filename)
{
// User home directory
#if NET20 || NET35
string homeDir = Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%");
homeDir = Path.Combine(Path.Combine(homeDir, ".config"), "ndecrypt");
#else
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
homeDir = Path.Combine(homeDir, ".config", "ndecrypt");
#endif
if (File.Exists(Path.Combine(homeDir, filename)))
return Path.Combine(homeDir, filename);
// Local directory
#if NET20 || NET35 || NET40 || NET452
string runtimeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
#else
string runtimeDir = AppContext.BaseDirectory;
#endif
if (File.Exists(Path.Combine(runtimeDir, filename)))
return Path.Combine(runtimeDir, filename);
// Process directory
using var processModule = System.Diagnostics.Process.GetCurrentProcess().MainModule;
string applicationDirectory = Path.GetDirectoryName(processModule?.FileName) ?? string.Empty;
if (File.Exists(Path.Combine(applicationDirectory, filename)))
return Path.Combine(applicationDirectory, filename);
// No file was found
return null;
}
#endregion
}
}

View File

@@ -1,204 +1,89 @@
using System;
using System.Collections.Generic;
using System.IO;
using NDecrypt.Core;
using NDecrypt.Features;
using SabreTools.CommandLine;
using SabreTools.CommandLine.Features;
namespace NDecrypt
{
class Program
{
/// <summary>
/// Mapping of reusable tools
/// </summary>
private static readonly Dictionary<FileType, ITool> _tools = [];
public static void Main(string[] args)
{
// Get the options from the arguments
var options = Options.ParseOptions(args);
// Create the command set
var commandSet = CreateCommands();
// If we have an invalid state
if (options == null)
// If we have no args, show the help and quit
if (args is null || args.Length == 0)
{
Options.DisplayHelp();
commandSet.OutputAllHelp();
return;
}
// Initialize the decrypt args, if possible
var decryptArgs = new DecryptArgs(options.ConfigPath); ;
// Get the first argument as a feature flag
string featureName = args[0];
// Create reusable tools
_tools[FileType.NDS] = new DSTool(decryptArgs);
_tools[FileType.N3DS] = new ThreeDSTool(options.Development, decryptArgs);
for (int i = 0; i < options.InputPaths.Count; i++)
// Get the associated feature
var topLevel = commandSet.GetTopLevel(featureName);
if (topLevel is null || topLevel is not Feature feature)
{
if (File.Exists(options.InputPaths[i]))
{
ProcessFile(options.InputPaths[i], options);
}
else if (Directory.Exists(options.InputPaths[i]))
{
foreach (string file in Directory.GetFiles(options.InputPaths[i], "*", SearchOption.AllDirectories))
{
ProcessFile(file, options);
}
}
else
{
Console.WriteLine($"{options.InputPaths[i]} is not a file or folder. Please check your spelling and formatting and try again.");
}
Console.WriteLine($"'{featureName}' is not valid feature flag");
commandSet.OutputFeatureHelp(featureName);
return;
}
// Handle default help functionality
if (topLevel is Help helpFeature)
{
helpFeature.ProcessArgs(args, 0, commandSet);
return;
}
// Now verify that all other flags are valid
if (!feature.ProcessArgs(args, 1))
return;
// If inputs are required
if (feature.RequiresInputs && !feature.VerifyInputs())
{
commandSet.OutputFeatureHelp(topLevel.Name);
Environment.Exit(0);
}
// Now execute the current feature
if (!feature.Execute())
{
Console.Error.WriteLine("An error occurred during processing!");
commandSet.OutputFeatureHelp(topLevel.Name);
}
}
/// <summary>
/// Process a single file path
/// Create the command set for the program
/// </summary>
/// <param name="input">File path to process</param>
/// <param name="options">Options indicating how to process the file</param>
private static void ProcessFile(string input, Options options)
private static CommandSet CreateCommands()
{
// Attempt to derive the tool for the path
var tool = DeriveTool(input);
if (tool == null)
return;
List<string> header = [
"Cart Image Encrypt/Decrypt Tool",
string.Empty,
"NDecrypt <operation> [options] <path> ...",
string.Empty,
];
Console.WriteLine($"Processing {input}");
List<string> footer = [
string.Empty,
"<path> can be any file or folder that contains uncompressed items.",
"More than one path can be specified at a time.",
];
// Derive the output filename, if required
string? output = null;
if (!options.Overwrite)
output = GetOutputFile(input, options);
var commandSet = new CommandSet(header, footer);
// Encrypt or decrypt the file as requested
if (options.Feature == Feature.Encrypt && !tool.EncryptFile(input, output, options.Force))
{
Console.WriteLine("Encryption failed!");
return;
}
else if (options.Feature == Feature.Decrypt && !tool.DecryptFile(input, output, options.Force))
{
Console.WriteLine("Decryption failed!");
return;
}
else if (options.Feature == Feature.Info)
{
string? infoString = tool.GetInformation(input);
infoString ??= "There was a problem getting file information!";
commandSet.Add(new Help());
commandSet.Add(new EncryptFeature());
commandSet.Add(new DecryptFeature());
commandSet.Add(new InfoFeature());
Console.WriteLine(infoString);
}
// Output the file hashes, if expected
if (options.OutputHashes)
WriteHashes(input);
}
/// <summary>
/// Derive the encryption tool to be used for the given file
/// </summary>
/// <param name="filename">Filename to derive the tool from</param>
private static ITool? DeriveTool(string filename)
{
if (!File.Exists(filename))
{
Console.WriteLine($"{filename} does not exist! Skipping...");
return null;
}
FileType type = DetermineFileType(filename);
return type switch
{
FileType.NDS => _tools[FileType.NDS],
FileType.NDSi => _tools[FileType.NDS],
FileType.iQueDS => _tools[FileType.NDS],
FileType.N3DS => _tools[FileType.N3DS],
_ => null,
};
}
/// <summary>
/// Determine the file type from the filename extension
/// </summary>
/// <param name="filename">Filename to derive the type from</param>
/// <returns>FileType value, if possible</returns>
private static FileType DetermineFileType(string filename)
{
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".nds.dec", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area decrypted
|| filename.EndsWith(".nds.enc", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area encrypted
|| filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo DS");
return FileType.NDS;
}
else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as Nintendo DSi");
return FileType.NDSi;
}
else if (filename.EndsWith(".ids", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as iQue DS");
return FileType.iQueDS;
}
else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".3ds.dec", StringComparison.OrdinalIgnoreCase) // Decrypted carts/images
|| filename.EndsWith(".3ds.enc", StringComparison.OrdinalIgnoreCase) // Encrypted carts/images
|| filename.EndsWith(".cci", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo 3DS");
return FileType.N3DS;
}
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds, *.cci");
return FileType.NULL;
}
/// <summary>
/// Derive an output filename from the input, if possible
/// </summary>
/// <param name="filename">Name of the input file to derive from</param>
/// <param name="options">Options indicating how to process the file</param>
/// <returns>Output filename based on the input</returns>
private static string GetOutputFile(string filename, Options options)
{
// Empty filenames are passed back
if (filename.Length == 0)
return filename;
// TODO: Replace the suffix instead of just appending
// TODO: Ensure that the input and output aren't the same
// Append '.enc' or '.dec' based on the feature
if (options.Feature == Feature.Decrypt)
filename += ".dec";
else if (options.Feature == Feature.Encrypt)
filename += ".enc";
// Return the reformatted name
return filename;
}
/// <summary>
/// Write out the hashes of a file to a named file
/// </summary>
/// <param name="filename">Filename to get hashes for/param>
private static void WriteHashes(string filename)
{
// If the file doesn't exist, don't try anything
if (!File.Exists(filename))
return;
// Get the hash string from the file
string? hashString = HashingHelper.GetInfo(filename);
if (hashString == null)
return;
// Open the output file and write the hashes
using var fs = File.Create(Path.GetFullPath(filename) + ".hash");
using var sw = new StreamWriter(fs);
sw.WriteLine(hashString);
return commandSet;
}
}
}

View File

@@ -49,18 +49,18 @@ echo " No archive (-a) $NO_ARCHIVE"
echo " "
# Create the build matrix arrays
FRAMEWORKS=("net9.0")
FRAMEWORKS=("net10.0")
RUNTIMES=("win-x86" "win-x64" "win-arm64" "linux-x64" "linux-arm64" "osx-x64" "osx-arm64")
# Use expanded lists, if requested
if [ $USE_ALL = true ]; then
FRAMEWORKS=("net20" "net35" "net40" "net452" "net462" "net472" "net48" "netcoreapp3.1" "net5.0" "net6.0" "net7.0" "net8.0" "net9.0")
FRAMEWORKS=("net20" "net35" "net40" "net452" "net462" "net472" "net48" "netcoreapp3.1" "net5.0" "net6.0" "net7.0" "net8.0" "net9.0" "net10.0")
fi
# Create the filter arrays
SINGLE_FILE_CAPABLE=("net5.0" "net6.0" "net7.0" "net8.0" "net9.0")
VALID_APPLE_FRAMEWORKS=("net6.0" "net7.0" "net8.0" "net9.0")
VALID_CROSS_PLATFORM_FRAMEWORKS=("netcoreapp3.1" "net5.0" "net6.0" "net7.0" "net8.0" "net9.0")
SINGLE_FILE_CAPABLE=("net5.0" "net6.0" "net7.0" "net8.0" "net9.0" "net10.0")
VALID_APPLE_FRAMEWORKS=("net6.0" "net7.0" "net8.0" "net9.0" "net10.0")
VALID_CROSS_PLATFORM_FRAMEWORKS=("netcoreapp3.1" "net5.0" "net6.0" "net7.0" "net8.0" "net9.0" "net10.0")
VALID_CROSS_PLATFORM_RUNTIMES=("win-arm64" "linux-x64" "linux-arm64" "osx-x64" "osx-arm64")
# Only build if requested

View File

@@ -40,18 +40,18 @@ Write-Host " No archive (-NoArchive) $NO_ARCHIVE"
Write-Host " "
# Create the build matrix arrays
$FRAMEWORKS = @('net9.0')
$FRAMEWORKS = @('net10.0')
$RUNTIMES = @('win-x86', 'win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')
# Use expanded lists, if requested
if ($USE_ALL.IsPresent) {
$FRAMEWORKS = @('net20', 'net35', 'net40', 'net452', 'net462', 'net472', 'net48', 'netcoreapp3.1', 'net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0')
$FRAMEWORKS = @('net20', 'net35', 'net40', 'net452', 'net462', 'net472', 'net48', 'netcoreapp3.1', 'net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0')
}
# Create the filter arrays
$SINGLE_FILE_CAPABLE = @('net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0')
$VALID_APPLE_FRAMEWORKS = @('net6.0', 'net7.0', 'net8.0', 'net9.0')
$VALID_CROSS_PLATFORM_FRAMEWORKS = @('netcoreapp3.1', 'net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0')
$SINGLE_FILE_CAPABLE = @('net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0')
$VALID_APPLE_FRAMEWORKS = @('net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0')
$VALID_CROSS_PLATFORM_FRAMEWORKS = @('netcoreapp3.1', 'net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0')
$VALID_CROSS_PLATFORM_RUNTIMES = @('win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')
# Only build if requested