47 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
Matt Nadareski
415b3f17c1 Bump version 2025-09-19 11:03:00 -04:00
Matt Nadareski
06d52e04c5 Fix hashing output 2025-09-19 11:02:34 -04:00
Matt Nadareski
a26e8b260c Bump version 2025-09-17 22:51:28 -04:00
Matt Nadareski
6c2edd225d Fix the error statement too 2025-09-17 22:44:54 -04:00
Matt Nadareski
1fa9345f06 InputPaths are not args 2025-09-17 22:43:47 -04:00
Matt Nadareski
e342448599 Bump version 2025-09-07 08:39:18 -04:00
Matt Nadareski
27431e05de Stop reinventing the wheel again 2025-09-06 19:41:39 -04:00
Matt Nadareski
56f8869d1d Be consistent, for once 2025-09-06 19:38:52 -04:00
Matt Nadareski
9e9efaf491 Always overwrite 2025-09-06 19:35:51 -04:00
Matt Nadareski
e8a0d706de Simplest solution to start 2025-09-06 19:29:26 -04:00
Matt Nadareski
9eb49d170f Add basically unused option 2025-09-06 18:19:19 -04:00
Matt Nadareski
c31f95b85d Just pass the options 2025-09-06 18:16:40 -04:00
Matt Nadareski
dc1952d6f9 Be less vague, kinda 2025-09-06 18:15:34 -04:00
Matt Nadareski
6ae797a6eb Copy inputs to outputs for processing, optionally 2025-09-06 18:14:39 -04:00
Matt Nadareski
ed8e97aa0c More prep for a better future 2025-09-06 18:09:54 -04:00
Matt Nadareski
cc16770126 Start prepping for things 2025-09-06 18:04:24 -04:00
Matt Nadareski
8c186f969c Clear up them there wording 2025-09-06 17:56:05 -04:00
Matt Nadareski
ac542076aa Forgot to remove another helper 2025-09-06 17:48:01 -04:00
Matt Nadareski
94bebddda2 Goodbye old formats, I won't miss you 2025-09-06 17:45:17 -04:00
Matt Nadareski
8c66c40b48 Use separate Options class like nearly every other reference implementation 2025-09-06 17:34:23 -04:00
Matt Nadareski
1fbacaffb0 Take advantage of Wrapper functions for NDS 2025-09-06 16:07:37 -04:00
Matt Nadareski
0448682934 Add .NET Standard 2.x support 2025-09-06 15:51:44 -04:00
Matt Nadareski
9e563785c9 Update Nuget packages 2025-09-06 15:51:11 -04:00
Matt Nadareski
7469c97c6b Use existing IO extension 2025-07-28 09:36:52 -04:00
Matt Nadareski
7c909af154 Update IO to 1.6.3 2025-05-12 08:25:57 -04:00
23 changed files with 1014 additions and 1894 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,21 +1,15 @@
using System;
using System.IO;
using System.Text;
#if NETFRAMEWORK || NETSTANDARD2_0_OR_GREATER
using SabreTools.IO.Extensions;
using SabreTools.Models.Nitro;
using SabreTools.Serialization.Deserializers;
#endif
using SabreTools.Serialization.Wrappers;
namespace NDecrypt.Core
{
public class DSTool : ITool
{
#region Encryption process variables
private uint[] _cardHash = new uint[0x412];
private uint[] _arg2 = new uint[3];
#endregion
/// <summary>
/// Decryption args to use while processing
/// </summary>
@@ -29,302 +23,105 @@ namespace NDecrypt.Core
#region Encrypt
/// <inheritdoc/>
public bool EncryptFile(string filename, bool force)
public bool EncryptFile(string input, string? output, bool force)
{
try
{
// Open the read and write on the same file for inplace processing
using var reader = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// If the output is provided, copy the input file
if (output is not null)
File.Copy(input, output, overwrite: true);
else
output = input;
// Open the output file for processing
using var reader = File.Open(output, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Deserialize the cart information
var commonHeader = Nitro.ParseCommonHeader(reader);
if (commonHeader == null)
var nitro = Nitro.Create(reader);
if (nitro is null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
}
// Reset state variables
_cardHash = new uint[0x412];
_arg2 = new uint[3];
// Ensure the secure area was read
if (nitro.SecureArea is null)
{
Console.WriteLine("Error: Invalid secure area!");
return false;
}
// Encrypt the secure area
EncryptSecureArea(commonHeader, force, reader, writer);
nitro.EncryptSecureArea(_decryptArgs.NitroEncryptionData, force);
// Write the encrypted secure area
using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
writer.Seek(0x4000, SeekOrigin.Begin);
writer.Write(nitro.SecureArea);
writer.Flush();
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine($"An error has occurred. {output} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid DS or DSi file and try again.");
return false;
}
}
/// <summary>
/// Encrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="commonHeader">CommonHeader representing the DS file header</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void EncryptSecureArea(CommonHeader commonHeader, bool force, Stream reader, Stream writer)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
// If we're not forcing the operation, check to see if we should be proceeding
else
{
bool? isDecrypted = CheckIfDecrypted(reader);
if (isDecrypted == null)
{
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (!isDecrypted.Value)
{
Console.WriteLine("File is already encrypted");
return;
}
}
EncryptARM9(commonHeader, reader, writer);
Console.WriteLine("File has been encrypted");
}
/// <summary>
/// Encrypt the secure ARM9 region of the file, if possible
/// </summary>
/// <param name="commonHeader">CommonHeader representing the DS header</param>
/// <param name="encrypt">Indicates if the file should be encrypted or decrypted</param>
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void EncryptARM9(CommonHeader commonHeader, Stream reader, Stream writer)
{
// Seek to the beginning of the secure area
reader.Seek(0x4000, SeekOrigin.Begin);
writer.Seek(0x4000, SeekOrigin.Begin);
// Grab the first two blocks
uint p0 = reader.ReadUInt32();
uint p1 = reader.ReadUInt32();
// Perform the initialization steps
Init1(commonHeader);
_arg2[1] <<= 1;
_arg2[2] >>= 1;
Init2();
// Ensure alignment
reader.Seek(0x4008, SeekOrigin.Begin);
writer.Seek(0x4008, SeekOrigin.Begin);
// Loop throgh the main encryption step
uint size = 0x800 - 8;
while (size > 0)
{
p0 = reader.ReadUInt32();
p1 = reader.ReadUInt32();
Encrypt(ref p1, ref p0);
writer.Write(p0);
writer.Write(p1);
size -= 8;
}
// Replace the header explicitly
reader.Seek(0x4000, SeekOrigin.Begin);
writer.Seek(0x4000, SeekOrigin.Begin);
p0 = reader.ReadUInt32();
p1 = reader.ReadUInt32();
if (p0 == 0xE7FFDEFF && p1 == 0xE7FFDEFF)
{
p0 = Constants.MAGIC30;
p1 = Constants.MAGIC34;
}
Encrypt(ref p1, ref p0);
Init1(commonHeader);
Encrypt(ref p1, ref p0);
writer.Write(p0);
writer.Write(p1);
}
/// <summary>
/// Perform an encryption step
/// </summary>
/// <param name="arg1">First unsigned value to use in encryption</param>
/// <param name="arg2">Second unsigned value to use in encryption</param>
private void Encrypt(ref uint arg1, ref uint arg2)
{
uint a = arg1;
uint b = arg2;
for (int i = 0; i < 16; i++)
{
uint c = _cardHash[i] ^ a;
a = b ^ Lookup(c);
b = c;
}
arg2 = a ^ _cardHash[16];
arg1 = b ^ _cardHash[17];
}
#endregion
#region Decrypt
/// <inheritdoc/>
public bool DecryptFile(string filename, bool force)
public bool DecryptFile(string input, string? output, bool force)
{
try
{
// If the output is provided, copy the input file
if (output is not null)
File.Copy(input, output, overwrite: true);
else
output = input;
// Open the read and write on the same file for inplace processing
using var reader = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
using var reader = File.Open(output, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Deserialize the cart information
var commonHeader = Nitro.ParseCommonHeader(reader);
if (commonHeader == null)
var nitro = Nitro.Create(reader);
if (nitro is null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
}
// Reset state variables
_cardHash = new uint[0x412];
_arg2 = new uint[3];
// Ensure the secure area was read
if (nitro.SecureArea is null)
{
Console.WriteLine("Error: Invalid secure area!");
return false;
}
// Decrypt the secure area
DecryptSecureArea(commonHeader, force, reader, writer);
nitro.DecryptSecureArea(_decryptArgs.NitroEncryptionData, force);
// Write the decrypted secure area
using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
writer.Seek(0x4000, SeekOrigin.Begin);
writer.Write(nitro.SecureArea);
writer.Flush();
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine($"An error has occurred. {output} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid DS or DSi file and try again.");
return false;
}
}
/// <summary>
/// Decrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="commonHeader">CommonHeader representing the DS file header</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void DecryptSecureArea(CommonHeader commonHeader, bool force, Stream reader, Stream writer)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
// If we're not forcing the operation, check to see if we should be proceeding
else
{
bool? isDecrypted = CheckIfDecrypted(reader);
if (isDecrypted == null)
{
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (isDecrypted.Value)
{
Console.WriteLine("File is already decrypted");
return;
}
}
DecryptARM9(commonHeader, reader, writer);
Console.WriteLine("File has been decrypted");
}
/// <summary>
/// Decrypt the secure ARM9 region of the file, if possible
/// </summary>
/// <param name="commonHeader">CommonHeader representing the DS header</param>
/// <param name="">Indicates if the file should be encrypted or decrypted</param>
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void DecryptARM9(CommonHeader commonHeader, Stream reader, Stream writer)
{
// Seek to the beginning of the secure area
reader.Seek(0x4000, SeekOrigin.Begin);
writer.Seek(0x4000, SeekOrigin.Begin);
// Grab the first two blocks
uint p0 = reader.ReadUInt32();
uint p1 = reader.ReadUInt32();
// Perform the initialization steps
Init1(commonHeader);
Decrypt(ref p1, ref p0);
_arg2[1] <<= 1;
_arg2[2] >>= 1;
Init2();
// Set the proper flags
Decrypt(ref p1, ref p0);
if (p0 == Constants.MAGIC30 && p1 == Constants.MAGIC34)
{
p0 = 0xE7FFDEFF;
p1 = 0xE7FFDEFF;
}
writer.Write(p0);
writer.Write(p1);
// Ensure alignment
reader.Seek(0x4008, SeekOrigin.Begin);
writer.Seek(0x4008, SeekOrigin.Begin);
// Loop throgh the main encryption step
uint size = 0x800 - 8;
while (size > 0)
{
p0 = reader.ReadUInt32();
p1 = reader.ReadUInt32();
Decrypt(ref p1, ref p0);
writer.Write(p0);
writer.Write(p1);
size -= 8;
}
}
/// <summary>
/// Perform a decryption step
/// </summary>
/// <param name="arg1">First unsigned value to use in decryption</param>
/// <param name="arg2">Second unsigned value to use in decryption</param>
private void Decrypt(ref uint arg1, ref uint arg2)
{
uint a = arg1;
uint b = arg2;
for (int i = 17; i > 1; i--)
{
uint c = _cardHash[i] ^ a;
a = b ^ Lookup(c);
b = c;
}
arg1 = b ^ _cardHash[0];
arg2 = a ^ _cardHash[1];
}
#endregion
#region Info
@@ -337,13 +134,18 @@ namespace NDecrypt.Core
// Open the file for reading
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Deserialize the cart information
var cart = Nitro.Create(input);
if (cart?.Model is null)
return "Error: Not a DS/DSi cart image!";
// Get a string builder for the status
var sb = new StringBuilder();
sb.Append("\tSecure Area: ");
// Get the encryption status
bool? decrypted = CheckIfDecrypted(input);
if (decrypted == null)
bool? decrypted = cart.CheckIfDecrypted(out _);
if (decrypted is null)
sb.Append("Empty");
else if (decrypted == true)
sb.Append("Decrypted");
@@ -362,181 +164,5 @@ namespace NDecrypt.Core
}
#endregion
#region Common
/// <summary>
/// Determine if the current file is already decrypted or not (or has an empty secure area)
/// </summary>
/// <param name="reader">Stream representing the input</param>
/// <returns>True if the file has known values for a decrypted file, null if it's empty, false otherwise</returns>
private static bool? CheckIfDecrypted(Stream reader)
{
reader.Seek(0x4000, SeekOrigin.Begin);
uint firstValue = reader.ReadUInt32();
uint secondValue = reader.ReadUInt32();
// Empty secure area standard
if (firstValue == 0x00000000 && secondValue == 0x00000000)
{
Console.WriteLine("Empty secure area found. Cannot be encrypted or decrypted.");
return null;
}
// Improperly decrypted empty secure area (decrypt empty with woodsec)
else if ((firstValue == 0xE386C397 && secondValue == 0x82775B7E)
|| (firstValue == 0xF98415B8 && secondValue == 0x698068FC)
|| (firstValue == 0xA71329EE && secondValue == 0x2A1D4C38)
|| (firstValue == 0xC44DCC48 && secondValue == 0x38B6F8CB)
|| (firstValue == 0x3A9323B5 && secondValue == 0xC0387241))
{
Console.WriteLine("Improperly decrypted empty secure area found. Should be encrypted to get proper value.");
return true;
}
// Improperly encrypted empty secure area (encrypt empty with woodsec)
else if ((firstValue == 0x4BCE88BE && secondValue == 0xD3662DD1)
|| (firstValue == 0x2543C534 && secondValue == 0xCC4BE38E))
{
Console.WriteLine("Improperly encrypted empty secure area found. Should be decrypted to get proper value.");
return false;
}
// Properly decrypted nonstandard value (mastering issue)
else if ((firstValue == 0xD0D48B67 && secondValue == 0x39392F23) // Dragon Quest 5 (EU)
|| (firstValue == 0x014A191A && secondValue == 0xA5C470B9) // Dragon Quest 5 (USA)
|| (firstValue == 0x7829BC8D && secondValue == 0x9968EF44) // Dragon Quest 5 (JP)
|| (firstValue == 0xC4A15AB8 && secondValue == 0xD2E667C8) // Prince of Persia (EU)
|| (firstValue == 0xD5E97D20 && secondValue == 0x21B2A159)) // Prince of Persia (USA)
{
Console.WriteLine("Decrypted secure area for known, nonstandard value found.");
return true;
}
// Properly decrypted prototype value
else if (firstValue == 0xBA35F813 && secondValue == 0xB691AAE8)
{
Console.WriteLine("Decrypted secure area for prototype found.");
return true;
}
// Strange, unlicenced values that can't determine decryption state
else if ((firstValue == 0xE1D830D8 && secondValue == 0xE3530000) // Aquela Ball (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xDC002A02 && secondValue == 0x2900E612) // Bahlz (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03BA3 && secondValue == 0xE2011CFF) // Battle Ship (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3A01001 && secondValue == 0xE1A02001) // Breakout!! DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE793200C && secondValue == 0xE4812004) // Bubble Fusion (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE583C0DC && secondValue == 0x0A00000B) // Carre Rouge (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x0202453C && secondValue == 0x02060164) // ChainReaction (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xEBFFF218 && secondValue == 0xE31000FF) // Collection (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x4A6CD003 && secondValue == 0x425B2301) // DiggerDS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3A00001 && secondValue == 0xEBFFFF8C) // Double Skill (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x21043701 && secondValue == 0x45BA448C) // DSChess (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE59D0010 && secondValue == 0xE0833000) // Hexa-Virus (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE5C3A006 && secondValue == 0xE5C39007) // Invasion (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1D920F4 && secondValue == 0xE06A3000) // JoggleDS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE59F32EC && secondValue == 0xE5DD7011) // London Underground (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE08A3503 && secondValue == 0xE1D3C4B8) // NumberMinds (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A0C001 && secondValue == 0xE0031001) // Paddle Battle (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03005 && secondValue == 0xE88D0180) // Pop the Balls (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE8BD4030 && secondValue == 0xE12FFF1E) // Solitaire DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE0A88006 && secondValue == 0xE1A00003) // Squash DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE51F3478 && secondValue == 0xEB004A02) // Super Snake DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x1C200052 && secondValue == 0xFD12F013) // Tales of Dagur (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x601F491E && secondValue == 0x041B880B) // Tetris & Touch (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03843 && secondValue == 0xE0000293) // Tic Tac Toe (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3530000 && secondValue == 0x13A03003) // Warrior Training (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x02054A80 && secondValue == 0x02054B80)) // Zi (World) (Unl) (Datel Games n' Music)
{
Console.WriteLine("Unlicensed invalid value found. Unknown if encrypted or decrypted.");
return null;
}
// Standard decryption values
return firstValue == 0xE7FFDEFF && secondValue == 0xE7FFDEFF;
}
/// <summary>
/// First common initialization step
/// </summary>
/// <param name="commonHeader">CommonHeader representing the DS file</param>
private void Init1(CommonHeader commonHeader)
{
Buffer.BlockCopy(_decryptArgs.NitroEncryptionData, 0, _cardHash, 0, 4 * (1024 + 18));
_arg2 = [commonHeader.GameCode, commonHeader.GameCode >> 1, commonHeader.GameCode << 1];
Init2();
Init2();
}
/// <summary>
/// Second common initialization step
/// </summary>
private void Init2()
{
Encrypt(ref _arg2[2], ref _arg2[1]);
Encrypt(ref _arg2[1], ref _arg2[0]);
byte[] allBytes =[.. BitConverter.GetBytes(_arg2[0]),
.. BitConverter.GetBytes(_arg2[1]),
.. BitConverter.GetBytes(_arg2[2])];
UpdateHashtable(allBytes);
}
/// <summary>
/// Lookup the value from the hashtable
/// </summary>
/// <param name="v">Value to lookup in the hashtable</param>
/// <returns>Processed value through the hashtable</returns>
private uint Lookup(uint v)
{
uint a = (v >> 24) & 0xFF;
uint b = (v >> 16) & 0xFF;
uint c = (v >> 8) & 0xFF;
uint d = (v >> 0) & 0xFF;
a = _cardHash[a + 18 + 0];
b = _cardHash[b + 18 + 256];
c = _cardHash[c + 18 + 512];
d = _cardHash[d + 18 + 768];
return d + (c ^ (b + a));
}
/// <summary>
/// Update the hashtable
/// </summary>
/// <param name="arg1">Value to update the hashtable with</param>
private void UpdateHashtable(byte[] arg1)
{
for (int j = 0; j < 18; j++)
{
uint r3 = 0;
for (int i = 0; i < 4; i++)
{
r3 <<= 8;
r3 |= arg1[(j * 4 + i) & 7];
}
_cardHash[j] ^= r3;
}
uint tmp1 = 0;
uint tmp2 = 0;
for (int i = 0; i < 18; i += 2)
{
Encrypt(ref tmp1, ref tmp2);
_cardHash[i + 0] = tmp1;
_cardHash[i + 1] = tmp2;
}
for (int i = 0; i < 0x400; i += 2)
{
Encrypt(ref tmp1, ref tmp2);
_cardHash[i + 18 + 0] = tmp1;
_cardHash[i + 18 + 1] = tmp2;
}
}
#endregion
}
}

View File

@@ -1,8 +1,8 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.IO.Encryption;
using SabreTools.IO.Extensions;
using SabreTools.IO.Readers;
using SabreTools.Matching;
namespace NDecrypt.Core
{
@@ -24,273 +24,6 @@ namespace NDecrypt.Core
/// </summary>
public byte[] NitroEncryptionData { get; private set; } = [];
/// <summary>
/// Encryption data taken from woodsec
/// </summary>
private static readonly byte[] _nitroEncryptionData = [
0x99,0xD5,0x20,0x5F,0x57,0x44,0xF5,0xB9,0x6E,0x19,0xA4,0xD9,0x9E,0x6A,0x5A,0x94,
0xD8,0xAE,0xF1,0xEB,0x41,0x75,0xE2,0x3A,0x93,0x82,0xD0,0x32,0x33,0xEE,0x31,0xD5,
0xCC,0x57,0x61,0x9A,0x37,0x06,0xA2,0x1B,0x79,0x39,0x72,0xF5,0x55,0xAE,0xF6,0xBE,
0x5F,0x1B,0x69,0xFB,0xE5,0x9D,0xF1,0xE9,0xCE,0x2C,0xD9,0xA1,0x5E,0x32,0x05,0xE6,
0xFE,0xD3,0xFE,0xCF,0xD4,0x62,0x04,0x0D,0x8B,0xF5,0xEC,0xB7,0x2B,0x60,0x79,0xBB,
0x12,0x95,0x31,0x0D,0x6E,0x3F,0xDA,0x2B,0x88,0x84,0xF0,0xF1,0x3D,0x12,0x7E,0x25,
0x45,0x22,0xF1,0xBB,0x24,0x06,0x1A,0x06,0x11,0xAD,0xDF,0x28,0x8B,0x64,0x81,0x34,
0x2B,0xEB,0x33,0x29,0x99,0xAA,0xF2,0xBD,0x9C,0x14,0x95,0x9D,0x9F,0xF7,0xF5,0x8C,
0x72,0x97,0xA1,0x29,0x9D,0xD1,0x5F,0xCF,0x66,0x4D,0x07,0x1A,0xDE,0xD3,0x4A,0x4B,
0x85,0xC9,0xA7,0xA3,0x17,0x95,0x05,0x3A,0x3D,0x49,0x0A,0xBF,0x0A,0x89,0x8B,0xA2,
0x4A,0x82,0x49,0xDD,0x27,0x90,0xF1,0x0B,0xE9,0xEB,0x1C,0x6A,0x83,0x76,0x45,0x05,
0xBA,0x81,0x70,0x61,0x17,0x3F,0x4B,0xDE,0xAE,0xCF,0xAB,0x39,0x57,0xF2,0x3A,0x56,
0x48,0x11,0xAD,0x8A,0x40,0xE1,0x45,0x3F,0xFA,0x9B,0x02,0x54,0xCA,0xA6,0x93,0xFB,
0xEF,0x4D,0xFE,0x6F,0xA3,0xD8,0x87,0x9C,0x08,0xBA,0xD5,0x48,0x6A,0x8D,0x2D,0xFD,
0x6E,0x15,0xF8,0x74,0xBD,0xBE,0x52,0x8B,0x18,0x22,0x8A,0x9E,0xFB,0x74,0x37,0x07,
0x1B,0x36,0x6C,0x4A,0x19,0xBA,0x42,0x62,0xB9,0x79,0x91,0x10,0x7B,0x67,0x65,0x96,
0xFE,0x02,0x23,0xE8,0xEE,0x99,0x8C,0x77,0x3E,0x5C,0x86,0x64,0x4D,0x6D,0x78,0x86,
0xA5,0x4F,0x65,0xE2,0x1E,0xB2,0xDF,0x5A,0x0A,0xD0,0x7E,0x08,0x14,0xB0,0x71,0xAC,
0xBD,0xDB,0x83,0x1C,0xB9,0xD7,0xA1,0x62,0xCD,0xC6,0x63,0x7C,0x52,0x69,0xC3,0xE6,
0xBF,0x75,0xCE,0x12,0x44,0x5D,0x21,0x04,0xFA,0xFB,0xD3,0x3C,0x38,0x11,0x63,0xD4,
0x95,0x85,0x41,0x49,0x46,0x09,0xF2,0x08,0x43,0x11,0xDC,0x1F,0x76,0xC0,0x15,0x6D,
0x1F,0x3C,0x63,0x70,0xEA,0x87,0x80,0x6C,0xC3,0xBD,0x63,0x8B,0xC2,0x37,0x21,0x37,
0xDC,0xEE,0x09,0x23,0x2E,0x37,0x6A,0x4D,0x73,0x90,0xF7,0x50,0x30,0xAC,0x1C,0x92,
0x04,0x10,0x23,0x91,0x4F,0xD2,0x07,0xAA,0x68,0x3E,0x4F,0x9A,0xC9,0x64,0x60,0x6A,
0xC8,0x14,0x21,0xF3,0xD6,0x22,0x41,0x12,0x44,0x24,0xCF,0xE6,0x8A,0x56,0xDD,0x0D,
0x53,0x4D,0xE1,0x85,0x1E,0x8C,0x52,0x5A,0x9C,0x19,0x84,0xC2,0x03,0x57,0xF1,0x6F,
0xE3,0x00,0xBE,0x58,0xF6,0x4C,0xED,0xD5,0x21,0x64,0x9C,0x1F,0xBE,0x55,0x03,0x3C,
0x4A,0xDC,0xFF,0xAA,0xC9,0xDA,0xE0,0x5D,0x5E,0xBF,0xE6,0xDE,0xF5,0xD8,0xB1,0xF8,
0xFF,0x36,0xB3,0xB9,0x62,0x67,0x95,0xDB,0x31,0x5F,0x37,0xED,0x4C,0x70,0x67,0x99,
0x90,0xB5,0x18,0x31,0x6C,0x3D,0x99,0x99,0xE4,0x42,0xDA,0xD3,0x25,0x42,0x13,0xA0,
0xAE,0xD7,0x70,0x6C,0xB1,0x55,0xCF,0xC7,0xD7,0x46,0xD5,0x43,0x61,0x17,0x3D,0x44,
0x28,0xE9,0x33,0x85,0xD5,0xD0,0xA2,0x93,0xAA,0x25,0x12,0x1F,0xFB,0xC5,0x0B,0x46,
0xF5,0x97,0x76,0x56,0x45,0xA6,0xBE,0x87,0xB1,0x94,0x6B,0xE8,0xB1,0xFE,0x33,0x99,
0xAE,0x1F,0x3E,0x6C,0x39,0x71,0x1D,0x09,0x00,0x90,0x37,0xE4,0x10,0x3E,0x75,0x74,
0xFF,0x8C,0x83,0x3B,0xB0,0xF1,0xB0,0xF9,0x01,0x05,0x47,0x42,0x95,0xF1,0xD6,0xAC,
0x7E,0x38,0xE6,0x9E,0x95,0x74,0x26,0x3F,0xB4,0x68,0x50,0x18,0xD0,0x43,0x30,0xB4,
0x4C,0x4B,0xE3,0x68,0xBF,0xE5,0x4D,0xB6,0x95,0x8B,0x0A,0xA0,0x74,0x25,0x32,0x77,
0xCF,0xA1,0xF7,0x2C,0xD8,0x71,0x13,0x5A,0xAB,0xEA,0xC9,0x51,0xE8,0x0D,0xEE,0xEF,
0xE9,0x93,0x7E,0x19,0xA7,0x1E,0x43,0x38,0x81,0x16,0x2C,0xA1,0x48,0xE3,0x73,0xCC,
0x29,0x21,0x6C,0xD3,0x5D,0xCE,0xA0,0xD9,0x61,0x71,0x43,0xA0,0x15,0x13,0xB5,0x64,
0x92,0xCF,0x2A,0x19,0xDC,0xAD,0xB7,0xA5,0x9F,0x86,0x65,0xF8,0x1A,0x9F,0xE7,0xFB,
0xF7,0xFD,0xB8,0x13,0x6C,0x27,0xDB,0x6F,0xDF,0x35,0x1C,0xF7,0x8D,0x2C,0x5B,0x9B,
0x12,0xAB,0x38,0x64,0x06,0xCC,0xDE,0x31,0xE8,0x4E,0x75,0x11,0x64,0xE3,0xFA,0xEA,
0xEB,0x34,0x54,0xC2,0xAD,0x3F,0x34,0xEB,0x93,0x2C,0x7D,0x26,0x36,0x9D,0x56,0xF3,
0x5A,0xE1,0xF6,0xB3,0x98,0x63,0x4A,0x9E,0x32,0x83,0xE4,0x9A,0x84,0x60,0x7D,0x90,
0x2E,0x13,0x0E,0xEE,0x93,0x4B,0x36,0xA2,0x85,0xEC,0x16,0x38,0xE8,0x88,0x06,0x02,
0xBF,0xF0,0xA0,0x3A,0xED,0xD7,0x6A,0x9A,0x73,0xE1,0x57,0xCF,0xF8,0x44,0xB8,0xDC,
0x2E,0x23,0x59,0xD1,0xDF,0x95,0x52,0x71,0x99,0x61,0xA0,0x4B,0xD5,0x7F,0x6E,0x78,
0xBA,0xA9,0xC5,0x30,0xD3,0x40,0x86,0x32,0x9D,0x32,0x0C,0x9C,0x37,0xB7,0x02,0x2F,
0xBA,0x54,0x98,0xA9,0xC4,0x13,0x04,0xC9,0x8D,0xBE,0xC8,0xE7,0x5D,0x97,0x50,0x2E,
0x93,0xD6,0x22,0x59,0x0C,0x27,0xBC,0x22,0x92,0xE0,0xA7,0x20,0x0F,0x93,0x6F,0x7F,
0x4C,0x9F,0xD3,0xB5,0xA6,0x2A,0x0B,0x74,0x67,0x49,0x7D,0x10,0x26,0xCB,0xD1,0xC5,
0x86,0x71,0xE7,0x8C,0xA0,0x9C,0xE9,0x5B,0xB2,0x1A,0xF6,0x01,0xEE,0x8C,0x9E,0x5E,
0x83,0xF2,0x1A,0xDB,0xE6,0xE5,0xEA,0x84,0x59,0x76,0xD2,0x7C,0xF6,0x8D,0xA5,0x49,
0x36,0x48,0xC2,0x16,0x52,0xBB,0x83,0xA3,0x74,0xB9,0x07,0x0C,0x3B,0xFF,0x61,0x28,
0xE1,0x61,0xE9,0xE4,0xEF,0x6E,0x15,0xAA,0x4E,0xBA,0xE8,0x5D,0x05,0x96,0xBB,0x32,
0x56,0xB0,0xFB,0x72,0x52,0x0F,0x0E,0xC8,0x42,0x25,0x65,0x76,0x89,0xAF,0xF2,0xDE,
0x10,0x27,0xF0,0x01,0x4B,0x74,0xA7,0x97,0x07,0xD5,0x26,0x54,0x54,0x09,0x1F,0x82,
0x0A,0x86,0x7D,0x30,0x39,0x0E,0xB3,0x26,0x9B,0x0B,0x57,0xBB,0x36,0x06,0x31,0xAF,
0xFD,0x79,0xFC,0xD9,0x30,0x10,0x2B,0x0C,0xB3,0xE1,0x9B,0xD7,0x7B,0xDC,0x5F,0xEF,
0xD2,0xF8,0x13,0x45,0x4D,0x47,0x75,0xBD,0x46,0x96,0x3C,0x7E,0x75,0xF3,0x3E,0xB5,
0x67,0xC5,0x9A,0x3B,0xB0,0x5B,0x29,0x6B,0xDE,0x80,0x5B,0xC8,0x15,0x05,0xB1,0x31,
0xB6,0xCE,0x49,0xDD,0xAD,0x84,0xB5,0xAE,0x60,0xDC,0x67,0x31,0x34,0x30,0xFE,0x4E,
0xBD,0x80,0x2F,0xA6,0xBF,0x63,0x39,0x21,0x86,0xD9,0x35,0x7F,0x16,0x68,0x22,0x05,
0x54,0xE9,0x90,0x26,0x8C,0x07,0x6C,0x51,0xA4,0x31,0x55,0xD7,0x09,0x07,0xA8,0x3E,
0x2E,0x53,0x66,0xC1,0xF8,0xF2,0x7B,0xC4,0xF2,0x58,0xCF,0xF1,0x87,0xC5,0xA2,0xE7,
0x27,0x8F,0x30,0x87,0x58,0xA0,0x64,0x62,0x23,0x18,0xB9,0x88,0x7C,0xFA,0xCE,0xC4,
0x98,0xAE,0xAD,0x17,0xCC,0x4A,0x5B,0xF3,0xE9,0x48,0xD5,0x56,0xD3,0x0D,0xF2,0xC8,
0x92,0x73,0x8C,0xDB,0xD7,0x2F,0x56,0xAC,0x81,0xF9,0x92,0x69,0x4D,0xC6,0x32,0xF6,
0xE6,0xC0,0x8D,0x21,0xE2,0x76,0x80,0x61,0x11,0xBC,0xDC,0x6C,0x93,0xAF,0x19,0x69,
0x9B,0xD0,0xBF,0xB9,0x31,0x9F,0x02,0x67,0xA3,0x51,0xEE,0x83,0x06,0x22,0x7B,0x0C,
0xAB,0x49,0x42,0x40,0xB8,0xD5,0x01,0x7D,0xCE,0x5E,0xF7,0x55,0x53,0x39,0xC5,0x99,
0x46,0xD8,0x87,0x9F,0xBA,0xF7,0x64,0xB4,0xE3,0x9A,0xFA,0xA1,0x6D,0x90,0x68,0x10,
0x30,0xCA,0x8A,0x54,0xA7,0x9F,0x60,0xC3,0x19,0xF5,0x6B,0x0D,0x7A,0x51,0x98,0xE6,
0x98,0x43,0x51,0xB4,0xD6,0x35,0xE9,0x4F,0xC3,0xDF,0x0F,0x7B,0xD6,0x2F,0x5C,0xBD,
0x3A,0x15,0x61,0x19,0xF1,0x4B,0xCB,0xAA,0xDC,0x6D,0x64,0xC9,0xD3,0xC6,0x1E,0x56,
0xEF,0x38,0x4C,0x50,0x71,0x86,0x75,0xCC,0x0D,0x0D,0x4E,0xE9,0x28,0xF6,0x06,0x5D,
0x70,0x1B,0xAA,0xD3,0x45,0xCF,0xA8,0x39,0xAC,0x95,0xA6,0x2E,0xB4,0xE4,0x22,0xD4,
0x74,0xA8,0x37,0x5F,0x48,0x7A,0x04,0xCC,0xA5,0x4C,0x40,0xD8,0x28,0xB4,0x28,0x08,
0x0D,0x1C,0x72,0x52,0x41,0xF0,0x7D,0x47,0x19,0x3A,0x53,0x4E,0x58,0x84,0x62,0x6B,
0x93,0xB5,0x8A,0x81,0x21,0x4E,0x0D,0xDC,0xB4,0x3F,0xA2,0xC6,0xFC,0xC9,0x2B,0x40,
0xDA,0x38,0x04,0xE9,0x5E,0x5A,0x86,0x6B,0x0C,0x22,0x25,0x85,0x68,0x11,0x8D,0x7C,
0x92,0x1D,0x95,0x55,0x4D,0xAB,0x8E,0xBB,0xDA,0xA6,0xE6,0xB7,0x51,0xB6,0x32,0x5A,
0x05,0x41,0xDD,0x05,0x2A,0x0A,0x56,0x50,0x91,0x17,0x47,0xCC,0xC9,0xE6,0x7E,0xB5,
0x61,0x4A,0xDB,0x73,0x67,0x51,0xC8,0x33,0xF5,0xDA,0x6E,0x74,0x2E,0x54,0xC3,0x37,
0x0D,0x6D,0xAF,0x08,0xE8,0x15,0x8A,0x5F,0xE2,0x59,0x21,0xCD,0xA8,0xDE,0x0C,0x06,
0x5A,0x77,0x6B,0x5F,0xDB,0x18,0x65,0x3E,0xC8,0x50,0xDE,0x78,0xE0,0xB8,0x82,0xB3,
0x5D,0x4E,0x72,0x32,0x07,0x4F,0xC1,0x34,0x23,0xBA,0x96,0xB7,0x67,0x4E,0xA4,0x28,
0x1E,0x34,0x62,0xEB,0x2D,0x6A,0x70,0xE9,0x2F,0x42,0xC4,0x70,0x4E,0x5A,0x31,0x9C,
0xF9,0x5B,0x47,0x28,0xAA,0xDA,0x71,0x6F,0x38,0x1F,0xB3,0x78,0xC4,0x92,0x6B,0x1C,
0x9E,0xF6,0x35,0x9A,0xB7,0x4D,0x0E,0xBF,0xCC,0x18,0x29,0x41,0x03,0x48,0x35,0x5D,
0x55,0xD0,0x2B,0xC6,0x29,0xAF,0x5C,0x60,0x74,0x69,0x8E,0x5E,0x9B,0x7C,0xD4,0xBD,
0x7B,0x44,0x64,0x7D,0x3F,0x92,0x5D,0x69,0xB6,0x1F,0x00,0x4B,0xD4,0x83,0x35,0xCF,
0x7E,0x64,0x4E,0x17,0xAE,0x8D,0xD5,0x2E,0x9A,0x28,0x12,0x4E,0x2E,0x2B,0x49,0x08,
0x5C,0xAE,0xC6,0x46,0x85,0xAE,0x41,0x61,0x1E,0x6F,0x82,0xD2,0x51,0x37,0x16,0x1F,
0x0B,0xF6,0x59,0xA4,0x9A,0xCA,0x5A,0xAF,0x0D,0xD4,0x33,0x8B,0x20,0x63,0xF1,0x84,
0x80,0x5C,0xCB,0xCF,0x08,0xB4,0xB9,0xD3,0x16,0x05,0xBD,0x62,0x83,0x31,0x9B,0x56,
0x51,0x98,0x9F,0xBA,0xB2,0x5B,0xAA,0xB2,0x22,0x6B,0x2C,0xB5,0xD4,0x48,0xFA,0x63,
0x2B,0x5F,0x58,0xFA,0x61,0xFA,0x64,0x09,0xBB,0x38,0xE0,0xB8,0x9D,0x92,0x60,0xA8,
0x0D,0x67,0x6F,0x0E,0x37,0xF5,0x0D,0x01,0x9F,0xC2,0x77,0xD4,0xFE,0xEC,0xF1,0x73,
0x30,0x39,0xE0,0x7D,0xF5,0x61,0x98,0xE4,0x2C,0x28,0x55,0x04,0x56,0x55,0xDB,0x2F,
0x6B,0xEC,0xE5,0x58,0x06,0xB6,0x64,0x80,0x6A,0x2A,0x1A,0x4E,0x5B,0x0F,0xD8,0xC4,
0x0A,0x2E,0x52,0x19,0xD9,0x62,0xF5,0x30,0x48,0xBE,0x8C,0x7B,0x4F,0x38,0x9B,0xA2,
0xC3,0xAF,0xC9,0xD3,0xC7,0xC1,0x62,0x41,0x86,0xB9,0x61,0x21,0x57,0x6F,0x99,0x4F,
0xC1,0xBA,0xCE,0x7B,0xB5,0x3B,0x4D,0x5E,0x8A,0x8B,0x44,0x57,0x5F,0x13,0x5F,0x70,
0x6D,0x5B,0x29,0x47,0xDC,0x38,0xE2,0xEC,0x04,0x55,0x65,0x12,0x2A,0xE8,0x17,0x43,
0xE1,0x8E,0xDD,0x2A,0xB3,0xE2,0x94,0xF7,0x09,0x6E,0x5C,0xE6,0xEB,0x8A,0xF8,0x6D,
0x89,0x49,0x54,0x48,0xF5,0x2F,0xAD,0xBF,0xEA,0x94,0x4B,0xCA,0xFC,0x39,0x87,0x82,
0x5F,0x8A,0x01,0xF2,0x75,0xF2,0xE6,0x71,0xD6,0xD8,0x42,0xDE,0xF1,0x2D,0x1D,0x28,
0xA6,0x88,0x7E,0xA3,0xA0,0x47,0x1D,0x30,0xD9,0xA3,0x71,0xDF,0x49,0x1C,0xCB,0x01,
0xF8,0x36,0xB1,0xF2,0xF0,0x22,0x58,0x5D,0x45,0x6B,0xBD,0xA0,0xBB,0xB2,0x88,0x42,
0xC7,0x8C,0x28,0xCE,0x93,0xE8,0x90,0x63,0x08,0x90,0x7C,0x89,0x3C,0xF5,0x7D,0xB7,
0x04,0x2D,0x4F,0x55,0x51,0x16,0xFD,0x7E,0x79,0xE8,0xBE,0xC1,0xF2,0x12,0xD4,0xF8,
0xB4,0x84,0x05,0x23,0xA0,0xCC,0xD2,0x2B,0xFD,0xE1,0xAB,0xAD,0x0D,0xD1,0x55,0x6C,
0x23,0x41,0x94,0x4D,0x77,0x37,0x4F,0x05,0x28,0x0C,0xBF,0x17,0xB3,0x12,0x67,0x6C,
0x8C,0xC3,0x5A,0xF7,0x41,0x84,0x2A,0x6D,0xD0,0x94,0x12,0x27,0x2C,0xB4,0xED,0x9C,
0x4D,0xEC,0x47,0x82,0x97,0xD5,0x67,0xB9,0x1B,0x9D,0xC0,0x55,0x07,0x7E,0xE5,0x8E,
0xE2,0xA8,0xE7,0x3E,0x12,0xE4,0x0E,0x3A,0x2A,0x45,0x55,0x34,0xA2,0xF9,0x2D,0x5A,
0x1B,0xAB,0x52,0x7C,0x83,0x10,0x5F,0x55,0xD2,0xF1,0x5A,0x43,0x2B,0xC6,0xA7,0xA4,
0x89,0x15,0x95,0xE8,0xB4,0x4B,0x9D,0xF8,0x75,0xE3,0x9F,0x60,0x78,0x5B,0xD6,0xE6,
0x0D,0x44,0xE6,0x21,0x06,0xBD,0x47,0x22,0x53,0xA4,0x00,0xAD,0x8D,0x43,0x13,0x85,
0x39,0xF7,0xAA,0xFC,0x38,0xAF,0x7B,0xED,0xFC,0xE4,0x2B,0x54,0x50,0x98,0x4C,0xFC,
0x85,0x80,0xF7,0xDF,0x3C,0x80,0x22,0xE1,0x94,0xDA,0xDE,0x24,0xC6,0xB0,0x7A,0x39,
0x38,0xDC,0x0F,0xA1,0xA7,0xF4,0xF9,0x6F,0x63,0x18,0x57,0x8B,0x84,0x41,0x2A,0x2E,
0xD4,0x53,0xF2,0xD9,0x00,0x0F,0xD0,0xDD,0x99,0x6E,0x19,0xA6,0x0A,0xD0,0xEC,0x5B,
0x58,0x24,0xAB,0xC0,0xCB,0x06,0x65,0xEC,0x1A,0x13,0x38,0x94,0x0A,0x67,0x03,0x2F,
0x3F,0xF7,0xE3,0x77,0x44,0x77,0x33,0xC6,0x14,0x39,0xD0,0xE3,0xC0,0xA2,0x08,0x79,
0xBB,0x40,0x99,0x57,0x41,0x0B,0x01,0x90,0xCD,0xE1,0xCC,0x48,0x67,0xDB,0xB3,0xAF,
0x88,0x74,0xF3,0x4C,0x82,0x8F,0x72,0xB1,0xB5,0x23,0x29,0xC4,0x12,0x6C,0x19,0xFC,
0x8E,0x46,0xA4,0x9C,0xC4,0x25,0x65,0x87,0xD3,0x6D,0xBE,0x8A,0x93,0x11,0x03,0x38,
0xED,0x83,0x2B,0xF3,0x46,0xA4,0x93,0xEA,0x3B,0x53,0x85,0x1D,0xCE,0xD4,0xF1,0x08,
0x83,0x27,0xED,0xFC,0x9B,0x1A,0x18,0xBC,0xF9,0x8B,0xAE,0xDC,0x24,0xAB,0x50,0x38,
0xE9,0x72,0x4B,0x10,0x22,0x17,0x7B,0x46,0x5D,0xAB,0x59,0x64,0xF3,0x40,0xAE,0xF8,
0xBB,0xE5,0xC8,0xF9,0x26,0x03,0x4E,0x55,0x7D,0xEB,0xEB,0xFE,0xF7,0x39,0xE6,0xE0,
0x0A,0x11,0xBE,0x2E,0x28,0xFF,0x98,0xED,0xC0,0xC9,0x42,0x56,0x42,0xC3,0xFD,0x00,
0xF6,0xAF,0x87,0xA2,0x5B,0x01,0x3F,0x32,0x92,0x47,0x95,0x9A,0x72,0xA5,0x32,0x3D,
0xAE,0x6B,0xD0,0x9B,0x07,0xD2,0x49,0x92,0xE3,0x78,0x4A,0xFA,0xA1,0x06,0x7D,0xF2,
0x41,0xCF,0x77,0x74,0x04,0x14,0xB2,0x0C,0x86,0x84,0x64,0x16,0xD5,0xBB,0x51,0xA1,
0xE5,0x6F,0xF1,0xD1,0xF2,0xE2,0xF7,0x5F,0x58,0x20,0x4D,0xB8,0x57,0xC7,0xCF,0xDD,
0xC5,0xD8,0xBE,0x76,0x3D,0xF6,0x5F,0x7E,0xE7,0x2A,0x8B,0x88,0x24,0x1B,0x38,0x3F,
0x0E,0x41,0x23,0x77,0xF5,0xF0,0x4B,0xD4,0x0C,0x1F,0xFA,0xA4,0x0B,0x80,0x5F,0xCF,
0x45,0xF6,0xE0,0xDA,0x2F,0x34,0x59,0x53,0xFB,0x20,0x3C,0x52,0x62,0x5E,0x35,0xB5,
0x62,0xFE,0x8B,0x60,0x63,0xE3,0x86,0x5A,0x15,0x1A,0x6E,0xD1,0x47,0x45,0xBC,0x32,
0xB4,0xEB,0x67,0x38,0xAB,0xE4,0x6E,0x33,0x3A,0xB5,0xED,0xA3,0xAD,0x67,0xE0,0x4E,
0x41,0x95,0xEE,0x62,0x62,0x71,0x26,0x1D,0x31,0xEF,0x62,0x30,0xAF,0xD7,0x82,0xAC,
0xC2,0xDC,0x05,0x04,0xF5,0x97,0x07,0xBF,0x11,0x59,0x23,0x07,0xC0,0x64,0x02,0xE8,
0x97,0xE5,0x3E,0xAF,0x18,0xAC,0x59,0xA6,0x8B,0x4A,0x33,0x90,0x1C,0x6E,0x7C,0x9C,
0x20,0x7E,0x4C,0x3C,0x3E,0x61,0x64,0xBB,0xC5,0x6B,0x7C,0x7E,0x3E,0x9F,0xC5,0x4C,
0x9F,0xEA,0x73,0xF5,0xD7,0x89,0xC0,0x4C,0xF4,0xFB,0xF4,0x2D,0xEC,0x14,0x1B,0x51,
0xD5,0xC1,0x12,0xC8,0x10,0xDF,0x0B,0x4A,0x8B,0x9C,0xBC,0x93,0x45,0x6A,0x3E,0x3E,
0x7D,0xC1,0xA9,0xBA,0xCD,0xC1,0xB4,0x07,0xE4,0xE1,0x68,0x86,0x43,0xB2,0x6D,0x38,
0xF3,0xFB,0x0C,0x5C,0x66,0x37,0x71,0xDE,0x56,0xEF,0x6E,0xA0,0x10,0x40,0x65,0xA7,
0x98,0xF7,0xD0,0xBE,0x0E,0xC8,0x37,0x36,0xEC,0x10,0xCA,0x7C,0x9C,0xAB,0x84,0x1E,
0x05,0x17,0x76,0x02,0x1C,0x4F,0x52,0xAA,0x5F,0xC1,0xC6,0xA0,0x56,0xB9,0xD8,0x04,
0x84,0x44,0x4D,0xA7,0x59,0xD8,0xDE,0x60,0xE6,0x38,0x0E,0x05,0x8F,0x03,0xE1,0x3B,
0x6D,0x81,0x04,0x33,0x6F,0x30,0x0B,0xCE,0x69,0x05,0x21,0x33,0xFB,0x26,0xBB,0x89,
0x7D,0xB6,0xAE,0x87,0x7E,0x51,0x07,0xE0,0xAC,0xF7,0x96,0x0A,0x6B,0xF9,0xC4,0x5C,
0x1D,0xE4,0x44,0x47,0xB8,0x5E,0xFA,0xE3,0x78,0x84,0x55,0x42,0x4B,0x48,0x5E,0xF7,
0x7D,0x47,0x35,0x86,0x1D,0x2B,0x43,0x05,0x03,0xEC,0x8A,0xB8,0x1E,0x06,0x3C,0x76,
0x0C,0x48,0x1A,0x43,0xA7,0xB7,0x8A,0xED,0x1E,0x13,0xC6,0x43,0xEE,0x10,0xEF,0xDB,
0xEC,0xFB,0x3C,0x83,0xB2,0x95,0x44,0xEF,0xD8,0x54,0x51,0x4E,0x2D,0x11,0x44,0x1D,
0xFB,0x36,0x59,0x1E,0x7A,0x34,0xC1,0xC3,0xCA,0x57,0x00,0x61,0xEA,0x67,0xA5,0x16,
0x9B,0x55,0xD0,0x55,0xE1,0x7F,0xD9,0x36,0xD2,0x40,0x76,0xAE,0xDC,0x01,0xCE,0xB0,
0x7A,0x83,0xD5,0xCB,0x20,0x98,0xEC,0x6B,0xC1,0x72,0x92,0x34,0xF3,0x82,0x57,0x37,
0x62,0x8A,0x32,0x36,0x0C,0x90,0x43,0xAE,0xAE,0x5C,0x9B,0x78,0x8E,0x13,0x65,0x02,
0xFD,0x68,0x71,0xC1,0xFE,0xB0,0x31,0xA0,0x24,0x82,0xB0,0xC3,0xB1,0x79,0x69,0xA7,
0xF5,0xD2,0xEB,0xD0,0x82,0xC0,0x32,0xDC,0x9E,0xC7,0x26,0x3C,0x6D,0x8D,0x98,0xC1,
0xBB,0x22,0xD4,0xD0,0x0F,0x33,0xEC,0x3E,0xB9,0xCC,0xE1,0xDC,0x6A,0x4C,0x77,0x36,
0x14,0x1C,0xF9,0xBF,0x81,0x9F,0x28,0x5F,0x71,0x85,0x32,0x29,0x90,0x75,0x48,0xC4,
0xB3,0x4A,0xCE,0xD8,0x44,0x8F,0x14,0x2F,0xFD,0x40,0x57,0xEF,0xAA,0x08,0x75,0xD9,
0x46,0xD1,0xD6,0x6E,0x32,0x55,0x1F,0xC3,0x18,0xFE,0x84,0x1F,0xFC,0x84,0xD5,0xFF,
0x71,0x5E,0x1B,0x48,0xC3,0x86,0x95,0x0E,0x28,0x08,0x27,0xD3,0x38,0x83,0x71,0x7B,
0x4C,0x80,0x63,0x54,0x9A,0x56,0xB0,0xAC,0xCF,0x80,0xCA,0x31,0x09,0xEF,0xFE,0xF3,
0xBE,0xAF,0x24,0x7E,0xA6,0xFE,0x53,0x3F,0xC2,0x8D,0x4A,0x33,0x68,0xD1,0x22,0xA6,
0x66,0xAD,0x7B,0xEA,0xDE,0xB6,0x43,0xB0,0xA1,0x25,0x95,0x00,0xA3,0x3F,0x75,0x46,
0x14,0x11,0x44,0xEC,0xD7,0x95,0xBC,0x92,0xF0,0x4F,0xA9,0x16,0x53,0x62,0x97,0x60,
0x2A,0x0F,0x41,0xF1,0x71,0x24,0xBE,0xEE,0x94,0x7F,0x08,0xCD,0x60,0x93,0xB3,0x85,
0x5B,0x07,0x00,0x3F,0xD8,0x0F,0x28,0x83,0x9A,0xD1,0x69,0x9F,0xD1,0xDA,0x2E,0xC3,
0x90,0x01,0xA2,0xB9,0x6B,0x4E,0x2A,0x66,0x9D,0xDA,0xAE,0xA6,0xEA,0x2A,0xD3,0x68,
0x2F,0x0C,0x0C,0x9C,0xD2,0x8C,0x4A,0xED,0xE2,0x9E,0x57,0x65,0x9D,0x09,0x87,0xA3,
0xB4,0xC4,0x32,0x5D,0xC9,0xD4,0x32,0x2B,0xB1,0xE0,0x71,0x1E,0x64,0x4D,0xE6,0x90,
0x71,0xE3,0x1E,0x40,0xED,0x7D,0xF3,0x84,0x0E,0xED,0xC8,0x78,0x76,0xAE,0xC0,0x71,
0x27,0x72,0xBB,0x05,0xEA,0x02,0x64,0xFB,0xF3,0x48,0x6B,0xB5,0x42,0x93,0x3F,0xED,
0x9F,0x13,0x53,0xD2,0xF7,0xFE,0x2A,0xEC,0x1D,0x47,0x25,0xDB,0x3C,0x91,0x86,0xC6,
0x8E,0xF0,0x11,0xFD,0x23,0x74,0x36,0xF7,0xA4,0xF5,0x9E,0x7A,0x7E,0x53,0x50,0x44,
0xD4,0x47,0xCA,0xD3,0xEB,0x38,0x6D,0xE6,0xD9,0x71,0x94,0x7F,0x4A,0xC6,0x69,0x4B,
0x11,0xF4,0x52,0xEA,0x22,0xFE,0x8A,0xB0,0x36,0x67,0x8B,0x59,0xE8,0xE6,0x80,0x2A,
0xEB,0x65,0x04,0x13,0xEE,0xEC,0xDC,0x9E,0x5F,0xB1,0xEC,0x05,0x6A,0x59,0xE6,0x9F,
0x5E,0x59,0x6B,0x89,0xBF,0xF7,0x1A,0xCA,0x44,0xF9,0x5B,0x6A,0x71,0x85,0x03,0xE4,
0x29,0x62,0xE0,0x70,0x6F,0x41,0xC4,0xCF,0xB2,0xB1,0xCC,0xE3,0x7E,0xA6,0x07,0xA8,
0x87,0xE7,0x7F,0x84,0x93,0xDB,0x52,0x4B,0x6C,0xEC,0x7E,0xDD,0xD4,0x24,0x48,0x10,
0x69,0x9F,0x04,0x60,0x74,0xE6,0x48,0x18,0xF3,0xE4,0x2C,0xB9,0x4F,0x2E,0x50,0x7A,
0xDF,0xD4,0x54,0x69,0x2B,0x8B,0xA7,0xF3,0xCE,0xFF,0x1F,0xF3,0x3E,0x26,0x01,0x39,
0x17,0x95,0x84,0x89,0xB0,0xF0,0x4C,0x4B,0x82,0x91,0x9F,0xC4,0x4B,0xAC,0x9D,0xA5,
0x74,0xAF,0x17,0x25,0xC9,0xCA,0x32,0xD3,0xBC,0x89,0x8A,0x84,0x89,0xCC,0x0D,0xAE,
0x7C,0xA2,0xDB,0x9C,0x6A,0x78,0x91,0xEE,0xEA,0x76,0x5D,0x4E,0x87,0x60,0xF5,0x69,
0x15,0x67,0xD4,0x02,0xCF,0xAF,0x48,0x36,0x07,0xEA,0xBF,0x6F,0x66,0x2D,0x06,0x8F,
0xC4,0x9A,0xFE,0xF9,0xF6,0x90,0x87,0x75,0xB8,0xF7,0xAD,0x0F,0x76,0x10,0x5A,0x3D,
0x59,0xB0,0x2E,0xB3,0xC7,0x35,0x2C,0xCC,0x70,0x56,0x2B,0xCB,0xE3,0x37,0x96,0xC5,
0x2F,0x46,0x1B,0x8A,0x22,0x46,0xC7,0x88,0xA7,0x26,0x32,0x98,0x61,0xDF,0x86,0x22,
0x8A,0xF4,0x1C,0x2F,0x87,0xA1,0x09,0xAA,0xCC,0xA9,0xAE,0xD3,0xBD,0x00,0x45,0x1C,
0x9A,0x54,0x87,0x86,0x52,0x87,0xEF,0xFF,0x1E,0x8F,0xA1,0x8F,0xC1,0x89,0x5C,0x35,
0x1B,0xDA,0x2D,0x3A,0x2C,0x16,0xB2,0xC2,0xF1,0x56,0xE2,0x78,0xC1,0x6B,0x63,0x97,
0xC5,0x56,0x8F,0xC9,0x32,0x7F,0x2C,0xAA,0xAF,0xA6,0xA8,0xAC,0x20,0x91,0x22,0x88,
0xDE,0xE4,0x60,0x8B,0xF9,0x4B,0x42,0x25,0x1A,0xE3,0x7F,0x9C,0x2C,0x19,0x89,0x3A,
0x7E,0x05,0xD4,0x36,0xCC,0x69,0x58,0xC2,0xC1,0x32,0x8B,0x2F,0x90,0x85,0xEB,0x7A,
0x39,0x50,0xA5,0xA1,0x27,0x92,0xC5,0x66,0xB0,0x20,0x4F,0x58,0x7E,0x55,0x83,0x43,
0x2B,0x45,0xE2,0x9C,0xE4,0xD8,0x12,0x90,0x2C,0x16,0x83,0x56,0x16,0x79,0x03,0xB3,
0xAD,0x2D,0x61,0x18,0x1A,0x13,0x1F,0x37,0xE2,0xE1,0x9C,0x73,0x7B,0x80,0xD5,0xFD,
0x2D,0x51,0x87,0xFC,0x7B,0xAA,0xD7,0x1F,0x2C,0x7A,0x8E,0xAF,0xF4,0x8D,0xBB,0xCD,
0x95,0x11,0x7C,0x72,0x0B,0xEE,0x6F,0xE2,0xB9,0xAF,0xDE,0x37,0x83,0xDE,0x8C,0x8D,
0x62,0x05,0x67,0xB7,0x96,0xC6,0x8D,0x56,0xB6,0x0D,0xD7,0x62,0xBA,0xD6,0x46,0x36,
0xBD,0x8E,0xC8,0xE6,0xEA,0x2A,0x6C,0x10,0x14,0xFF,0x6B,0x5B,0xFA,0x82,0x3C,0x46,
0xB1,0x30,0x43,0x46,0x51,0x8A,0x7D,0x9B,0x92,0x3E,0x83,0x79,0x5B,0x55,0x5D,0xB2,
0x6C,0x5E,0xCE,0x90,0x62,0x8E,0x53,0x98,0xC9,0x0D,0x6D,0xE5,0x2D,0x57,0xCD,0xC5,
0x81,0x57,0xBA,0xE1,0xE8,0xB8,0x8F,0x72,0xE5,0x4F,0x13,0xDC,0xEA,0x9D,0x71,0x15,
0x10,0xB2,0x11,0x88,0xD5,0x09,0xD4,0x7F,0x5B,0x65,0x7F,0x2C,0x3B,0x38,0x4C,0x11,
0x68,0x50,0x8D,0xFB,0x9E,0xB0,0x59,0xBF,0x94,0x80,0x89,0x4A,0xC5,0x1A,0x18,0x12,
0x89,0x53,0xD1,0x4A,0x10,0x29,0xE8,0x8C,0x1C,0xEC,0xB6,0xEA,0x46,0xC7,0x17,0x8B,
0x25,0x15,0x31,0xA8,0xA2,0x6B,0x43,0xB1,0x9D,0xE2,0xDB,0x0B,0x87,0x9B,0xB0,0x11,
0x04,0x0E,0x71,0xD2,0x29,0x77,0x89,0x82,0x0A,0x66,0x41,0x7F,0x1D,0x0B,0x48,0xFF,
0x72,0xBB,0x24,0xFD,0xC2,0x48,0xA1,0x9B,0xFE,0x7B,0x7F,0xCE,0x88,0xDB,0x86,0xD9,
0x85,0x3B,0x1C,0xB0,0xDC,0xA8,0x33,0x07,0xBF,0x51,0x2E,0xE3,0x0E,0x9A,0x00,0x97,
0x1E,0x06,0xC0,0x97,0x43,0x9D,0xD8,0xB6,0x45,0xC4,0x86,0x67,0x5F,0x00,0xF8,0x88,
0x9A,0xA4,0x52,0x9E,0xC7,0xAA,0x8A,0x83,0x75,0xEC,0xC5,0x18,0xAE,0xCE,0xC3,0x2F,
0x1A,0x2B,0xF9,0x18,0xFF,0xAE,0x1A,0xF5,0x53,0x0B,0xB5,0x33,0x51,0xA7,0xFD,0xE8,
0xA8,0xE1,0xA2,0x64,0xB6,0x22,0x17,0x43,0x80,0xCC,0x0A,0xD8,0xAE,0x3B,0xBA,0x40,
0xD7,0xD9,0x92,0x4A,0x89,0xDF,0x04,0x10,0xEE,0x9B,0x18,0x2B,0x6A,0x77,0x69,0x8A,
0x68,0xF4,0xF9,0xB9,0xA2,0x21,0x15,0x6E,0xE6,0x1E,0x3B,0x03,0x62,0x30,0x9B,0x60,
0x41,0x7E,0x25,0x9B,0x9E,0x8F,0xC5,0x52,0x10,0x08,0xF8,0xC2,0x69,0xA1,0x21,0x11,
0x88,0x37,0x5E,0x79,0x35,0x66,0xFF,0x10,0x42,0x18,0x6E,0xED,0x97,0xB6,0x6B,0x1C,
0x4E,0x36,0xE5,0x6D,0x7D,0xB4,0xE4,0xBF,0x20,0xB9,0xE0,0x05,0x3A,0x69,0xD5,0xB8,
0xE3,0xD5,0xDC,0xE0,0xB9,0xAC,0x53,0x3E,0x07,0xA4,0x57,0xAD,0x77,0xFF,0x48,0x18,
0x76,0x2A,0xAC,0x49,0x2A,0x8E,0x47,0x75,0x6D,0x9F,0x67,0x63,0x30,0x35,0x8C,0x39,
0x05,0x39,0xD5,0x6F,0x64,0x3A,0x5B,0xAD,0xCA,0x0B,0xBB,0x82,0x52,0x99,0x45,0xB1,
0x93,0x36,0x36,0x99,0xAF,0x13,0x20,0x44,0x36,0xD8,0x02,0x44,0x09,0x39,0x92,0x85,
0xFF,0x4A,0x4A,0x97,0x87,0xA6,0x63,0xD7,0xC7,0xB5,0xB5,0x24,0xED,0x0F,0xB4,0x6F,
0x0C,0x58,0x52,0x14,0xD9,0xA6,0x7B,0xD3,0x79,0xBC,0x38,0x58,0xA1,0xBD,0x3B,0x84,
0x06,0xD8,0x1A,0x06,0xFD,0x6B,0xA8,0xEA,0x4B,0x69,0x28,0x04,0x37,0xAD,0x82,0x99,
0xFB,0x0E,0x1B,0x85,0xBD,0xA8,0x5D,0x73,0xCD,0xDC,0x58,0x75,0x0A,0xBE,0x63,0x6C,
0x48,0xE7,0x4C,0xE4,0x30,0x2B,0x04,0x60,0xB9,0x15,0xD8,0xDA,0x86,0x81,0x75,0x8F,
0x96,0xD4,0x8D,0x1C,0x5D,0x70,0x85,0x7C,0x1C,0x67,0x7B,0xD5,0x08,0x67,0xA6,0xCE,
0x4B,0x0A,0x66,0x70,0xB7,0xE5,0x63,0xD4,0x5B,0x8A,0x82,0xEA,0x10,0x67,0xCA,0xE2,
0xF4,0xEF,0x17,0x85,0x2F,0x2A,0x5F,0x8A,0x97,0x82,0xF8,0x6A,0xD6,0x34,0x10,0xEA,
0xEB,0xC9,0x5C,0x3C,0xE1,0x49,0xF8,0x46,0xEB,0xDE,0xBD,0xF6,0xA9,0x92,0xF1,0xAA,
0xA6,0xA0,0x18,0xB0,0x3A,0xD3,0x0F,0x1F,0xF3,0x6F,0xFF,0x31,0x45,0x43,0x44,0xD3,
0x50,0x9A,0xF7,0x88,0x09,0x96,0xC1,0xCE,0x76,0xCC,0xF2,0x2C,0x2C,0xBA,0xAD,0x82,
0x77,0x8F,0x18,0x84,0xC0,0xD2,0x07,0x9C,0x36,0x90,0x83,0x4E,0x0B,0xA5,0x4F,0x43,
0x3E,0x04,0xAB,0x78,0x4F,0xD6,0xFB,0x09,0x01,0x24,0x90,0xDA,0x6F,0x3C,0x3A,0x61,
0x0D,0x7F,0x69,0x4A,0xEB,0x2B,0x30,0x02,0xB4,0xDB,0xE0,0x84,0xA9,0xEC,0xD7,0x35,
0xBF,0x37,0x7D,0x85,0x58,0xCE,0xA9,0x4E,0xE4,0x80,0xC7,0xA8,0xD3,0x30,0x67,0x48,
0xEB,0x29,0xAF,0x2F,0x74,0x6A,0xB4,0xA7,0x3F,0x0F,0x3F,0x92,0xAF,0xF3,0xCA,0xAC,
0xAF,0x4B,0xD9,0x94,0xC0,0x43,0xCA,0x81,0x0D,0x2F,0x48,0xA1,0xB0,0x27,0xD5,0xD2,
0xEF,0x4B,0x05,0x85,0xA3,0xDE,0x4D,0x93,0x30,0x3C,0xF0,0xBB,0x4A,0x8F,0x30,0x27,
0x4C,0xEB,0xE3,0x3E,0x64,0xED,0x9A,0x2F,0x3B,0xF1,0x82,0xF0,0xBA,0xF4,0xCF,0x7F,
0x40,0xCB,0xB0,0xE1,0x7F,0xBC,0xAA,0x57,0xD3,0xC9,0x74,0xF2,0xFA,0x43,0x0D,0x22,
0xD0,0xF4,0x77,0x4E,0x93,0xD7,0x85,0x70,0x1F,0x99,0xBF,0xB6,0xDE,0x35,0xF1,0x30,
0xA7,0x5E,0x71,0xF0,0x6B,0x01,0x2D,0x7B,0x64,0xF0,0x33,0x53,0x0A,0x39,0x88,0xF3,
0x6B,0x3A,0xA6,0x6B,0x35,0xD2,0x2F,0x43,0xCD,0x02,0xFD,0xB5,0xE9,0xBC,0x5B,0xAA,
0xD8,0xA4,0x19,0x7E,0x0E,0x5D,0x94,0x81,0x9E,0x6F,0x77,0xAD,0xD6,0x0E,0x74,0x93,
0x96,0xE7,0xC4,0x18,0x5F,0xAD,0xF5,0x19,
];
#endregion
#region 3DS-Specific Fields
@@ -511,150 +244,7 @@ namespace NDecrypt.Core
/// <param name="keyfile">Path to the keyfile</param>
public DecryptArgs(string? config)
{
InitConfigJson(config);
ValidateKeys();
}
/// <summary>
/// Setup all of the necessary constants
/// </summary>
/// <param name="keyfile">Path to the keyfile</param>
/// <param name="useAesKeysTxt">Indicates if the keyfile format is aeskeys.txt</param>
public DecryptArgs(string? keyfile, bool useAesKeysTxt)
{
// Set the default DS values
NitroEncryptionData = _nitroEncryptionData;
// Read the proper keyfile format
if (useAesKeysTxt)
InitAesKeysTxt(keyfile);
else
InitKeysBin(keyfile);
// Perform validation tests
ValidateKeys();
}
/// <summary>
/// Setup all of the necessary constants from aes_keys.txt
/// </summary>
/// <param name="keyfile">Path to aes_keys.txt</param>
private void InitAesKeysTxt(string? keyfile)
{
if (keyfile == null || !File.Exists(keyfile))
{
IsReady = false;
return;
}
try
{
using var reader = new IniReader(keyfile);
while (reader.ReadNextLine())
{
// Ignore comments in the file
if (reader.RowType == IniRowType.Comment)
continue;
if (reader.KeyValuePair == null || string.IsNullOrEmpty(reader.KeyValuePair?.Key))
break;
var kvp = reader.KeyValuePair!.Value;
byte[] value = StringToByteArray(kvp.Value);
switch (kvp.Key)
{
// Hardware constant
case "generator":
AESHardwareConstant = value;
break;
// Retail Keys
case "slot0x18KeyX":
KeyX0x18 = value;
break;
case "slot0x1BKeyX":
KeyX0x1B = value;
break;
case "slot0x25KeyX":
KeyX0x25 = value;
break;
case "slot0x2CKeyX":
KeyX0x2C = value;
break;
// Currently Unused KeyX
case "slot0x03KeyX":
case "slot0x19KeyX":
case "slot0x1AKeyX":
case "slot0x1CKeyX":
case "slot0x1DKeyX":
case "slot0x1EKeyX":
case "slot0x1FKeyX":
case "slot0x2DKeyX":
case "slot0x2EKeyX":
case "slot0x2FKeyX":
case "slot0x30KeyX":
case "slot0x31KeyX":
case "slot0x32KeyX":
case "slot0x33KeyX":
case "slot0x34KeyX":
case "slot0x35KeyX":
case "slot0x36KeyX":
case "slot0x37KeyX":
case "slot0x38KeyX":
case "slot0x3AKeyX":
case "slot0x3BKeyX":
break;
// Currently Unused KeyY
case "slot0x03KeyY":
case "slot0x06KeyY":
case "slot0x07KeyY":
case "slot0x2EKeyY":
case "slot0x2FKeyY":
case "slot0x31KeyY":
break;
// Currently Unused KeyN
case "slot0x0DKeyN":
case "slot0x15KeyN":
case "slot0x16KeyN":
case "slot0x19KeyN":
case "slot0x1AKeyN":
case "slot0x1BKeyN":
case "slot0x1CKeyN":
case "slot0x1DKeyN":
case "slot0x1EKeyN":
case "slot0x1FKeyN":
case "slot0x24KeyN":
case "slot0x2DKeyN":
case "slot0x2EKeyN":
case "slot0x2FKeyN":
case "slot0x31KeyN":
case "slot0x32KeyN":
case "slot0x36KeyN":
case "slot0x37KeyN":
case "slot0x38KeyN":
case "slot0x3BKeyN":
break;
}
}
}
catch
{
IsReady = false;
return;
}
IsReady = true;
}
/// <summary>
/// Setup all of the necessary constants from config.json
/// </summary>
/// <param name="config">Path to config.json</param>
private void InitConfigJson(string? config)
{
if (config == null || !File.Exists(config))
if (config is null || !File.Exists(config))
{
IsReady = false;
return;
@@ -662,75 +252,26 @@ namespace NDecrypt.Core
// Try to read the configuration file
var configObj = Configuration.Create(config);
if (configObj == null)
if (configObj is null)
{
IsReady = false;
return;
}
// Set the fields from the configuration
NitroEncryptionData = StringToByteArray(configObj.NitroEncryptionData);
AESHardwareConstant = StringToByteArray(configObj.AESHardwareConstant);
KeyX0x18 = StringToByteArray(configObj.KeyX0x18);
KeyX0x1B = StringToByteArray(configObj.KeyX0x1B);
KeyX0x25 = StringToByteArray(configObj.KeyX0x25);
KeyX0x2C = StringToByteArray(configObj.KeyX0x2C);
DevKeyX0x18 = StringToByteArray(configObj.DevKeyX0x18);
DevKeyX0x1B = StringToByteArray(configObj.DevKeyX0x1B);
DevKeyX0x25 = StringToByteArray(configObj.DevKeyX0x25);
DevKeyX0x2C = StringToByteArray(configObj.DevKeyX0x2C);
IsReady = true;
}
/// <summary>
/// Setup all of the necessary constants from keys.bin
/// </summary>
/// <param name="keyfile">Path to keys.bin</param>
/// <remarks>keys.bin should be in little endian format</remarks>
private void InitKeysBin(string? keyfile)
{
if (keyfile == null || !File.Exists(keyfile))
{
IsReady = false;
return;
}
try
{
using Stream reader = File.Open(keyfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Hardware constant
AESHardwareConstant = reader.ReadBytes(16);
Array.Reverse(AESHardwareConstant);
// Retail keys
KeyX0x18 = reader.ReadBytes(16);
Array.Reverse(KeyX0x18);
KeyX0x1B = reader.ReadBytes(16);
Array.Reverse(KeyX0x1B);
KeyX0x25 = reader.ReadBytes(16);
Array.Reverse(KeyX0x25);
KeyX0x2C = reader.ReadBytes(16);
Array.Reverse(KeyX0x2C);
// Development keys
DevKeyX0x18 = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x18);
DevKeyX0x1B = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x1B);
DevKeyX0x25 = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x25);
DevKeyX0x2C = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x2C);
}
catch
{
IsReady = false;
return;
}
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>
@@ -742,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 = [];
@@ -754,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 = [];
@@ -766,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 = [];
@@ -778,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 = [];
@@ -790,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 = [];
@@ -802,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 = [];
@@ -814,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 = [];
@@ -826,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 = [];
@@ -838,31 +378,14 @@ 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 = [];
}
}
}
// https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa
private static byte[] StringToByteArray(string? hex)
{
// Handle null values
if (hex == null)
return [];
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
}
}
}

View File

@@ -5,24 +5,28 @@
/// <summary>
/// Attempts to encrypt an input file
/// </summary>
/// <param name="filename">Name of the file to encrypt</param>
/// <param name="input">Name of the file to encrypt</param>
/// <param name="output">Optional name of the file to write to</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <returns>True if the file could be encrypted, false otherwise</returns>
bool EncryptFile(string filename, bool force);
/// <remarks>If an output filename is not provided, the input file will be overwritten</remarks>
public bool EncryptFile(string input, string? output, bool force);
/// <summary>
/// Attempts to decrypt an input file
/// </summary>
/// <param name="filename">Name of the file to decrypt</param>
/// <param name="input">Name of the file to decrypt</param>
/// <param name="output">Optional name of the file to write to</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <returns>True if the file could be decrypted, false otherwise</returns>
bool DecryptFile(string filename, bool force);
/// <remarks>If an output filename is not provided, the input file will be overwritten</remarks>
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</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.3.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.4.2" />
<PackageReference Include="SabreTools.IO" Version="1.6.2" />
<PackageReference Include="SabreTools.Models" Version="1.5.8" />
<PackageReference Include="SabreTools.Serialization" Version="1.8.6" />
<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>
</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
{
@@ -24,7 +24,7 @@ namespace NDecrypt.Core
/// <summary>
/// Set of all partition keys
/// </summary>
private readonly PartitionKeys[] KeysMap = new PartitionKeys[8];
private readonly PartitionKeys[] _keysMap = new PartitionKeys[8];
public ThreeDSTool(bool development, DecryptArgs decryptArgs)
{
@@ -35,7 +35,7 @@ namespace NDecrypt.Core
#region Decrypt
/// <inheritdoc/>
public bool DecryptFile(string filename, bool force)
public bool DecryptFile(string input, string? output, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
@@ -46,25 +46,31 @@ namespace NDecrypt.Core
try
{
// Open the read and write on the same file for inplace processing
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var output = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// If the output is provided, copy the input file
if (output is not null)
File.Copy(input, output, overwrite: true);
else
output = input;
// Open the output file for processing
using var reader = File.Open(output, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DS.Create(input);
if (cart?.Model == null)
var cart = N3DS.Create(reader);
if (cart?.Model is null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
}
// Decrypt all 8 NCCH partitions
DecryptAllPartitions(cart, force, input, output);
DecryptAllPartitions(cart, force, reader, writer);
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine($"An error has occurred. {output} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid 3DS or New 3DS cart image and try again.");
return false;
}
@@ -75,12 +81,12 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptAllPartitions(N3DS cart, bool force, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
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;
@@ -90,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;
@@ -98,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;
@@ -106,7 +112,7 @@ namespace NDecrypt.Core
// Decrypt the partition, if possible
if (ShouldDecryptPartition(cart, p, force))
DecryptPartition(cart, p, input, output);
DecryptPartition(cart, p, reader, writer);
}
}
@@ -137,20 +143,20 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptPartition(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void DecryptPartition(N3DS cart, int index, Stream reader, Stream writer)
{
// Determine the keys needed for this partition
SetDecryptionKeys(cart, index);
// Decrypt the parts of the partition
DecryptExtendedHeader(cart, index, input, output);
DecryptExeFS(cart, index, input, output);
DecryptRomFS(cart, index, input, output);
DecryptExtendedHeader(cart, index, reader, writer);
DecryptExeFS(cart, index, reader, writer);
DecryptRomFS(cart, index, reader, writer);
// Update the flags
UpdateDecryptCryptoAndMasks(cart, index, output);
UpdateDecryptCryptoAndMasks(cart, index, writer);
}
/// <summary>
@@ -162,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
@@ -171,7 +177,7 @@ namespace NDecrypt.Core
CryptoMethod method = cart.GetCryptoMethod(index);
// Get the partition keys
KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
_keysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
}
/// <summary>
@@ -179,13 +185,13 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExtendedHeader(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private bool DecryptExtendedHeader(N3DS cart, int index, Stream reader, Stream writer)
{
// Get required offsets
uint partitionOffset = cart.GetPartitionOffset(index);
if (partitionOffset == 0 || partitionOffset > input.Length)
if (partitionOffset == 0 || partitionOffset > reader.Length)
{
Console.WriteLine($"Partition {index} No Data... Skipping...");
return false;
@@ -199,22 +205,22 @@ namespace NDecrypt.Core
}
// Seek to the extended header
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
reader.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
writer.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index}: Decrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = 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, input, output, 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
input.Seek(0, SeekOrigin.Begin);
reader.Seek(0, SeekOrigin.Begin);
#endif
output.Flush();
writer.Flush();
return true;
}
@@ -223,13 +229,13 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExeFS(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private bool DecryptExeFS(N3DS cart, int index, Stream reader, Stream writer)
{
// Validate the ExeFS
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > input.Length)
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > reader.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
@@ -243,31 +249,31 @@ namespace NDecrypt.Core
}
// Decrypt the filename table
DecryptExeFSFilenameTable(cart, index, input, output);
DecryptExeFSFilenameTable(cart, index, reader, writer);
// For all but the original crypto method, process each of the files in the table
if (cart.GetCryptoMethod(index) != CryptoMethod.Original)
DecryptExeFSFileEntries(cart, index, input, output);
DecryptExeFSFileEntries(cart, index, reader, writer);
// Get the ExeFS files offset
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Seek to the ExeFS
input.Seek(exeFsFilesOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset, SeekOrigin.Begin);
reader.Seek(exeFsFilesOffset, SeekOrigin.Begin);
writer.Seek(exeFsFilesOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = 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,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
reader,
writer,
s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
return true;
}
@@ -277,37 +283,37 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFilenameTable(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void DecryptExeFSFilenameTable(N3DS cart, int index, Stream reader, Stream writer)
{
// Get ExeFS offset
uint exeFsOffset = cart.GetExeFSOffset(index);
if (exeFsOffset == 0 || exeFsOffset > input.Length)
if (exeFsOffset == 0 || exeFsOffset > reader.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Seek to the ExeFS header
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
reader.Seek(exeFsOffset, SeekOrigin.Begin);
writer.Seek(exeFsOffset, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Decrypting - ExeFS Filename Table");
// Create the ExeFS AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, cart.ExeFSIV(index));
var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
byte[] readBytes = input.ReadBytes((int)cart.MediaUnitSize);
byte[] readBytes = reader.ReadBytes((int)cart.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.Write(processedBytes);
writer.Write(processedBytes);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
input.Seek(0, SeekOrigin.Begin);
reader.Seek(0, SeekOrigin.Begin);
#endif
output.Flush();
writer.Flush();
}
/// <summary>
@@ -315,11 +321,11 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFileEntries(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <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;
@@ -327,12 +333,12 @@ namespace NDecrypt.Core
// Reread the decrypted ExeFS header
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
input.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
cart.ExeFSHeaders[index] = SabreTools.Serialization.Deserializers.N3DS.ParseExeFSHeader(input);
reader.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
cart.ExeFSHeaders[index] = SabreTools.Serialization.Readers.N3DS.ParseExeFSHeader(reader);
// Get the ExeFS header
var exeFsHeader = cart.ExeFSHeaders[index];
if (exeFsHeader?.FileHeaders == null)
if (exeFsHeader?.FileHeaders is null)
{
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
@@ -350,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
input.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
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,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
reader,
writer,
s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
}
}
@@ -378,13 +384,13 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptRomFS(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private bool DecryptRomFS(N3DS cart, int index, Stream reader, Stream writer)
{
// Validate the RomFS
uint romFsOffset = cart.GetRomFSOffset(index);
if (romFsOffset == 0 || romFsOffset > input.Length)
if (romFsOffset == 0 || romFsOffset > reader.Length)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
@@ -398,18 +404,18 @@ namespace NDecrypt.Core
}
// Seek to the RomFS
input.Seek(romFsOffset, SeekOrigin.Begin);
output.Seek(romFsOffset, SeekOrigin.Begin);
reader.Seek(romFsOffset, SeekOrigin.Begin);
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,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
reader,
writer,
s => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
return true;
}
@@ -419,28 +425,28 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="output">Stream representing the output</param>
private static void UpdateDecryptCryptoAndMasks(N3DS cart, int index, Stream output)
/// <param name="writer">Stream representing the output</param>
private static void UpdateDecryptCryptoAndMasks(N3DS cart, int index, Stream writer)
{
// Get required offsets
uint partitionOffset = cart.GetPartitionOffset(index);
// Seek to the CryptoMethod location
output.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
writer.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
// Write the new CryptoMethod
output.Write((byte)CryptoMethod.Original);
output.Flush();
writer.Write((byte)CryptoMethod.Original);
writer.Flush();
// Seek to the BitMasks location
output.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
writer.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
// Write the new BitMasks flag
BitMasks flag = cart.GetBitMasks(index);
flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag |= BitMasks.NoCrypto;
output.Write((byte)flag);
output.Flush();
writer.Write((byte)flag);
writer.Flush();
}
#endregion
@@ -448,7 +454,7 @@ namespace NDecrypt.Core
#region Encrypt
/// <inheritdoc/>
public bool EncryptFile(string filename, bool force)
public bool EncryptFile(string input, string? output, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
@@ -459,25 +465,31 @@ namespace NDecrypt.Core
try
{
// Open the read and write on the same file for inplace processing
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var output = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// If the output is provided, copy the input file
if (output is not null)
File.Copy(input, output, overwrite: true);
else
output = input;
// Open the output file for processing
using var reader = File.Open(output, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DS.Create(input);
if (cart?.Model == null)
var cart = N3DS.Create(reader);
if (cart?.Model is null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
}
// Encrypt all 8 NCCH partitions
EncryptAllPartitions(cart, force, input, output);
EncryptAllPartitions(cart, force, reader, writer);
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine($"An error has occurred. {output} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid 3DS or New 3DS cart image and try again.");
return false;
}
@@ -488,12 +500,12 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptAllPartitions(N3DS cart, bool force, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
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;
@@ -504,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;
@@ -512,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;
@@ -520,7 +532,7 @@ namespace NDecrypt.Core
// Encrypt the partition, if possible
if (ShouldEncryptPartition(cart, p, force))
EncryptPartition(cart, p, input, output);
EncryptPartition(cart, p, reader, writer);
}
}
@@ -551,20 +563,20 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptPartition(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void EncryptPartition(N3DS cart, int index, Stream reader, Stream writer)
{
// Determine the keys needed for this partition
SetEncryptionKeys(cart, index);
// Encrypt the parts of the partition
EncryptExtendedHeader(cart, index, input, output);
EncryptExeFS(cart, index, input, output);
EncryptRomFS(cart, index, input, output);
EncryptExtendedHeader(cart, index, reader, writer);
EncryptExeFS(cart, index, reader, writer);
EncryptRomFS(cart, index, reader, writer);
// Update the flags
UpdateEncryptCryptoAndMasks(cart, index, output);
UpdateEncryptCryptoAndMasks(cart, index, writer);
}
/// <summary>
@@ -576,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
@@ -590,7 +602,7 @@ namespace NDecrypt.Core
CryptoMethod method = backupHeader.Flags.CryptoMethod;
// Get the partition keys
KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
_keysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
}
/// <summary>
@@ -598,13 +610,13 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExtendedHeader(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private bool EncryptExtendedHeader(N3DS cart, int index, Stream reader, Stream writer)
{
// Get required offsets
uint partitionOffset = cart.GetPartitionOffset(index);
if (partitionOffset == 0 || partitionOffset > input.Length)
if (partitionOffset == 0 || partitionOffset > reader.Length)
{
Console.WriteLine($"Partition {index} No Data... Skipping...");
return false;
@@ -618,22 +630,22 @@ namespace NDecrypt.Core
}
// Seek to the extended header
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
reader.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
writer.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index}: Encrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = 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, input, output, 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
input.Seek(0, SeekOrigin.Begin);
reader.Seek(0, SeekOrigin.Begin);
#endif
output.Flush();
writer.Flush();
return true;
}
@@ -642,11 +654,11 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExeFS(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <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;
@@ -654,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;
@@ -663,31 +675,31 @@ namespace NDecrypt.Core
// For all but the original crypto method, process each of the files in the table
var backupHeader = cart.BackupHeader;
if (backupHeader!.Flags!.CryptoMethod != CryptoMethod.Original)
EncryptExeFSFileEntries(cart, index, input, output);
EncryptExeFSFileEntries(cart, index, reader, writer);
// Encrypt the filename table
EncryptExeFSFilenameTable(cart, index, input, output);
EncryptExeFSFilenameTable(cart, index, reader, writer);
// Get the ExeFS files offset
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Seek to the ExeFS
input.Seek(exeFsFilesOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset, SeekOrigin.Begin);
reader.Seek(exeFsFilesOffset, SeekOrigin.Begin);
writer.Seek(exeFsFilesOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = 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,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
reader,
writer,
s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
return true;
}
@@ -697,37 +709,37 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFilenameTable(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void EncryptExeFSFilenameTable(N3DS cart, int index, Stream reader, Stream writer)
{
// Get ExeFS offset
uint exeFsOffset = cart.GetExeFSOffset(index);
if (exeFsOffset == 0 || exeFsOffset > input.Length)
if (exeFsOffset == 0 || exeFsOffset > reader.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Seek to the ExeFS header
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
reader.Seek(exeFsOffset, SeekOrigin.Begin);
writer.Seek(exeFsOffset, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Encrypting - ExeFS Filename Table");
// Create the ExeFS AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, cart.ExeFSIV(index));
var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
byte[] readBytes = input.ReadBytes((int)cart.MediaUnitSize);
byte[] readBytes = reader.ReadBytes((int)cart.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.Write(processedBytes);
writer.Write(processedBytes);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
input.Seek(0, SeekOrigin.Begin);
reader.Seek(0, SeekOrigin.Begin);
#endif
output.Flush();
writer.Flush();
}
/// <summary>
@@ -735,13 +747,13 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFileEntries(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void EncryptExeFSFileEntries(N3DS cart, int index, Stream reader, Stream writer)
{
// Get ExeFS offset
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > input.Length)
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > reader.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
@@ -752,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;
@@ -767,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
input.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
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,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
reader,
writer,
s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
}
}
@@ -795,13 +807,13 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptRomFS(N3DS cart, int index, Stream input, Stream output)
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private bool EncryptRomFS(N3DS cart, int index, Stream reader, Stream writer)
{
// Validate the RomFS
uint romFsOffset = cart.GetRomFSOffset(index);
if (romFsOffset == 0 || romFsOffset > input.Length)
if (romFsOffset == 0 || romFsOffset > reader.Length)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
@@ -815,25 +827,25 @@ namespace NDecrypt.Core
}
// Seek to the RomFS
input.Seek(romFsOffset, SeekOrigin.Begin);
output.Seek(romFsOffset, SeekOrigin.Begin);
reader.Seek(romFsOffset, SeekOrigin.Begin);
writer.Seek(romFsOffset, SeekOrigin.Begin);
// Force setting encryption keys for partitions 1 and above
if (index > 0)
{
var backupHeader = cart.BackupHeader;
KeysMap[index].SetRomFSValues(backupHeader!.Flags!.BitMasks);
_keysMap[index].SetRomFSValues(backupHeader!.Flags!.BitMasks);
}
// 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,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
reader,
writer,
s => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
return true;
}
@@ -843,36 +855,36 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="output">Stream representing the output</param>
private static void UpdateEncryptCryptoAndMasks(N3DS cart, int index, Stream output)
/// <param name="writer">Stream representing the output</param>
private static void UpdateEncryptCryptoAndMasks(N3DS cart, int index, Stream writer)
{
// Get required offsets
uint partitionOffset = cart.GetPartitionOffset(index);
// Get the backup header
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == null)
if (backupHeader?.Flags is null)
return;
// Seek to the CryptoMethod location
output.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
writer.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
// Write the new CryptoMethod
// - For partitions 1 and up, set crypto-method to 0x00
// - If partition 0, restore crypto-method from backup flags
byte cryptoMethod = index > 0 ? (byte)CryptoMethod.Original : (byte)backupHeader.Flags.CryptoMethod;
output.Write(cryptoMethod);
output.Flush();
writer.Write(cryptoMethod);
writer.Flush();
// Seek to the BitMasks location
output.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
writer.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
// Write the new BitMasks flag
BitMasks flag = cart.GetBitMasks(index);
flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF;
flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupHeader.Flags.BitMasks;
output.Write((byte)flag);
output.Flush();
writer.Write((byte)flag);
writer.Flush();
}
#endregion
@@ -889,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.3.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>
</Project>

View File

@@ -1,381 +1,89 @@
using System;
using System.Collections.Generic;
using System.IO;
#if NET20 || NET35 || NET40 || NET452
using System.Reflection;
#endif
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)
{
if (args.Length < 2)
// Create the command set
var commandSet = CreateCommands();
// If we have no args, show the help and quit
if (args is null || args.Length == 0)
{
DisplayHelp("Not enough arguments");
commandSet.OutputAllHelp();
return;
}
Feature feature;
if (args[0] == "decrypt" || args[0] == "d")
// Get the first argument as a feature flag
string featureName = args[0];
// Get the associated feature
var topLevel = commandSet.GetTopLevel(featureName);
if (topLevel is null || topLevel is not Feature feature)
{
feature = Feature.Decrypt;
}
else if (args[0] == "encrypt" || args[0] == "e")
{
feature = Feature.Encrypt;
}
else if (args[0] == "info" || args[0] == "i")
{
feature = Feature.Info;
}
else
{
DisplayHelp($"Invalid operation: {args[0]}");
Console.WriteLine($"'{featureName}' is not valid feature flag");
commandSet.OutputFeatureHelp(featureName);
return;
}
bool development = false,
force = false,
outputHashes = false,
useAesKeysTxt = false;
string? config = null;
string? keyfile = null;
int start = 1;
for (; start < args.Length; start++)
// Handle default help functionality
if (topLevel is Help helpFeature)
{
if (args[start] == "-a" || args[start] == "--aes-keys")
{
useAesKeysTxt = true;
}
else if (args[start] == "-d" || args[start] == "--development")
{
development = true;
}
else if (args[start] == "-f" || args[start] == "--force")
{
force = true;
}
else if (args[start] == "-h" || args[start] == "--hash")
{
outputHashes = true;
}
else if (args[start] == "-k" || args[start] == "--keyfile")
{
if (start == args.Length - 1)
Console.WriteLine("Invalid keyfile path: no additional arguments found!");
start++;
string tempPath = args[start];
if (string.IsNullOrEmpty(tempPath))
Console.WriteLine($"Invalid keyfile path: null or empty path found!");
tempPath = Path.GetFullPath(tempPath);
if (!File.Exists(tempPath))
Console.WriteLine($"Invalid keyfile path: file {tempPath} not found!");
else
keyfile = tempPath;
}
else if (args[start] == "-c" || args[start] == "--config")
{
if (start == args.Length - 1)
Console.WriteLine("Invalid config path: no additional arguments found!");
start++;
string tempPath = args[start];
if (string.IsNullOrEmpty(tempPath))
Console.WriteLine($"Invalid config path: null or empty path found!");
tempPath = Path.GetFullPath(tempPath);
if (!File.Exists(tempPath))
Console.WriteLine($"Invalid config path: file {tempPath} not found!");
else
config = tempPath;
}
else
{
break;
}
}
// Derive the config path based on the runtime folder if not already set
config = DeriveConfigFile(config);
// Derive the keyfile path based on the runtime folder if not already set
keyfile = DeriveKeyFile(keyfile, useAesKeysTxt);
// If we are using a Citra keyfile, there are no development keys
if (development && useAesKeysTxt)
{
Console.WriteLine("AES keyfiles don't contain development keys; disabling the option...");
development = false;
}
// Initialize the decrypt args, if possible
DecryptArgs decryptArgs;
if (config != null)
decryptArgs = new DecryptArgs(config);
else
decryptArgs = new DecryptArgs(keyfile, useAesKeysTxt);
// Create reusable tools
_tools[FileType.NDS] = new DSTool(decryptArgs);
_tools[FileType.N3DS] = new ThreeDSTool(development, decryptArgs);
for (int i = start; i < args.Length; i++)
{
if (File.Exists(args[i]))
{
ProcessPath(args[i], feature, force, outputHashes);
}
else if (Directory.Exists(args[i]))
{
foreach (string file in Directory.GetFiles(args[i], "*", SearchOption.AllDirectories))
{
ProcessPath(file, feature, force, outputHashes);
}
}
else
{
Console.WriteLine($"{args[i]} is not a file or folder. Please check your spelling and formatting and try again.");
}
}
}
/// <summary>
/// Process a single file path
/// </summary>
/// <param name="path">File path to process</param>
/// <param name="feature">Indicates what should be done to the file</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="outputHashes">Indicates if hashes should be output after a successful operation</param>
private static void ProcessPath(string path, Feature feature, bool force, bool outputHashes)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(path);
if (tool == null)
return;
Console.WriteLine($"Processing {path}");
// Encrypt or decrypt the file as requested
if (feature == Feature.Encrypt && !tool.EncryptFile(path, force))
{
Console.WriteLine("Encryption failed!");
helpFeature.ProcessArgs(args, 0, commandSet);
return;
}
else if (feature == Feature.Decrypt && !tool.DecryptFile(path, force))
{
Console.WriteLine("Decryption failed!");
return;
}
else if (feature == Feature.Info)
{
string? infoString = tool.GetInformation(path);
infoString ??= "There was a problem getting file information!";
Console.WriteLine(infoString);
}
// Output the file hashes, if expected
if (outputHashes)
WriteHashes(path);
}
/// <summary>
/// Display a basic help text
/// </summary>
/// <param name="err">Additional error text to display, can be null to ignore</param>
private static void DisplayHelp(string? err = null)
{
if (!string.IsNullOrEmpty(err))
Console.WriteLine($"Error: {err}");
Console.WriteLine(@"Usage: NDecrypt <operation> [flags] <path> ...
Possible values for <operation>:
e, encrypt - Encrypt the input files
d, decrypt - Decrypt the input files
i, info - Output file information
Possible values for [flags] (one or more can be used):
-c, --config <path> Path to config.json
-a, --aes-keys Enable using aes_keys.txt instead of keys.bin
-k, --keyfile <path> Path to keys.bin or aes_keys.txt
-d, --development Enable using development keys, if available
-f, --force Force operation by avoiding sanity checks
-h, --hash Output size and hashes to a companion file
<path> can be any file or folder that contains uncompressed items.
More than one path can be specified at a time.");
}
/// <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>
/// Derive the full path to the keyfile, if possible
/// </summary>
private static string? DeriveKeyFile(string? keyfile, bool useAesKeysTxt)
{
// If a path is passed in
if (!string.IsNullOrEmpty(keyfile))
{
keyfile = Path.GetFullPath(keyfile);
if (File.Exists(keyfile))
return keyfile;
}
// Derive the keyfile path, if possible
return GetFileLocation(useAesKeysTxt ? "aes_keys.txt" : "keys.bin");
}
/// <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>
/// 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;
}
/// <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))
// Now verify that all other flags are valid
if (!feature.ProcessArgs(args, 1))
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))
// If inputs are required
if (feature.RequiresInputs && !feature.VerifyInputs())
{
sw.WriteLine(hashString);
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>
/// Create the command set for the program
/// </summary>
private static CommandSet CreateCommands()
{
List<string> header = [
"Cart Image Encrypt/Decrypt Tool",
string.Empty,
"NDecrypt <operation> [options] <path> ...",
string.Empty,
];
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.",
];
var commandSet = new CommandSet(header, footer);
commandSet.Add(new Help());
commandSet.Add(new EncryptFeature());
commandSet.Add(new DecryptFeature());
commandSet.Add(new InfoFeature());
return commandSet;
}
}
}

View File

@@ -32,12 +32,11 @@ For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/N
i, info - Output file information
Possible values for [flags] (one or more can be used):
-c, --config <path> Path to config.json
-a, --aes-keys Enable using aes_keys.txt instead of keys.bin
-k, --keyfile <path> Path to keys.bin or aes_keys.txt
-d, --development Enable using development keys, if available
-f, --force Force operation by avoiding sanity checks
-h, --hash Output size and hashes to a companion file
-?, -h, --help Display this help text and quit
-c, --config <path> Path to config.json
-d, --development Enable using development keys, if available
-f, --force Force operation by avoiding sanity checks
--hash Output size and hashes to a companion file
<path> can be any file or folder that contains uncompressed items.
More than one path can be specified at a time.
@@ -47,13 +46,12 @@ For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/N
- Input files are overwritten, even if they are only partially processed. You should make backups of the files you're working on if you're worried about this.
- Mixed folders or inputs are also accepted, you can decrypt or encrypt multiple files, regardless of their type. This being said, you can only do encrypt _OR_ decrypt at one time.
- Required files will automatically be searched for in the application runtime directory as well as `%HOME%/.config/ndecrypt`, also known as `%USERPROFILE%\.config\ndecrypt` on Windows.
- If found, `config.json` will take priority over both `keys.bin` and `aes_keys.txt`, even if `-a` and/or `-k` are defined. You've been warned.
## I feel like something is missing
There are 3 major files that you can use to give NDecrypt that extra _oomph_ of functionality that it really needs. That is, you can't do any encryption or decryption without at least one of these present. I can't give you the files and I can't generate them for you on the fly with the correct values. Keys are a thorny thing and I just do not want to deal with them. Values are validated, at least, but you'll only get yelled at on run if one of them is wrong. Don't worry, they're just disabled, not removed.
There is a major file that you can use to give NDecrypt that extra _oomph_ of functionality that it really needs. That is, you can't do any encryption or decryption without it present. I can't give you the files and I can't generate them for you on the fly with the correct values. Keys are a thorny thing and I just do not want to deal with them. Values are validated, at least, but you'll only get yelled at on run if one of them is wrong. Don't worry, they're just disabled, not removed.
This convenient table gives an overview of the 3 supported types, the keys that they provide, as well as an even more convenient map to a well-known external tool's configuration format.
This convenient table gives an overview of mappings between the current `config.json` type along with the 2 formerly-supported types and a completely unsupported but common type.
| `config.json` | `keys.bin` order | `aes_keys.txt` | rom-properties `keys.conf` |
| --- | --- | --- | --- |
@@ -70,27 +68,25 @@ This convenient table gives an overview of the 3 supported types, the keys that
**Note:** `Dev*` keys are not required for the vast majority of normal operations. They're only used if the `-d` option is included. Working with your own retail carts will pretty much never require these, so don't drive yourself silly dealing with them.
**Note:** The `NitroEncryptionData` field is also known as the "Blowfish table" for Nintendo DS carts. It's stored in the same hex string format as the other keys. There's some complicated stuff about how it's used and where it's stored, but all you need to know is that it wasn't required for `keys.bin` and `aes_keys.txt` but will be for `config.json`.
**Note:** The `NitroEncryptionData` field is also known as the "Blowfish table" for Nintendo DS carts. It's stored in the same hex string format as the other keys. There's some complicated stuff about how it's used and where it's stored, but all you need to know is that it is required.
**Community Note:** If you would like to try out the new `config.json` format below and already have either `keys.bin` or `aes_keys.txt`, consider using [this helpful community-made script](https://gist.github.com/Dimensional/82f212a0b35bcf9caaa2bc9a70b3a92a) to make your life a bit easier.
**Community Note:** If you have used previous versions of NDecrypt and already have either `keys.bin` or `aes_keys.txt`, consider using [this helpful community-made script](https://gist.github.com/Dimensional/82f212a0b35bcf9caaa2bc9a70b3a92a) to make your life a bit easier. It will convert them into the new `config.json` format that will be supported from here on out.
### `config.json`
The up-and-coming, shiny, new, exciting, JSON-based format for storing the encryption keys that you need for Nintendo DS, 3DS, and New 3DS. This JSON file is not generated by anything, but maps pretty much one-to-one with the code inside of NDecrypt, making it super convenient to use. Keys provided need to be hex strings (e.g. `"AABBCCDD"`). Any keys that are left with `null` or `""` as the value will be ignored. See [the sample config](https://github.com/SabreTools/NDecrypt/blob/master/config-default.json) that I've nicely generated for you. You're welcome.
This is used if it's found, even if you have a `keys.bin` file or if you're using the `-a` flag. It's intentionally very bullish about being used because this will be the singular format for keys in the future. I know I mentioned this above as well, but I also know users don't like reading.
In the future, this file will be automatically generated on first run along with some cutesy little message telling you to fill it out when you get a chance. It's not doing it right now because I don't want to confuse users. Including those reading this. How meta.
### `keys.bin`
### `keys.bin` (Deprecated)
This is the OG of NDecrypt key file formats. It's a weird, binary blob of a format that is composed of little-endian values (most common extraction methods produce big endian, so keep that in mind). It's only compatible wtih Nintendo 3DS and New 3DS keys and is incredibly inflexible in its layout. The little-endianness of it is a relic of how keys were handled in-code previously and I really can't fix it now. If you don't have a key, it needs to be filled with `0x00` bytes so it doesn't mess up the read. Yeah.
Oddly, this gets confused with some similar format that GodMode9 works with, but it has nothing to do with it. If you try to use one of those files in place of this one, something will probably break. It wasn't intentional, I just didn't look ahead of time. See the table in the main part of this section for the order the keys need to be stored in.
### `aes_keys.txt`
### `aes_keys.txt` (Deprecated)
This is an INI-based format that was super popular among 3DS emulators and probably still is. To use this over `keys.bin`, the `-a` flag has to be included or else it won't be found. Yes, even if `keys.bin` isn't even in the folder. Weird thing, I know, but just roll with it please. The one major downside to this is that development keys can't be defined in this format. If you forget this and use `-d` anyway, NDecrypt will disable that flag for you. You're welcome.
This is an INI-based format that was super popular among 3DS emulators and probably still is. Weird thing, I know, but just roll with it please.
## But does it work?

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