69 Commits
0.3.0 ... 0.3.2

Author SHA1 Message Date
Matt Nadareski
31b40e6f10 Bump version 2025-04-04 12:43:54 -04:00
Matt Nadareski
69a07cde85 Change table columns to match sections 2025-04-04 12:04:02 -04:00
Matt Nadareski
b9b4d6876c Include native libraries in executable for publish 2025-04-03 12:16:17 -04:00
Matt Nadareski
bed965c6dd Add script link to README, thanks Dimensional 2025-04-02 17:01:01 -04:00
Matt Nadareski
f01c04d796 Be consistent with language for once 2025-04-02 13:35:42 -04:00
Matt Nadareski
5de970965e README overhaul and minor flag update 2025-04-02 13:34:19 -04:00
Matt Nadareski
f3e4aa0b7e Slight cleanup of notes in README 2025-04-02 12:55:29 -04:00
Matt Nadareski
e2110e80c0 Add keys.bin order for now 2025-04-02 11:52:18 -04:00
Matt Nadareski
b0f2c6658a Add keys.conf mappings to README 2025-04-02 11:50:24 -04:00
Matt Nadareski
baf0b9045e Remove old note, add new note 2025-04-02 11:15:37 -04:00
Matt Nadareski
b82eb54aab Do something with the results 2025-04-02 11:10:48 -04:00
Matt Nadareski
7d541b6c4b Document unlicensed DS values 2025-04-02 10:50:38 -04:00
Matt Nadareski
9d72a25cc2 Relax validation on NDS 2025-04-01 20:25:07 -04:00
Matt Nadareski
79e8c1b6bf Add debug prefix for all key validation 2025-04-01 20:12:29 -04:00
Matt Nadareski
8f6df04e2c Remove weirdly-phrased time limitation 2025-04-01 16:54:24 -04:00
Matt Nadareski
466144f8c9 Move default config to root level 2025-04-01 16:50:52 -04:00
Matt Nadareski
629f2cc11e Fix old .NET build 2025-04-01 16:47:03 -04:00
Matt Nadareski
1ffd1a3bff Look for files in places with things (fixes #19) 2025-04-01 16:43:21 -04:00
Matt Nadareski
d4bcd62941 Add more config stuff to README 2025-04-01 14:12:25 -04:00
Matt Nadareski
702c31413b Add minimal documentation of new input 2025-04-01 14:06:31 -04:00
Matt Nadareski
d4a6e902cf Add NED validation 2025-04-01 13:58:55 -04:00
Matt Nadareski
c00e72506e Prefer config file, if exists (nodoc) 2025-04-01 13:46:34 -04:00
Matt Nadareski
7ccc160e90 Prepare configuration for future use (unused) 2025-04-01 13:30:18 -04:00
Matt Nadareski
068b83da8e Add default configuration file (unused) 2025-04-01 13:11:26 -04:00
Matt Nadareski
d429ea3e64 Remove now-obsolete TODO 2025-04-01 13:05:05 -04:00
Matt Nadareski
5bf1d85e05 Add key validation (unused) 2025-04-01 13:04:52 -04:00
Matt Nadareski
32b655b1f9 Reread decrypted ExeFS headers (fixes #20) 2025-04-01 12:33:05 -04:00
Matt Nadareski
e3a64ece96 Fix stray reference to old AES keys param 2025-04-01 11:59:19 -04:00
Matt Nadareski
74986afc38 Start wiring through configuration file 2025-04-01 11:58:23 -04:00
Matt Nadareski
3cc2bcf022 Remove support for old "citra" flag name 2025-04-01 11:45:13 -04:00
Matt Nadareski
ebd13e3728 Update README 2025-04-01 11:44:29 -04:00
Matt Nadareski
2d7a54ddb7 Create unused Configuration class 2025-04-01 11:42:25 -04:00
Matt Nadareski
60d989d899 Move more encryption vars to common location 2025-04-01 11:23:27 -04:00
Matt Nadareski
4de3cfe6fa Fix how conditions are used for references 2025-02-25 21:26:08 -05:00
Matt Nadareski
50034975c3 Add newline for DSTool info output 2025-02-19 21:15:58 -05:00
Matt Nadareski
ab4f076846 Use tabs before info lines for readability in multi-file scenarios 2025-02-19 21:10:52 -05:00
Matt Nadareski
08840055a4 The British are coming! 2025-02-19 20:55:55 -05:00
Matt Nadareski
02eafc688a Let info writing also write hashes 2025-02-19 15:51:46 -05:00
Matt Nadareski
db7c7ee273 Add simple info check (fixes #18) 2025-02-19 15:47:05 -05:00
Matt Nadareski
383e6bdfce Add info to interface; add start of implementations 2025-02-19 15:36:36 -05:00
Matt Nadareski
169bd48762 Add info skeleton 2025-02-19 15:32:28 -05:00
Matt Nadareski
a2ab3c25c9 Convert encrypt/decrypt bool to an enum 2025-02-19 15:30:59 -05:00
Matt Nadareski
abf0843d22 Update copyright 2024-12-30 21:32:22 -05:00
Matt Nadareski
ca64a2575e Remove unnecessary action step 2024-12-30 21:32:01 -05:00
Matt Nadareski
035760b5f5 Ensure .NET versions are installed for testing 2024-12-30 21:31:43 -05:00
Matt Nadareski
66e6a8cd4a Allow symbols to be packed 2024-12-30 21:31:01 -05:00
Matt Nadareski
d85e7656c4 Update packages 2024-12-30 21:29:34 -05:00
Matt Nadareski
942151b6d7 Add .cci as a recognized extension for 3DS 2024-12-13 09:51:39 -05:00
Matt Nadareski
d7a51c7798 Ensure publish script is executable 2024-12-06 12:26:44 -05:00
Matt Nadareski
bd86b95494 Use publish script and update README 2024-12-06 12:24:31 -05:00
Matt Nadareski
5562b403a4 Use wrappers to be more safe 2024-11-28 22:06:52 -05:00
Matt Nadareski
ead70df624 Support back to .NET Framework 2.0 2024-11-18 16:20:09 -05:00
Matt Nadareski
33e62e046b Make Core an unpublished package 2024-11-14 21:59:04 -05:00
Matt Nadareski
bb0b5a3d05 Remove all CIA references
The implementation was woefully incomplete to begin with, and if working with 3DS cart images has taught anything, making sure that it can deserialize properly needs to be done first. Once CIA support can be started again, it should be built from the ground up.
2024-11-14 21:53:31 -05:00
Matt Nadareski
6a094b2dd8 Bump version 2024-11-14 21:40:01 -05:00
Matt Nadareski
0e3d22020b Fix remaining offset and size issues 2024-11-14 21:39:20 -05:00
Matt Nadareski
695f0f44b6 Ensure ExeFS table doesn't finalize 2024-11-14 21:25:38 -05:00
Matt Nadareski
dc4bd15e04 Fix incorrect size being used 2024-11-14 21:17:35 -05:00
Matt Nadareski
ce54eb24e2 Fix more issues 2024-11-14 13:43:07 -05:00
Matt Nadareski
15961e7047 Wording change 2024-11-14 11:56:18 -05:00
Matt Nadareski
edd8ebc048 Update Serialization to 1.7.3 2024-11-14 11:44:41 -05:00
Matt Nadareski
23e9edbf69 Remove null assurances 2024-11-14 02:16:50 -05:00
Matt Nadareski
00ac5e1ca2 Split some steps for readability 2024-11-14 01:56:06 -05:00
Matt Nadareski
c79781a6d7 Keep fixing byte array math 2024-11-14 01:48:14 -05:00
Matt Nadareski
5aa50ee252 Fix IV and addition 2024-11-14 00:26:58 -05:00
Matt Nadareski
3fbaedd7d5 Reduce unnecessary methods 2024-11-13 21:15:44 -05:00
Matt Nadareski
bc31cb0f6a Start fixing issues, remove BigInteger 2024-11-13 21:11:26 -05:00
Matt Nadareski
3ef32748e9 Remove temp code 2024-11-13 14:51:16 -05:00
Matt Nadareski
41293ab7c5 Add byte array helpers 2024-11-13 14:41:32 -05:00
22 changed files with 1517 additions and 2070 deletions

40
.github/workflows/build_and_test.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Build and Test
on:
push:
branches: [ "master" ]
jobs:
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
- 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

View File

@@ -1,55 +0,0 @@
name: Build Program
on:
push:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
project: [NDecrypt]
runtime: [win-x86, win-x64, win-arm64, linux-x64, linux-arm64, osx-x64]
framework: [net9.0] #[net20, net35, net40, net452, net472, net48, netcoreapp3.1, net5.0, net6.0, net7.0, net8.0, net9.0]
conf: [Debug] #[Release, Debug]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet publish ${{ matrix.project }}/${{ matrix.project }}.csproj -f ${{ matrix.framework }} -r ${{ matrix.runtime }} -c ${{ matrix.conf == 'Release' && 'Release -p:DebugType=None -p:DebugSymbols=false' || 'Debug'}} --self-contained true --version-suffix ${{ github.sha }} ${{ (startsWith(matrix.framework, 'net5') || startsWith(matrix.framework, 'net6') || startsWith(matrix.framework, 'net7') || startsWith(matrix.framework, 'net8') || startsWith(matrix.framework, 'net9')) && '-p:PublishSingleFile=true' || ''}}
- name: Archive build
run: |
cd ${{ matrix.project }}/bin/${{ matrix.conf }}/${{ matrix.framework }}/${{ matrix.runtime }}/publish/
zip -r ${{ github.workspace }}/${{ matrix.project }}_${{ matrix.framework }}_${{ matrix.runtime }}_${{ matrix.conf }}.zip ./
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.project }}_${{ matrix.framework }}_${{ matrix.runtime }}_${{ matrix.conf }}
path: ${{ matrix.project }}_${{ matrix.framework }}_${{ matrix.runtime }}_${{ matrix.conf }}.zip
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: ${{ matrix.project }}_${{ matrix.framework }}_${{ matrix.runtime }}_${{ matrix.conf }}.zip
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True

View File

@@ -11,7 +11,10 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Build
run: dotnet build

View File

@@ -1,4 +1,4 @@
Copyright (c) 2018-2024 Matt Nadareski
Copyright (c) 2018-2025 Matt Nadareski
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@@ -1,921 +0,0 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.N3DS;
using static NDecrypt.Core.CommonOperations;
using N3DSDeserializer = SabreTools.Serialization.Deserializers.N3DS;
namespace NDecrypt.Core
{
// https://www.3dbrew.org/wiki/CIA
public class CIATool : ITool
{
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs _decryptArgs;
/// <summary>
/// Indicates if development images are expected
/// </summary>
private readonly bool _development;
/// <summary>
/// Set of all partition keys
/// </summary>
private readonly PartitionKeys[] KeysMap = new PartitionKeys[8];
public CIATool(bool development, DecryptArgs decryptArgs)
{
_development = development;
_decryptArgs = decryptArgs;
}
/// <inheritdoc/>
public bool EncryptFile(string filename, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
{
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
try
{
// 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);
// Deserialize the CIA information
var cia = ReadCIA(input);
if (cia == null)
{
Console.WriteLine("Error: Not a 3DS CIA!");
return false;
}
// Encrypt all 8 NCCH partitions
EncryptAllPartitions(cia, force, input, output);
return false;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid 3DS CIA file and try again.");
return false;
}
}
/// <inheritdoc/>
public bool DecryptFile(string filename, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
{
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
try
{
// 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);
// Deserialize the CIA information
var cia = ReadCIA(input);
if (cia == null)
{
Console.WriteLine("Error: Not a 3DS CIA!");
return false;
}
// Decrypt all 8 NCCH partitions
DecryptAllPartitions(cia, force, input, output);
return false;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid 3DS CIA file and try again.");
return false;
}
}
#region Decrypt
/// <summary>
/// Decrypt all partitions in the content file data of a CIA header
/// </summary>
/// <param name="cia">CIA representing the 3DS CIA 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(CIA cia, bool force, Stream input, Stream output)
{
// Check the partitions table
if (cia.Partitions == null)
{
Console.WriteLine("Invalid partitions table!");
return;
}
// Iterate over all 8 NCCH partitions
for (int p = 0; p < cia.Partitions.Length; p++)
{
// Check the partition exists
var header = cia.Partitions[0];
if (header == null)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Decrypt the partition, if possible
if (ShouldDecryptPartition(cia, p, force))
DecryptPartition(header, p, input, output);
}
}
/// <summary>
/// Determine if the current partition should be decrypted
/// </summary>
private static bool ShouldDecryptPartition(CIA cia, int index, bool force)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine($"Partition {index} is not verified due to force flag being set.");
return true;
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (cia.Partitions![index]!.Flags!.PossblyDecrypted())
{
Console.WriteLine($"Partition {index}: Already Decrypted?...");
return false;
}
// By default, it passes
return true;
}
/// <summary>
/// Decrypt a single partition
/// </summary>
/// <param name="header">NCCH header representing the partition</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(NCCHHeader header, int index, Stream input, Stream output)
{
// Get the table entry -- TODO: Fix this to get the real entry
var tableEntry = new PartitionTableEntry();
// Determine the keys needed for this partition
SetDecryptionKeys(header, index);
// Decrypt the parts of the partition
DecryptExtendedHeader(header, index, tableEntry, input, output);
DecryptExeFS(header, index, tableEntry, input, output);
DecryptRomFS(header, index, tableEntry, input, output);
// Update the flags
UpdateDecryptCryptoAndMasks(header, tableEntry, output);
}
/// <summary>
/// Determine the set of keys to be used for decryption
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
private void SetDecryptionKeys(NCCHHeader header, int index)
{
// Get partition-specific values
byte[]? rsaSignature = header.RSA2048Signature;
// Set the header to use based on mode
BitMasks masks = header.Flags!.BitMasks;
CryptoMethod method = header.Flags.CryptoMethod;
// Get the partition keys
KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
}
/// <summary>
/// Decrypt the extended header, if it exists
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExtendedHeader(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Get required offsets
uint mediaUnitSize = 0x200;
uint partitionOffset = GetPartitionOffset(tableEntry, mediaUnitSize);
if (partitionOffset == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint extHeaderSize = GetExtendedHeaderSize(header);
if (extHeaderSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Extended Header... Skipping...");
return false;
}
// Seek to the extended header
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Decrypting: ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, header.PlainIV());
// Process the extended header
PerformAESOperation(Constants.CXTExtendedDataHeaderLength, cipher, input, output, 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);
#endif
output.Flush();
return true;
}
/// <summary>
/// Decrypt the ExeFS, if it exists
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExeFS(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Validate the ExeFS
uint mediaUnitSize = 0x200;
uint exeFsOffset = GetExeFSOffset(header, tableEntry, mediaUnitSize) - mediaUnitSize;
if (exeFsOffset == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint exeFsSize = GetExeFSSize(header, mediaUnitSize);
if (exeFsSize == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
// Decrypt the filename table
DecryptExeFSFilenameTable(header, index, tableEntry, input, output);
// For all but the original crypto method, process each of the files in the table
if (header.Flags!.CryptoMethod != CryptoMethod.Original)
DecryptExeFSFileEntries(header, index, tableEntry, input, output);
// Seek to the ExeFS
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
int ctroffsetE = (int)(mediaUnitSize / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(header.ExeFSIV(), ctroffsetE);
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the decryption
PerformAESOperation(exeFsSize - mediaUnitSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting: {s}"));
return true;
}
/// <summary>
/// Decrypt the ExeFS Filename Table
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFilenameTable(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Get ExeFS offset
uint mediaUnitSize = 0x200;
uint exeFsOffset = GetExeFSOffset(header, tableEntry, mediaUnitSize);
if (exeFsOffset == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Seek to the ExeFS header
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.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, header.ExeFSIV());
// Process the filename table
PerformAESOperation(mediaUnitSize, cipher, input, output, 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);
#endif
output.Flush();
}
/// <summary>
/// Decrypt the ExeFS file entries
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFileEntries(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Get ExeFS offset
uint mediaUnitSize = 0x200;
uint exeFsHeaderOffset = GetExeFSOffset(header, tableEntry, mediaUnitSize);
if (exeFsHeaderOffset == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Get to the start of the files
uint exeFsFilesOffset = exeFsHeaderOffset + mediaUnitSize;
input.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input);
// If the header failed to read, log and return
if (exefsHeader == null)
{
Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping...");
return;
}
foreach (var fileHeader in exefsHeader.FileHeaders!)
{
// Only decrypt a file if it's a code binary
if (fileHeader == null || !fileHeader.IsCodeBinary())
continue;
// Create the ExeFS AES ciphers for this partition
uint ctroffset = (fileHeader.FileOffset + mediaUnitSize) / 0x10;
byte[] exefsIVWithOffsetForHeader = AddToByteArray(header.ExeFSIV(), (int)ctroffset);
var firstCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
// Seek to the file entry
input.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
// Setup and perform the encryption
uint exeFsSize = GetExeFSSize(header, mediaUnitSize);
PerformAESOperation(exeFsSize,
firstCipher,
secondCipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting: {fileHeader.FileName}...{s}"));
}
}
/// <summary>
/// Decrypt the RomFS, if it exists
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptRomFS(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Validate the RomFS
uint mediaUnitSize = 0x200;
uint romFsOffset = GetRomFSOffset(header, tableEntry, mediaUnitSize);
if (romFsOffset == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
uint romFsSize = GetRomFSSize(header, mediaUnitSize);
if (romFsSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
// Seek to the RomFS
input.Seek(romFsOffset, SeekOrigin.Begin);
output.Seek(romFsOffset, SeekOrigin.Begin);
// Create the RomFS AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey, header.RomFSIV());
// Setup and perform the decryption
PerformAESOperation(romFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Decrypting: {s}"));
return true;
}
/// <summary>
/// Update the CryptoMethod and BitMasks for the decrypted partition
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="output">Stream representing the output</param>
private void UpdateDecryptCryptoAndMasks(NCCHHeader header,
PartitionTableEntry tableEntry,
Stream output)
{
// TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value?
uint mediaUnitSize = 0x200; // ncsdHeader.MediaUnitSize;
// Write the new CryptoMethod
output.Seek((tableEntry.Offset * mediaUnitSize) + 0x18B, SeekOrigin.Begin);
output.Write((byte)CryptoMethod.Original);
output.Flush();
// Write the new BitMasks flag
output.Seek((tableEntry.Offset * mediaUnitSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = header.Flags!.BitMasks;
flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag |= BitMasks.NoCrypto;
output.Write((byte)flag);
output.Flush();
}
#endregion
#region Encrypt
/// <summary>
/// Encrypt all partitions in the content file data of a CIA header
/// </summary>
/// <param name="cia">CIA representing the 3DS CIA 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(CIA cia, bool force, Stream input, Stream output)
{
// Check the partitions table
if (cia.Partitions == null)
{
Console.WriteLine("Invalid partitions table!");
return;
}
// Iterate over all 8 NCCH partitions
for (int p = 0; p < cia.Partitions.Length; p++)
{
// Check the partition exists
var header = cia.Partitions[0];
if (header == null)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Encrypt the partition, if possible
if (ShouldEncryptPartition(cia, p, force))
EncryptPartition(header, p, input, output);
}
}
/// <summary>
/// Determine if the current partition should be encrypted
/// </summary>
private static bool ShouldEncryptPartition(CIA cia, int index, bool force)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine($"Partition {index} is not verified due to force flag being set.");
return true;
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (!cia.Partitions![index]!.Flags!.PossblyDecrypted())
{
Console.WriteLine($"Partition {index}: Already Encrypted?...");
return false;
}
// By default, it passes
return true;
}
/// <summary>
/// Encrypt a single partition
/// </summary>
/// <param name="header">NCCH header representing the partition</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(NCCHHeader header, int index, Stream input, Stream output)
{
// Get the table entry -- TODO: Fix this to get the real entry
var tableEntry = new PartitionTableEntry();
// Determine the keys needed for this partition
SetEncryptionKeys(header, index);
// Encrypt the parts of the partition
EncryptExtendedHeader(header, index, tableEntry, input, output);
EncryptExeFS(header, index, tableEntry, input, output);
EncryptRomFS(header, index, tableEntry, input, output);
// Update the flags
UpdateEncryptCryptoAndMasks(header, index, tableEntry, output);
}
/// <summary>
/// Determine the set of keys to be used for encryption
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
private void SetEncryptionKeys(NCCHHeader header, int index)
{
// Get partition-specific values
byte[]? rsaSignature = header.RSA2048Signature;
// TODO: Figure out what sane defaults for these values are
// TODO: Can we actually re-encrypt a CIA?
// Set the header to use based on mode
BitMasks masks = BitMasks.NoCrypto; // ciaHeader.BackupHeader.Flags.BitMasks;
CryptoMethod method = CryptoMethod.Original; // ciaHeader.BackupHeader.Flags.CryptoMethod;
// Get the partition keys
KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
}
/// <summary>
/// Encrypt the extended header, if it exists
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExtendedHeader(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Get required offsets
uint mediaUnitSize = 0x200;
uint partitionOffset = GetPartitionOffset(tableEntry, mediaUnitSize);
if (partitionOffset == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint extHeaderSize = GetExtendedHeaderSize(header);
if (extHeaderSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Extended Header... Skipping...");
return false;
}
// Seek to the extended header
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Encrypting: ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, header.PlainIV());
// Process the extended header
PerformAESOperation(Constants.CXTExtendedDataHeaderLength, cipher, input, output, 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);
#endif
output.Flush();
return true;
}
/// <summary>
/// Encrypt the ExeFS, if it exists
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExeFS(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Validate the ExeFS
uint mediaUnitSize = 0x200;
uint exeFsOffset = GetExeFSOffset(header, tableEntry, mediaUnitSize) - mediaUnitSize;
if (exeFsOffset == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint exeFsSize = GetExeFSSize(header, mediaUnitSize);
if (exeFsSize == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
// TODO: Determine how to figure out the original crypto method, if possible
// For all but the original crypto method, process each of the files in the table
//if (ciaHeader.BackupHeader.Flags.CryptoMethod != CryptoMethod.Original)
// EncryptExeFSFileEntries(header, index, tableEntry, reader, writer);
// Encrypt the filename table
EncryptExeFSFilenameTable(header, index, tableEntry, input, output);
// Seek to the ExeFS
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
int ctroffsetE = (int)(mediaUnitSize / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(header.ExeFSIV(), ctroffsetE);
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the decryption
PerformAESOperation(exeFsSize - mediaUnitSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting: {s}"));
return true;
}
/// <summary>
/// Encrypt the ExeFS Filename Table
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFilenameTable(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Get ExeFS offset
uint mediaUnitSize = 0x200;
uint exeFsOffset = GetExeFSOffset(header, tableEntry, mediaUnitSize);
if (exeFsOffset == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Seek to the ExeFS header
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.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, header.ExeFSIV());
// Process the filename table
PerformAESOperation(mediaUnitSize, cipher, input, output, 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);
#endif
output.Flush();
}
/// <summary>
/// Encrypt the ExeFS file entries
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFileEntries(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Get ExeFS offset
uint mediaUnitSize = 0x200;
uint exeFsHeaderOffset = GetExeFSOffset(header, tableEntry, mediaUnitSize);
if (exeFsHeaderOffset == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Get to the start of the files
uint exeFsFilesOffset = exeFsHeaderOffset + mediaUnitSize;
input.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input);
// If the header failed to read, log and return
if (exefsHeader == null)
{
Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping...");
return;
}
foreach (var fileHeader in exefsHeader.FileHeaders!)
{
// Only decrypt a file if it's a code binary
if (fileHeader == null || !fileHeader.IsCodeBinary())
continue;
// Create the ExeFS AES ciphers for this partition
uint ctroffset = (fileHeader.FileOffset + mediaUnitSize) / 0x10;
byte[] exefsIVWithOffsetForHeader = AddToByteArray(header.ExeFSIV(), (int)ctroffset);
var firstCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
// Seek to the file entry
input.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
// Setup and perform the encryption
uint exeFsSize = GetExeFSSize(header, mediaUnitSize);
PerformAESOperation(exeFsSize,
firstCipher,
secondCipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting: {fileHeader.FileName}...{s}"));
}
}
/// <summary>
/// Encrypt the RomFS, if it exists
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptRomFS(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream input,
Stream output)
{
// Validate the RomFS
uint mediaUnitSize = 0x200;
uint romFsOffset = GetRomFSOffset(header, tableEntry, mediaUnitSize);
if (romFsOffset == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
uint romFsSize = GetRomFSSize(header, mediaUnitSize);
if (romFsSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
// Seek to the RomFS
input.Seek(romFsOffset, SeekOrigin.Begin);
output.Seek(romFsOffset, SeekOrigin.Begin);
// Force setting encryption keys for partitions 1 and above
if (index > 0)
{
//var backupHeader = ciaHeader.BackupHeader;
KeysMap[index].SetRomFSValues((BitMasks)0x00);
}
// Create the RomFS AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey, header.RomFSIV());
// Setup and perform the decryption
PerformAESOperation(romFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Encrypting: {s}"));
return true;
}
/// <summary>
/// Update the CryptoMethod and BitMasks for the encrypted partition
/// </summary>
/// <param name="header">NCCH header representing the partition</param>
/// <param name="index">Index of the partition</param>
/// <param name="tableEntry">PartitionTableEntry header representing the partition</param>
/// <param name="output">Stream representing the output</param>
private void UpdateEncryptCryptoAndMasks(NCCHHeader header,
int index,
PartitionTableEntry tableEntry,
Stream output)
{
// TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value?
uint mediaUnitSize = 0x200; // ncsdHeader.MediaUnitSize;
// Write the new CryptoMethod
output.Seek((tableEntry.Offset * mediaUnitSize) + 0x18B, SeekOrigin.Begin);
// For partitions 1 and up, set crypto-method to 0x00
if (index > 0)
output.Write((byte)CryptoMethod.Original);
// TODO: Determine how to figure out the original crypto method, if possible
// If partition 0, restore crypto-method from backup flags
//else
// writer.Write((byte)ciaHeader.BackupHeader.Flags.CryptoMethod);
output.Flush();
// Write the new BitMasks flag
output.Seek((tableEntry.Offset * mediaUnitSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = header.Flags!.BitMasks;
flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF;
// TODO: Determine how to figure out the original crypto method, if possible
//flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & ciaHeader.BackupHeader.Flags.BitMasks;
output.Write((byte)flag);
output.Flush();
}
#endregion
#region Serialization
/// <summary>
/// Read from a stream and get a CIA header, if possible
/// </summary>
/// <param name="input">Stream representing the input</param>
/// <returns>CIA header object, null on error</returns>
private static CIA? ReadCIA(Stream input)
{
try
{
return SabreTools.Serialization.Deserializers.CIA.DeserializeStream(input);
}
catch
{
return null;
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,89 @@
using System.IO;
using Newtonsoft.Json;
namespace NDecrypt.Core
{
internal class Configuration
{
#region DS-Specific Fields
/// <summary>
/// Encryption data taken from woodsec
/// </summary>
public string? NitroEncryptionData { get; set; }
#endregion
#region 3DS-Specific Fields
/// <summary>
/// AES Hardware Constant
/// </summary>
/// <remarks>generator</remarks>
public string? AESHardwareConstant { get; set; }
/// <summary>
/// KeyX 0x18 (New 3DS 9.3)
/// </summary>
/// <remarks>slot0x18KeyX</remarks>
public string? KeyX0x18 { get; set; }
/// <summary>
/// Dev KeyX 0x18 (New 3DS 9.3)
/// </summary>
public string? DevKeyX0x18 { get; set; }
/// <summary>
/// KeyX 0x1B (New 3DS 9.6)
/// </summary>
/// <remarks>slot0x1BKeyX</remarks>
public string? KeyX0x1B { get; set; }
/// <summary>
/// Dev KeyX 0x1B New 3DS 9.6)
/// </summary>
public string? DevKeyX0x1B { get; set; }
/// <summary>
/// KeyX 0x25 (> 7.x)
/// </summary>
/// <remarks>slot0x25KeyX</remarks>
public string? KeyX0x25 { get; set; }
/// <summary>
/// Dev KeyX 0x25 (> 7.x)
/// </summary>
public string? DevKeyX0x25 { get; set; }
/// <summary>
/// KeyX 0x2C (< 6.x)
/// </summary>
/// <remarks>slot0x2CKeyX</remarks>
public string? KeyX0x2C { get; set; }
/// <summary>
/// Dev KeyX 0x2C (< 6.x)
/// </summary>
public string? DevKeyX0x2C { get; set; }
#endregion
public static Configuration? Create(string path)
{
// Ensure the file exists
if (!File.Exists(path))
return null;
// Parse the configuration directly
try
{
string contents = File.ReadAllText(path);
return JsonConvert.DeserializeObject<Configuration?>(contents);
}
catch
{
return null;
}
}
}
}

View File

@@ -1,282 +1,14 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.Nitro;
using NitroDeserializer = SabreTools.Serialization.Deserializers.Nitro;
using SabreTools.Serialization.Deserializers;
namespace NDecrypt.Core
{
public class DSTool : ITool
{
#region Constants
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 Encryption process variables
private uint[] _cardHash = new uint[0x412];
@@ -284,6 +16,16 @@ namespace NDecrypt.Core
#endregion
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs _decryptArgs;
public DSTool(DecryptArgs decryptArgs)
{
_decryptArgs = decryptArgs;
}
#region Encrypt
/// <inheritdoc/>
@@ -296,8 +38,8 @@ namespace NDecrypt.Core
using var writer = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = NitroDeserializer.DeserializeStream(reader);
if (cart == null)
var commonHeader = Nitro.ParseCommonHeader(reader);
if (commonHeader == null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
@@ -308,7 +50,7 @@ namespace NDecrypt.Core
_arg2 = new uint[3];
// Encrypt the secure area
EncryptSecureArea(cart, force, reader, writer);
EncryptSecureArea(commonHeader, force, reader, writer);
return true;
}
catch
@@ -322,11 +64,11 @@ namespace NDecrypt.Core
/// <summary>
/// Encrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="cart">Cart representing the DS file</param>
/// <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(Cart cart, bool force, Stream reader, Stream writer)
private void EncryptSecureArea(CommonHeader commonHeader, bool force, Stream reader, Stream writer)
{
// If we're forcing the operation, tell the user
if (force)
@@ -349,7 +91,7 @@ namespace NDecrypt.Core
}
}
EncryptARM9(cart.CommonHeader!, reader, writer);
EncryptARM9(commonHeader, reader, writer);
Console.WriteLine("File has been encrypted");
}
@@ -450,8 +192,8 @@ namespace NDecrypt.Core
using var writer = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = NitroDeserializer.DeserializeStream(reader);
if (cart == null)
var commonHeader = Nitro.ParseCommonHeader(reader);
if (commonHeader == null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
@@ -462,7 +204,7 @@ namespace NDecrypt.Core
_arg2 = new uint[3];
// Decrypt the secure area
DecryptSecureArea(cart, force, reader, writer);
DecryptSecureArea(commonHeader, force, reader, writer);
return true;
}
@@ -477,11 +219,11 @@ namespace NDecrypt.Core
/// <summary>
/// Decrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="cart">Cart representing the DS file</param>
/// <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(Cart cart, bool force, Stream reader, Stream writer)
private void DecryptSecureArea(CommonHeader commonHeader, bool force, Stream reader, Stream writer)
{
// If we're forcing the operation, tell the user
if (force)
@@ -504,7 +246,7 @@ namespace NDecrypt.Core
}
}
DecryptARM9(cart.CommonHeader!, reader, writer);
DecryptARM9(commonHeader, reader, writer);
Console.WriteLine("File has been decrypted");
}
@@ -585,6 +327,42 @@ namespace NDecrypt.Core
#endregion
#region Info
/// <inheritdoc/>
public string? GetInformation(string filename)
{
try
{
// Open the file for reading
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// 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)
sb.Append("Empty");
else if (decrypted == true)
sb.Append("Decrypted");
else
sb.Append("Encrypted");
// Return the status for the secure area
sb.Append(Environment.NewLine);
return sb.ToString();
}
catch (Exception ex)
{
Console.WriteLine(ex);
return null;
}
}
#endregion
#region Common
/// <summary>
@@ -642,6 +420,38 @@ namespace NDecrypt.Core
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;
}
@@ -652,7 +462,7 @@ namespace NDecrypt.Core
/// <param name="commonHeader">CommonHeader representing the DS file</param>
private void Init1(CommonHeader commonHeader)
{
Buffer.BlockCopy(NitroEncryptionData, 0, _cardHash, 0, 4 * (1024 + 18));
Buffer.BlockCopy(_decryptArgs.NitroEncryptionData, 0, _cardHash, 0, 4 * (1024 + 18));
_arg2 = [commonHeader.GameCode, commonHeader.GameCode >> 1, commonHeader.GameCode << 1];
Init2();
Init2();

View File

@@ -1,8 +1,8 @@
using System;
using System.IO;
using System.Numerics;
using SabreTools.IO.Extensions;
using SabreTools.IO.Readers;
using SabreTools.Matching;
namespace NDecrypt.Core
{
@@ -17,63 +17,504 @@ namespace NDecrypt.Core
#endregion
#region DS-Specific Fields
/// <summary>
/// Blowfish Table
/// </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
/// <summary>
/// AES Hardware Constant
/// </summary>
public BigInteger AESHardwareConstant { get; private set; }
#region Retail Keys
public byte[] AESHardwareConstant { get; private set; } = [];
/// <summary>
/// KeyX 0x18 (New 3DS 9.3)
/// </summary>
public BigInteger KeyX0x18 { get; private set; }
/// <summary>
/// KeyX 0x1B (New 3DS 9.6)
/// </summary>
public BigInteger KeyX0x1B { get; private set; }
/// <summary>
/// KeyX 0x25 (> 7.x)
/// </summary>
public BigInteger KeyX0x25 { get; private set; }
/// <summary>
/// KeyX 0x2C (< 6.x)
/// </summary>
public BigInteger KeyX0x2C { get; private set; }
#endregion
#region Development Keys
public byte[] KeyX0x18 { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x18 (New 3DS 9.3)
/// </summary>
public BigInteger DevKeyX0x18 { get; private set; }
public byte[] DevKeyX0x18 { get; private set; } = [];
/// <summary>
/// KeyX 0x1B (New 3DS 9.6)
/// </summary>
public byte[] KeyX0x1B { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x1B New 3DS 9.6)
/// </summary>
public BigInteger DevKeyX0x1B { get; private set; }
public byte[] DevKeyX0x1B { get; private set; } = [];
/// <summary>
/// KeyX 0x25 (> 7.x)
/// </summary>
public byte[] KeyX0x25 { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x25 (> 7.x)
/// </summary>
public BigInteger DevKeyX0x25 { get; private set; }
public byte[] DevKeyX0x25 { get; private set; } = [];
/// <summary>
/// KeyX 0x2C (< 6.x)
/// </summary>
public byte[] KeyX0x2C { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x2C (< 6.x)
/// </summary>
public BigInteger DevKeyX0x2C { get; private set; }
public byte[] DevKeyX0x2C { get; private set; } = [];
#endregion
#region Internal Test Values
/// <summary>
/// Expected hash for NitroEncryptionData
/// </summary>
private static readonly byte[] ExpectedNitroSha512Hash =
[
0x1A, 0xD6, 0x40, 0x21, 0xFC, 0x3D, 0x1A, 0x9A,
0x9B, 0xC0, 0x88, 0x8E, 0x2E, 0x68, 0xDE, 0x4E,
0x8A, 0x60, 0x6B, 0x86, 0x63, 0x22, 0xD2, 0xC7,
0xC6, 0xD7, 0xD6, 0xCE, 0x65, 0xF5, 0xBA, 0xA7,
0xEA, 0x69, 0x63, 0x7E, 0xC9, 0xE4, 0x57, 0x7B,
0x01, 0xFD, 0xCE, 0xC2, 0x26, 0x3B, 0xD9, 0x0D,
0x84, 0x57, 0xC2, 0x00, 0xB8, 0x56, 0x9F, 0xE5,
0x56, 0xDA, 0x8D, 0xDE, 0x84, 0xB8, 0x8E, 0xE4,
];
/// <summary>
/// Initial value for key validation tests
/// </summary>
private static readonly byte[] TestIV =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
];
/// <summary>
/// Pattern to use for key validation tests
/// </summary>
private static readonly byte[] TestPattern =
[
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
];
/// <summary>
/// Expected output value for KeyX0x18
/// </summary>
private static readonly byte[] ExpectedKeyX0x18 =
[
0x06, 0xF1, 0xB2, 0x3B, 0x12, 0xAD, 0x80, 0xC1,
0x13, 0xC6, 0x18, 0x3D, 0x27, 0xB8, 0xB9, 0x95,
0x49, 0x73, 0x59, 0x82, 0xEF, 0xFE, 0x16, 0x48,
0x91, 0x2A, 0x89, 0x55, 0x9A, 0xDC, 0x3C, 0xA0,
0x84, 0x46, 0x14, 0xE0, 0x16, 0x59, 0x8E, 0x4F,
0xC2, 0x6C, 0x52, 0xA4, 0x7D, 0xAD, 0x4F, 0x23,
0xF1, 0xC6, 0x99, 0x44, 0x39, 0xB7, 0x42, 0xF0,
0x1F, 0xBB, 0x02, 0xF6, 0x0A, 0x8A, 0xC2, 0x9A,
];
/// <summary>
/// Expected output value for DevKeyX0x18
/// </summary>
private static readonly byte[] ExpectedDevKeyX0x18 =
[
0x99, 0x6E, 0x3C, 0x54, 0x97, 0x3C, 0xEA, 0xE8,
0xBA, 0xAE, 0x18, 0x5C, 0x93, 0x27, 0x65, 0x50,
0xF6, 0x6D, 0x67, 0xD7, 0xEF, 0xBD, 0x7C, 0xCB,
0x8A, 0xC1, 0x1A, 0x54, 0xFC, 0x3B, 0x8B, 0x3A,
0x0E, 0xE5, 0xEF, 0x27, 0x4A, 0x73, 0x7E, 0x0A,
0x2E, 0x2E, 0x9D, 0xAF, 0x6C, 0x03, 0xF2, 0x91,
0xC4, 0xFA, 0x73, 0xFD, 0x6B, 0xA0, 0x07, 0xD4,
0x75, 0x5B, 0x6F, 0x2E, 0x8B, 0x68, 0x4C, 0xD1,
];
/// <summary>
/// Expected output value for KeyX0x1B
/// </summary>
private static readonly byte[] ExpectedKeyX0x1B =
[
0x0A, 0xE4, 0x79, 0x02, 0x1B, 0xFA, 0x25, 0x4B,
0x2D, 0x92, 0x4F, 0xA8, 0x41, 0x59, 0xCE, 0x10,
0x09, 0xE6, 0x08, 0x61, 0x23, 0xC7, 0xD2, 0x30,
0x84, 0x37, 0xD5, 0x49, 0x42, 0x94, 0xB2, 0x70,
0x6A, 0xF3, 0x75, 0xB0, 0x1F, 0x4F, 0xA1, 0xCE,
0x03, 0xA2, 0x6A, 0x19, 0x5D, 0x32, 0x0D, 0xB5,
0x79, 0xCD, 0xFD, 0xF0, 0xDE, 0x49, 0x26, 0x2D,
0x29, 0x36, 0x30, 0x69, 0x8B, 0x45, 0xE1, 0xFC,
];
/// <summary>
/// Expected output value for DevKeyX0x1B
/// </summary>
private static readonly byte[] ExpectedDevKeyX0x1B =
[
0x16, 0x4F, 0xD9, 0x58, 0xC9, 0x20, 0xB3, 0xED,
0xC4, 0xEB, 0x57, 0x39, 0x10, 0xEF, 0xA8, 0xCC,
0xE5, 0x49, 0xBF, 0x52, 0x10, 0xA9, 0xCC, 0xE1,
0x65, 0x3B, 0x2D, 0x51, 0x45, 0xFB, 0x60, 0x52,
0x3E, 0x29, 0xEB, 0xEB, 0x3F, 0xF2, 0x76, 0x08,
0x00, 0x05, 0x7F, 0x64, 0x29, 0x4A, 0x17, 0x22,
0x56, 0x7F, 0x49, 0x94, 0x1A, 0x8C, 0x56, 0x35,
0x38, 0xBE, 0xA4, 0x2E, 0x58, 0xD3, 0x81, 0x8C,
];
/// <summary>
/// Expected output value for KeyX0x25
/// </summary>
private static readonly byte[] ExpectedKeyX0x25 =
[
0x37, 0xBC, 0x73, 0xD6, 0xEE, 0x73, 0xE0, 0x94,
0x42, 0x84, 0x74, 0xE5, 0xD8, 0xFB, 0x5F, 0x65,
0xF4, 0xCF, 0x2E, 0xC1, 0x43, 0x48, 0x6C, 0xAA,
0xC8, 0xF9, 0x96, 0xE6, 0x33, 0xDD, 0xE7, 0xBF,
0xD2, 0x21, 0x89, 0x39, 0x13, 0xD1, 0xEC, 0xCA,
0x1D, 0x5D, 0x1F, 0x77, 0x95, 0xD2, 0x8B, 0x27,
0x92, 0x79, 0xC5, 0x1D, 0x72, 0xA7, 0x28, 0x57,
0x41, 0x0E, 0x46, 0xB8, 0x80, 0x7B, 0x7C, 0x0D,
];
/// <summary>
/// Expected output value for DevKeyX0x25
/// </summary>
private static readonly byte[] ExpectedDevKeyX0x25 =
[
0x71, 0x65, 0x30, 0xF2, 0x68, 0xEC, 0x65, 0x0A,
0x8C, 0x9E, 0xC5, 0x5A, 0xFA, 0x37, 0x8E, 0xDA,
0x7B, 0x58, 0x3B, 0x66, 0x7C, 0x9D, 0x16, 0xD9,
0x2D, 0x8F, 0xCF, 0x04, 0x66, 0x7F, 0x27, 0x41,
0xBF, 0x5F, 0x1E, 0x11, 0x4C, 0xD6, 0xB9, 0x0A,
0xC5, 0x42, 0xCF, 0x2B, 0x87, 0x6B, 0xD4, 0x72,
0x4D, 0x9C, 0x29, 0x2E, 0xF8, 0xB0, 0x6F, 0x22,
0x35, 0x5B, 0x96, 0x83, 0xD1, 0xE4, 0x5E, 0xDB,
];
/// <summary>
/// Expected output value for KeyX0x2C
/// </summary>
private static readonly byte[] ExpectedKeyX0x2C =
[
0xAE, 0x44, 0x20, 0xDB, 0xA5, 0x96, 0xDC, 0xF3,
0xD8, 0x23, 0x9E, 0x3C, 0x44, 0x73, 0x3D, 0xCD,
0x07, 0xD5, 0xF8, 0xD0, 0xC6, 0xB3, 0x5A, 0x80,
0xB5, 0x5A, 0x55, 0x30, 0x5D, 0x4A, 0xBE, 0x61,
0xBF, 0xEF, 0x64, 0x17, 0x28, 0xD6, 0x26, 0x52,
0x42, 0x4D, 0x8F, 0x1C, 0xBC, 0x63, 0xD3, 0x91,
0x7D, 0xA6, 0x4F, 0xAF, 0x26, 0x38, 0x60, 0xEE,
0x79, 0x92, 0x2F, 0xD8, 0xCA, 0x4E, 0xE7, 0xEC,
];
/// <summary>
/// Expected output value for DevKeyX0x2C
/// </summary>
private static readonly byte[] ExpectedDevKeyX0x2C =
[
0x5F, 0x73, 0xD5, 0x9A, 0x67, 0xFF, 0x8C, 0x12,
0x31, 0x58, 0x0B, 0x58, 0x46, 0xFE, 0x05, 0x16,
0x92, 0xE4, 0x84, 0x06, 0x18, 0x9B, 0x58, 0x91,
0xE7, 0xF8, 0xCD, 0xA9, 0x95, 0xAC, 0x07, 0xCD,
0x43, 0x20, 0x7A, 0x8C, 0xCC, 0xAB, 0x48, 0x50,
0x29, 0x2F, 0x96, 0x73, 0xB0, 0xD9, 0xE5, 0xCB,
0xE6, 0x9A, 0x0D, 0xF7, 0xD0, 0x1E, 0xC2, 0xEC,
0xC1, 0xE2, 0x8E, 0xEE, 0x89, 0xB9, 0xB1, 0x97,
];
#endregion
/// <summary>
/// Setup all of the necessary constants
/// </summary>
/// <param name="keyfile">Path to the keyfile</param>
public DecryptArgs(string? config)
{
InitConfigJson(config);
ValidateKeys();
}
/// <summary>
/// Setup all of the necessary constants
/// </summary>
@@ -81,11 +522,17 @@ namespace NDecrypt.Core
/// <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>
@@ -103,42 +550,35 @@ namespace NDecrypt.Core
try
{
using var reader = new IniReader(keyfile);
// This is required to preserve sign for BigInteger
byte[] signByte = [0x00];
while (reader.ReadNextLine())
{
// Ignore comments in the file
if (reader.RowType == IniRowType.Comment)
continue;
if (reader.KeyValuePair == null || string.IsNullOrWhiteSpace(reader.KeyValuePair?.Key))
if (reader.KeyValuePair == null || string.IsNullOrEmpty(reader.KeyValuePair?.Key))
break;
var kvp = reader.KeyValuePair!.Value;
byte[] value = StringToByteArray(kvp.Value);
Array.Reverse(value);
byte[] valueWithSign = [.. value, .. signByte];
switch (kvp.Key)
{
// Hardware constant
case "generator":
AESHardwareConstant = new BigInteger(value);
AESHardwareConstant = value;
break;
// Retail Keys
case "slot0x18KeyX":
KeyX0x18 = new BigInteger(valueWithSign);
KeyX0x18 = value;
break;
case "slot0x1BKeyX":
KeyX0x1B = new BigInteger(valueWithSign);
KeyX0x1B = value;
break;
case "slot0x25KeyX":
KeyX0x25 = new BigInteger(valueWithSign);
KeyX0x25 = value;
break;
case "slot0x2CKeyX":
KeyX0x2C = new BigInteger(valueWithSign);
KeyX0x2C = value;
break;
// Currently Unused KeyX
@@ -208,6 +648,41 @@ namespace NDecrypt.Core
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))
{
IsReady = false;
return;
}
// Try to read the configuration file
var configObj = Configuration.Create(config);
if (configObj == 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>
@@ -225,23 +700,29 @@ namespace NDecrypt.Core
{
using Stream reader = File.Open(keyfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// This is required to preserve sign for BigInteger
byte[] signByte = [0x00];
// Hardware constant
AESHardwareConstant = new BigInteger(reader.ReadBytes(16));
AESHardwareConstant = reader.ReadBytes(16);
Array.Reverse(AESHardwareConstant);
// Retail keys
KeyX0x18 = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
KeyX0x1B = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
KeyX0x25 = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
KeyX0x2C = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
KeyX0x18 = reader.ReadBytes(16);
Array.Reverse(KeyX0x18);
KeyX0x1B = reader.ReadBytes(16);
Array.Reverse(KeyX0x1B);
KeyX0x25 = reader.ReadBytes(16);
Array.Reverse(KeyX0x25);
KeyX0x2C = reader.ReadBytes(16);
Array.Reverse(KeyX0x2C);
// Development keys
DevKeyX0x18 = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
DevKeyX0x1B = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
DevKeyX0x25 = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
DevKeyX0x2C = new BigInteger([.. reader.ReadBytes(16), .. signByte]);
DevKeyX0x18 = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x18);
DevKeyX0x1B = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x1B);
DevKeyX0x25 = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x25);
DevKeyX0x2C = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x2C);
}
catch
{
@@ -252,9 +733,128 @@ namespace NDecrypt.Core
IsReady = true;
}
// 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)
/// <summary>
/// Validate that all keys provided are going to be valid
/// </summary>
/// <remarks>Does not know what the keys are, just the result</remarks>
private void ValidateKeys()
{
// NitroEncryptionData
if (NitroEncryptionData.Length > 0)
{
using var hasher = System.Security.Cryptography.SHA512.Create();
byte[] actual = hasher.ComputeHash(NitroEncryptionData);
if (!Extensions.EqualsExactly(ExpectedNitroSha512Hash, actual))
{
Console.WriteLine($"NitroEncryptionData invalid value, disabling...");
NitroEncryptionData = [];
}
}
// KeyX0x18
if (KeyX0x18.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x18, actual))
{
Console.WriteLine($"KeyX0x18 invalid value, disabling...");
KeyX0x18 = [];
}
}
// DevKeyX0x18
if (DevKeyX0x18.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x18, actual))
{
Console.WriteLine($"DevKeyX0x18 invalid value, disabling...");
DevKeyX0x18 = [];
}
}
// KeyX0x1B
if (KeyX0x1B.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x1B, actual))
{
Console.WriteLine($"KeyX0x1B invalid value, disabling...");
KeyX0x1B = [];
}
}
// DevKeyX0x1B
if (DevKeyX0x1B.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x1B, actual))
{
Console.WriteLine($"DevKeyX0x1B invalid value, disabling...");
DevKeyX0x1B = [];
}
}
// KeyX0x25
if (KeyX0x25.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x25, actual))
{
Console.WriteLine($"KeyX0x25 invalid value, disabling...");
KeyX0x25 = [];
}
}
// DevKeyX0x25
if (DevKeyX0x25.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x25, actual))
{
Console.WriteLine($"DevKeyX0x25 invalid value, disabling...");
DevKeyX0x25 = [];
}
}
// KeyX0x2C
if (KeyX0x2C.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x2C, actual))
{
Console.WriteLine($"KeyX0x2C invalid value, disabling...");
KeyX0x2C = [];
}
}
// DevKeyX0x2C
if (DevKeyX0x2C.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x2C, actual))
{
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)

View File

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

View File

@@ -17,5 +17,12 @@
/// <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);
/// <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);
}
}

View File

@@ -2,19 +2,21 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>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</TargetFrameworks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<IncludeSymbols>true</IncludeSymbols>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.3.0</VersionPrefix>
<VersionPrefix>0.3.2</VersionPrefix>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Common code for all NDecrypt processors</Description>
<Copyright>Copyright (c) Matt Nadareski 2019-2024</Copyright>
<Copyright>Copyright (c) Matt Nadareski 2019-2025</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<RepositoryUrl>https://github.com/SabreTools/NDecrypt</RepositoryUrl>
@@ -35,19 +37,13 @@
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<!-- Support for old .NET versions -->
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net40`))">
<PackageReference Include="BouncyCastle.NetCore" Version="1.9.0" />
</ItemGroup>
<ItemGroup Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`))">
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.Hashing" Version="1.4.0" />
<PackageReference Include="SabreTools.IO" Version="1.5.0" />
<PackageReference Include="SabreTools.Models" Version="1.5.0" />
<PackageReference Include="SabreTools.Serialization" Version="1.7.0" />
<PackageReference Include="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" />
</ItemGroup>
</Project>

View File

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

View File

@@ -1,9 +1,11 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.N3DS;
using SabreTools.Serialization.Wrappers;
using static NDecrypt.Core.CommonOperations;
using N3DSDeserializer = SabreTools.Serialization.Deserializers.N3DS;
using static SabreTools.Models.N3DS.Constants;
namespace NDecrypt.Core
{
@@ -30,41 +32,7 @@ namespace NDecrypt.Core
_decryptArgs = decryptArgs;
}
/// <inheritdoc/>
public bool EncryptFile(string filename, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
{
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
try
{
// 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);
// Deserialize the cart information
var cart = N3DSDeserializer.DeserializeStream(input);
if (cart?.Header == null || cart?.CardInfoHeader?.InitialData?.BackupHeader == null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
}
// Encrypt all 8 NCCH partitions
EncryptAllPartitions(cart, force, input, output);
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} 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;
}
}
#region Decrypt
/// <inheritdoc/>
public bool DecryptFile(string filename, bool force)
@@ -83,8 +51,8 @@ namespace NDecrypt.Core
using var output = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DSDeserializer.DeserializeStream(input);
if (cart?.Header == null || cart?.CardInfoHeader?.InitialData?.BackupHeader == null)
var cart = N3DS.Create(input);
if (cart?.Model == null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
@@ -102,8 +70,6 @@ namespace NDecrypt.Core
}
}
#region Decrypt
/// <summary>
/// Decrypt all partitions in the partition table of an NCSD header
/// </summary>
@@ -111,10 +77,10 @@ namespace NDecrypt.Core
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptAllPartitions(Cart cart, bool force, Stream input, Stream output)
private void DecryptAllPartitions(N3DS cart, bool force, Stream input, Stream output)
{
// Check the partitions table
if (cart.Header?.PartitionsTable == null || cart.Partitions == null)
if (cart.PartitionsTable == null || cart.Partitions == null)
{
Console.WriteLine("Invalid partitions table!");
return;
@@ -123,13 +89,21 @@ namespace NDecrypt.Core
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
// Check the partition exists
if (cart.Partitions[p] == null)
var partition = cart.Partitions[p];
if (partition == null || partition.MagicID != NCCHMagicNumber)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Check the partition has data
var partitionEntry = cart.PartitionsTable[p];
if (partitionEntry == null || partitionEntry.Length == 0)
{
Console.WriteLine($"Partition {p} No data... Skipping...");
continue;
}
// Decrypt the partition, if possible
if (ShouldDecryptPartition(cart, p, force))
DecryptPartition(cart, p, input, output);
@@ -139,7 +113,7 @@ namespace NDecrypt.Core
/// <summary>
/// Determine if the current partition should be decrypted
/// </summary>s
private static bool ShouldDecryptPartition(Cart cart, int index, bool force)
private static bool ShouldDecryptPartition(N3DS cart, int index, bool force)
{
// If we're forcing the operation, tell the user
if (force)
@@ -148,7 +122,7 @@ namespace NDecrypt.Core
return true;
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (cart.Partitions![index]!.Flags!.PossblyDecrypted())
else if (cart.PossiblyDecrypted(index))
{
Console.WriteLine($"Partition {index}: Already Decrypted?...");
return false;
@@ -165,7 +139,7 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptPartition(Cart cart, int index, Stream input, Stream output)
private void DecryptPartition(N3DS cart, int index, Stream input, Stream output)
{
// Determine the keys needed for this partition
SetDecryptionKeys(cart, index);
@@ -184,19 +158,17 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
private void SetDecryptionKeys(Cart cart, int index)
private void SetDecryptionKeys(N3DS cart, int index)
{
// Get the partition
var partition = cart.Partitions?[index];
if (partition == null)
if (partition?.Flags == null)
return;
// Get partition-specific values
byte[]? rsaSignature = partition.RSA2048Signature;
// Set the header to use based on mode
BitMasks masks = partition.Flags!.BitMasks;
CryptoMethod method = partition.Flags!.CryptoMethod;
BitMasks masks = cart.GetBitMasks(index);
CryptoMethod method = cart.GetCryptoMethod(index);
// Get the partition keys
KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
@@ -209,20 +181,20 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExtendedHeader(Cart cart, int index, Stream input, Stream output)
private bool DecryptExtendedHeader(N3DS cart, int index, Stream input, Stream output)
{
// Get required offsets
uint partitionOffset = GetPartitionOffset(cart, index);
if (partitionOffset == 0)
uint partitionOffset = cart.GetPartitionOffset(index);
if (partitionOffset == 0 || partitionOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
Console.WriteLine($"Partition {index} No Data... Skipping...");
return false;
}
uint extHeaderSize = GetExtendedHeaderSize(cart, index);
uint extHeaderSize = cart.GetExtendedHeaderSize(index);
if (extHeaderSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Extended Header... Skipping...");
Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
return false;
}
@@ -230,7 +202,7 @@ namespace NDecrypt.Core
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Decrypting: ExHeader");
Console.WriteLine($"Partition {index}: Decrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, cart.PlainIV(index));
@@ -253,17 +225,17 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExeFS(Cart cart, int index, Stream input, Stream output)
private bool DecryptExeFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the ExeFS
uint exeFsOffset = GetExeFSOffset(cart, index);
if (exeFsOffset == 0)
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint exeFsSize = GetExeFSSize(cart, index);
uint exeFsSize = cart.GetExeFSSize(index);
if (exeFsSize == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
@@ -274,24 +246,28 @@ namespace NDecrypt.Core
DecryptExeFSFilenameTable(cart, index, input, output);
// For all but the original crypto method, process each of the files in the table
if (cart.Partitions![index]!.Flags!.CryptoMethod != CryptoMethod.Original)
if (cart.GetCryptoMethod(index) != CryptoMethod.Original)
DecryptExeFSFileEntries(cart, index, input, output);
// Get the ExeFS files offset
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Seek to the ExeFS
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
input.Seek(exeFsFilesOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
int ctroffsetE = (int)(cart.MediaUnitSize() / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(cart.ExeFSIV(index), ctroffsetE);
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = Add(cart.ExeFSIV(index), ctroffsetE);
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the decryption
exeFsSize -= cart.MediaUnitSize;
PerformAESOperation(exeFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting: {s}"));
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
return true;
}
@@ -303,11 +279,11 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFilenameTable(Cart cart, int index, Stream input, Stream output)
private void DecryptExeFSFilenameTable(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsOffset = GetExeFSOffset(cart, index);
if (exeFsOffset == 0)
uint exeFsOffset = cart.GetExeFSOffset(index);
if (exeFsOffset == 0 || exeFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
@@ -317,13 +293,15 @@ namespace NDecrypt.Core
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Decrypting: ExeFS Filename Table");
Console.WriteLine($"Partition {index} ExeFS: Decrypting - ExeFS Filename Table");
// Create the ExeFS AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
PerformAESOperation(cart.MediaUnitSize(), cipher, input, output, null);
byte[] readBytes = input.ReadBytes((int)cart.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.Write(processedBytes);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
@@ -339,37 +317,45 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFileEntries(Cart cart, int index, Stream input, Stream output)
private void DecryptExeFSFileEntries(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsHeaderOffset = GetExeFSOffset(cart, index);
if (exeFsHeaderOffset == 0)
if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Get to the start of the files
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize();
// Reread the decrypted ExeFS header
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
input.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input);
cart.ExeFSHeaders[index] = SabreTools.Serialization.Deserializers.N3DS.ParseExeFSHeader(input);
// If the header failed to read, log and return
if (exefsHeader == null)
// Get the ExeFS header
var exeFsHeader = cart.ExeFSHeaders[index];
if (exeFsHeader?.FileHeaders == null)
{
Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping...");
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
}
foreach (var fileHeader in exefsHeader.FileHeaders!)
// Get the ExeFS files offset
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Loop through and process all headers
for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
{
// Only decrypt a file if it's a code binary
if (fileHeader == null || !fileHeader.IsCodeBinary())
// Only attempt to process code binary files
if (!cart.IsCodeBinary(index, i))
continue;
// Get the file header
var fileHeader = exeFsHeader.FileHeaders[i];
if (fileHeader == null)
continue;
// Create the ExeFS AES ciphers for this partition
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize()) / 0x10;
byte[] exefsIVWithOffsetForHeader = AddToByteArray(cart.ExeFSIV(index), (int)ctroffset);
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize) / 0x10;
byte[] exefsIVWithOffsetForHeader = Add(cart.ExeFSIV(index), ctroffset);
var firstCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
@@ -378,13 +364,12 @@ namespace NDecrypt.Core
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
// Setup and perform the encryption
uint exeFsSize = GetExeFSSize(cart, index);
PerformAESOperation(exeFsSize,
PerformAESOperation(fileHeader.FileSize,
firstCipher,
secondCipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting: {fileHeader.FileName}...{s}"));
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
}
}
@@ -395,17 +380,17 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptRomFS(Cart cart, int index, Stream input, Stream output)
private bool DecryptRomFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the RomFS
uint romFsOffset = GetRomFSOffset(cart, index);
if (romFsOffset == 0)
uint romFsOffset = cart.GetRomFSOffset(index);
if (romFsOffset == 0 || romFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
uint romFsSize = GetRomFSSize(cart, index);
uint romFsSize = cart.GetRomFSSize(index);
if (romFsSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
@@ -424,7 +409,7 @@ namespace NDecrypt.Core
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Decrypting: {s}"));
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
return true;
}
@@ -435,10 +420,10 @@ namespace NDecrypt.Core
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="output">Stream representing the output</param>
private static void UpdateDecryptCryptoAndMasks(Cart cart, int index, Stream output)
private static void UpdateDecryptCryptoAndMasks(N3DS cart, int index, Stream output)
{
// Get required offsets
uint partitionOffset = GetPartitionOffset(cart, index);
uint partitionOffset = cart.GetPartitionOffset(index);
// Seek to the CryptoMethod location
output.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
@@ -451,7 +436,7 @@ namespace NDecrypt.Core
output.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
// Write the new BitMasks flag
BitMasks flag = cart.Partitions![index]!.Flags!.BitMasks;
BitMasks flag = cart.GetBitMasks(index);
flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag |= BitMasks.NoCrypto;
output.Write((byte)flag);
@@ -462,6 +447,42 @@ namespace NDecrypt.Core
#region Encrypt
/// <inheritdoc/>
public bool EncryptFile(string filename, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
{
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
try
{
// 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);
// Deserialize the cart information
var cart = N3DS.Create(input);
if (cart?.Model == null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
}
// Encrypt all 8 NCCH partitions
EncryptAllPartitions(cart, force, input, output);
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} 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;
}
}
/// <summary>
/// Encrypt all partitions in the partition table of an NCSD header
/// </summary>
@@ -469,10 +490,10 @@ namespace NDecrypt.Core
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptAllPartitions(Cart cart, bool force, Stream input, Stream output)
private void EncryptAllPartitions(N3DS cart, bool force, Stream input, Stream output)
{
// Check the partitions table
if (cart.Header?.PartitionsTable == null || cart.Partitions == null)
if (cart.PartitionsTable == null || cart.Partitions == null)
{
Console.WriteLine("Invalid partitions table!");
return;
@@ -482,12 +503,21 @@ namespace NDecrypt.Core
for (int p = 0; p < 8; p++)
{
// Check the partition exists
if (cart.Partitions[p] == null)
var partition = cart.Partitions[p];
if (partition == null || partition.MagicID != NCCHMagicNumber)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Check the partition has data
var partitionEntry = cart.PartitionsTable[p];
if (partitionEntry == null || partitionEntry.Length == 0)
{
Console.WriteLine($"Partition {p} No data... Skipping...");
continue;
}
// Encrypt the partition, if possible
if (ShouldEncryptPartition(cart, p, force))
EncryptPartition(cart, p, input, output);
@@ -497,7 +527,7 @@ namespace NDecrypt.Core
/// <summary>
/// Determine if the current partition should be encrypted
/// </summary>
private static bool ShouldEncryptPartition(Cart cart, int index, bool force)
private static bool ShouldEncryptPartition(N3DS cart, int index, bool force)
{
// If we're forcing the operation, tell the user
if (force)
@@ -506,7 +536,7 @@ namespace NDecrypt.Core
return true;
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (!cart.Partitions![index]!.Flags!.PossblyDecrypted())
else if (!cart.PossiblyDecrypted(index))
{
Console.WriteLine($"Partition {index}: Already Encrypted?...");
return false;
@@ -523,7 +553,7 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptPartition(Cart cart, int index, Stream input, Stream output)
private void EncryptPartition(N3DS cart, int index, Stream input, Stream output)
{
// Determine the keys needed for this partition
SetEncryptionKeys(cart, index);
@@ -542,19 +572,21 @@ namespace NDecrypt.Core
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
private void SetEncryptionKeys(Cart cart, int index)
private void SetEncryptionKeys(N3DS cart, int index)
{
// Get the partition
var partition = cart.Partitions?[index];
if (partition == null)
return;
// Get the backup header
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == null)
return;
// Get partition-specific values
byte[]? rsaSignature = partition.RSA2048Signature;
// Set the header to use based on mode
var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader;
BitMasks masks = backupHeader!.Flags!.BitMasks;
BitMasks masks = backupHeader.Flags.BitMasks;
CryptoMethod method = backupHeader.Flags.CryptoMethod;
// Get the partition keys
@@ -568,20 +600,20 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExtendedHeader(Cart cart, int index, Stream input, Stream output)
private bool EncryptExtendedHeader(N3DS cart, int index, Stream input, Stream output)
{
// Get required offsets
uint partitionOffset = GetPartitionOffset(cart, index);
if (partitionOffset == 0)
uint partitionOffset = cart.GetPartitionOffset(index);
if (partitionOffset == 0 || partitionOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
Console.WriteLine($"Partition {index} No Data... Skipping...");
return false;
}
uint extHeaderSize = GetExtendedHeaderSize(cart, index);
uint extHeaderSize = cart.GetExtendedHeaderSize(index);
if (extHeaderSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Extended Header... Skipping...");
Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
return false;
}
@@ -589,7 +621,7 @@ namespace NDecrypt.Core
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Encrypting: ExHeader");
Console.WriteLine($"Partition {index}: Encrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, cart.PlainIV(index));
@@ -612,46 +644,50 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExeFS(Cart cart, int index, Stream input, Stream output)
private bool EncryptExeFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the ExeFS
uint exeFsOffset = GetExeFSOffset(cart, index);
if (exeFsOffset == 0)
if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint exeFsSize = GetExeFSSize(cart, index);
if (exeFsSize == 0)
// Get the ExeFS header
var exefsHeader = cart.ExeFSHeaders[index];
if (exefsHeader == null)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return false;
}
// For all but the original crypto method, process each of the files in the table
var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader;
var backupHeader = cart.BackupHeader;
if (backupHeader!.Flags!.CryptoMethod != CryptoMethod.Original)
EncryptExeFSFileEntries(cart, index, input, output);
// Encrypt the filename table
EncryptExeFSFilenameTable(cart, index, input, output);
// Get the ExeFS files offset
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Seek to the ExeFS
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
input.Seek(exeFsFilesOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
int ctroffsetE = (int)(cart.MediaUnitSize() / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(cart.ExeFSIV(index), ctroffsetE);
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = Add(cart.ExeFSIV(index), ctroffsetE);
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the encryption
uint exeFsSize = cart.GetExeFSSize(index) - cart.MediaUnitSize;
PerformAESOperation(exeFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting: {s}"));
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
return true;
}
@@ -663,11 +699,11 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFilenameTable(Cart cart, int index, Stream input, Stream output)
private void EncryptExeFSFilenameTable(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsOffset = GetExeFSOffset(cart, index);
if (exeFsOffset == 0)
uint exeFsOffset = cart.GetExeFSOffset(index);
if (exeFsOffset == 0 || exeFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
@@ -677,13 +713,15 @@ namespace NDecrypt.Core
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Encrypting: ExeFS Filename Table");
Console.WriteLine($"Partition {index} ExeFS: Encrypting - ExeFS Filename Table");
// Create the ExeFS AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
PerformAESOperation(cart.MediaUnitSize(), cipher, input, output, null);
byte[] readBytes = input.ReadBytes((int)cart.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.Write(processedBytes);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
@@ -699,37 +737,42 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFileEntries(Cart cart, int index, Stream input, Stream output)
private void EncryptExeFSFileEntries(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsHeaderOffset = GetExeFSOffset(cart, index);
if (exeFsHeaderOffset == 0)
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Get to the start of the files
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize();
input.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
var exefsHeader = N3DSDeserializer.ParseExeFSHeader(input);
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// If the header failed to read, log and return
if (exefsHeader == null)
var exeFsHeader = cart.ExeFSHeaders?[index];
if (exeFsHeader?.FileHeaders == null)
{
Console.WriteLine($"Partition {index} ExeFS header could not be read. Skipping...");
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
}
foreach (var fileHeader in exefsHeader.FileHeaders!)
// Loop through and process all headers
for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
{
// Only decrypt a file if it's a code binary
if (fileHeader == null || !fileHeader.IsCodeBinary())
// Only attempt to process code binary files
if (!cart.IsCodeBinary(index, i))
continue;
// Get the file header
var fileHeader = exeFsHeader.FileHeaders[i];
if (fileHeader == null)
continue;
// Create the ExeFS AES ciphers for this partition
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize()) / 0x10;
byte[] exefsIVWithOffsetForHeader = AddToByteArray(cart.ExeFSIV(index), (int)ctroffset);
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize) / 0x10;
byte[] exefsIVWithOffsetForHeader = Add(cart.ExeFSIV(index), ctroffset);
var firstCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
@@ -738,13 +781,12 @@ namespace NDecrypt.Core
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
// Setup and perform the encryption
uint exeFsSize = GetExeFSSize(cart, index);
PerformAESOperation(exeFsSize,
PerformAESOperation(fileHeader.FileSize,
firstCipher,
secondCipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting: {fileHeader.FileName}...{s}"));
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
}
}
@@ -755,17 +797,17 @@ namespace NDecrypt.Core
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptRomFS(Cart cart, int index, Stream input, Stream output)
private bool EncryptRomFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the RomFS
uint romFsOffset = GetRomFSOffset(cart, index);
if (romFsOffset == 0)
uint romFsOffset = cart.GetRomFSOffset(index);
if (romFsOffset == 0 || romFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
uint romFsSize = GetRomFSSize(cart, index);
uint romFsSize = cart.GetRomFSSize(index);
if (romFsSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
@@ -779,7 +821,7 @@ namespace NDecrypt.Core
// Force setting encryption keys for partitions 1 and above
if (index > 0)
{
var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader;
var backupHeader = cart.BackupHeader;
KeysMap[index].SetRomFSValues(backupHeader!.Flags!.BitMasks);
}
@@ -791,7 +833,7 @@ namespace NDecrypt.Core
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Encrypting: {s}"));
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
return true;
}
@@ -802,13 +844,15 @@ namespace NDecrypt.Core
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="output">Stream representing the output</param>
private static void UpdateEncryptCryptoAndMasks(Cart cart, int index, Stream output)
private static void UpdateEncryptCryptoAndMasks(N3DS cart, int index, Stream output)
{
// Get required offsets
uint partitionOffset = GetPartitionOffset(cart, index);
uint partitionOffset = cart.GetPartitionOffset(index);
// Get the backup header
var backupHeader = cart.CardInfoHeader!.InitialData!.BackupHeader;
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == null)
return;
// Seek to the CryptoMethod location
output.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
@@ -816,7 +860,7 @@ namespace NDecrypt.Core
// Write the new CryptoMethod
// - For partitions 1 and up, set crypto-method to 0x00
// - If partition 0, restore crypto-method from backup flags
byte cryptoMethod = index > 0 ? (byte)CryptoMethod.Original : (byte)backupHeader!.Flags!.CryptoMethod;
byte cryptoMethod = index > 0 ? (byte)CryptoMethod.Original : (byte)backupHeader.Flags.CryptoMethod;
output.Write(cryptoMethod);
output.Flush();
@@ -824,13 +868,50 @@ namespace NDecrypt.Core
output.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
// Write the new BitMasks flag
BitMasks flag = cart.Partitions![index]!.Flags!.BitMasks;
BitMasks flag = cart.GetBitMasks(index);
flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF;
flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupHeader!.Flags!.BitMasks;
flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupHeader.Flags.BitMasks;
output.Write((byte)flag);
output.Flush();
}
#endregion
#region Info
/// <inheritdoc/>
public string? GetInformation(string filename)
{
try
{
// Open the file for reading
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DS.Create(input);
if (cart?.Model == null)
return "Error: Not a 3DS cart image!";
// Get a string builder for the status
var sb = new StringBuilder();
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
bool decrypted = cart.PossiblyDecrypted(p);
sb.AppendLine($"\tPartition {p}: {(decrypted ? "Decrypted" : "Encrypted")}");
}
// Return the status for all partitions
return sb.ToString();
}
catch (Exception ex)
{
Console.WriteLine(ex);
return null;
}
}
#endregion
}
}

View File

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

View File

@@ -2,21 +2,22 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>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</TargetFrameworks>
<OutputType>Exe</OutputType>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.3.0</VersionPrefix>
<VersionPrefix>0.3.2</VersionPrefix>
<!-- Package Properties -->
<Title>NDecrypt</Title>
<Authors>Matt Nadareski</Authors>
<Description>DS/3DS Encryption Tool</Description>
<Copyright>Copyright (c) Matt Nadareski 2018-2024</Copyright>
<Copyright>Copyright (c) Matt Nadareski 2018-2025</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<RepositoryUrl>https://github.com/SabreTools/NDecrypt</RepositoryUrl>
@@ -41,4 +42,8 @@
<ProjectReference Include="..\NDecrypt.Core\NDecrypt.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
#if NET20 || NET35 || NET40 || NET452
using System.Reflection;
#endif
using NDecrypt.Core;
namespace NDecrypt
@@ -20,14 +23,18 @@ namespace NDecrypt
return;
}
bool encrypt;
Feature feature;
if (args[0] == "decrypt" || args[0] == "d")
{
encrypt = false;
feature = Feature.Decrypt;
}
else if (args[0] == "encrypt" || args[0] == "e")
{
encrypt = true;
feature = Feature.Encrypt;
}
else if (args[0] == "info" || args[0] == "i")
{
feature = Feature.Info;
}
else
{
@@ -39,16 +46,16 @@ namespace NDecrypt
force = false,
outputHashes = false,
useAesKeysTxt = false;
string? config = null;
string? keyfile = null;
int start = 1;
for (; start < args.Length; start++)
{
if (args[start] == "-a" || args[start] == "--aes-keys"
|| args[start] == "-c" || args[start] == "--citra")
if (args[start] == "-a" || args[start] == "--aes-keys")
{
useAesKeysTxt = true;
}
else if (args[start] == "-dev" || args[start] == "--development")
else if (args[start] == "-d" || args[start] == "--development")
{
development = true;
}
@@ -67,7 +74,7 @@ namespace NDecrypt
start++;
string tempPath = args[start];
if (string.IsNullOrWhiteSpace(tempPath))
if (string.IsNullOrEmpty(tempPath))
Console.WriteLine($"Invalid keyfile path: null or empty path found!");
tempPath = Path.GetFullPath(tempPath);
@@ -76,12 +83,31 @@ namespace NDecrypt
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);
@@ -93,24 +119,27 @@ namespace NDecrypt
}
// Initialize the decrypt args, if possible
var decryptArgs = new DecryptArgs(keyfile, useAesKeysTxt);
DecryptArgs decryptArgs;
if (config != null)
decryptArgs = new DecryptArgs(config);
else
decryptArgs = new DecryptArgs(keyfile, useAesKeysTxt);
// Create reusable tools
_tools[FileType.NDS] = new DSTool();
_tools[FileType.NDS] = new DSTool(decryptArgs);
_tools[FileType.N3DS] = new ThreeDSTool(development, decryptArgs);
_tools[FileType.N3DSCIA] = new CIATool(development, decryptArgs);
for (int i = start; i < args.Length; i++)
{
if (File.Exists(args[i]))
{
ProcessPath(args[i], encrypt, force, outputHashes);
ProcessPath(args[i], feature, force, outputHashes);
}
else if (Directory.Exists(args[i]))
{
foreach (string file in Directory.EnumerateFiles(args[i], "*", SearchOption.AllDirectories))
foreach (string file in Directory.GetFiles(args[i], "*", SearchOption.AllDirectories))
{
ProcessPath(file, encrypt, force, outputHashes);
ProcessPath(file, feature, force, outputHashes);
}
}
else
@@ -124,10 +153,10 @@ namespace NDecrypt
/// Process a single file path
/// </summary>
/// <param name="path">File path to process</param>
/// <param name="encrypt">Indicates if the file should be encrypted or decrypted</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, bool encrypt, bool force, bool outputHashes)
private static void ProcessPath(string path, Feature feature, bool force, bool outputHashes)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(path);
@@ -137,16 +166,23 @@ namespace NDecrypt
Console.WriteLine($"Processing {path}");
// Encrypt or decrypt the file as requested
if (encrypt && !tool.EncryptFile(path, force))
if (feature == Feature.Encrypt && !tool.EncryptFile(path, force))
{
Console.WriteLine("Encryption failed!");
return;
}
else if (!encrypt && !tool.DecryptFile(path, force))
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)
@@ -159,7 +195,7 @@ namespace NDecrypt
/// <param name="err">Additional error text to display, can be null to ignore</param>
private static void DisplayHelp(string? err = null)
{
if (!string.IsNullOrWhiteSpace(err))
if (!string.IsNullOrEmpty(err))
Console.WriteLine($"Error: {err}");
Console.WriteLine(@"Usage: NDecrypt <operation> [flags] <path> ...
@@ -167,18 +203,37 @@ namespace NDecrypt
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
-dev, --development Enable using development keys, if available
-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
-k, --keyfile <path> Path to keys.bin or aes_keys.txt
<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>
@@ -192,18 +247,8 @@ More than one path can be specified at a time.");
return keyfile;
}
// Derive the keyfile path based on the runtime folder if not already set
using var processModule = System.Diagnostics.Process.GetCurrentProcess().MainModule;
string applicationDirectory = Path.GetDirectoryName(processModule?.FileName) ?? string.Empty;
// Use the proper default name for the type
if (useAesKeysTxt)
keyfile = Path.Combine(applicationDirectory, "aes_keys.txt");
else
keyfile = Path.Combine(applicationDirectory, "keys.bin");
// Only return the path if the file exists
return File.Exists(keyfile) ? keyfile : null;
// Derive the keyfile path, if possible
return GetFileLocation(useAesKeysTxt ? "aes_keys.txt" : "keys.bin");
}
/// <summary>
@@ -225,7 +270,6 @@ More than one path can be specified at a time.");
FileType.NDSi => _tools[FileType.NDS],
FileType.iQueDS => _tools[FileType.NDS],
FileType.N3DS => _tools[FileType.N3DS],
FileType.N3DSCIA => _tools[FileType.N3DSCIA],
_ => null,
};
}
@@ -257,21 +301,60 @@ More than one path can be specified at a time.");
}
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(".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;
}
else if (filename.EndsWith(".cia", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as Nintendo 3DS CIA [CAUTION: NOT WORKING CURRENTLY]");
return FileType.N3DSCIA;
}
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds");
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>

View File

@@ -1,7 +1,6 @@
# NDecrypt
[![Build status](https://ci.appveyor.com/api/projects/status/cc1n298syn6r50mq?svg=true)](https://ci.appveyor.com/project/mnadareski/ndecrypt)
[![Program Build](https://github.com/SabreTools/NDecrypt/actions/workflows/build_program.yml/badge.svg)](https://github.com/SabreTools/NDecrypt/actions/workflows/build_program.yml)
[![Build and Test](https://github.com/SabreTools/NDecrypt/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/SabreTools/NDecrypt/actions/workflows/build_and_test.yml)
A simple tool for simple people.
@@ -15,7 +14,7 @@ This is a code port of 3 different programs:
## No really, what is this?
This tool allows you to encrypt and decrypt your personally dumped NDS and N3DS roms with minimal hassle. The only caveat right now is that you need a `keys.bin` or `aes_keys.txt` file for your personally obtained encryption keys.
This tool allows you to encrypt and decrypt your personally dumped Nintendo DS, 3DS, and New 3DS cart images with minimal hassle.
## Where do I find it?
@@ -25,45 +24,73 @@ For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/N
## So how do I use this?
NDecrypt.exe <operation> [flags] <path> ...
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, --citra - Enable using aes_keys.txt instead of keys.bin
-dev, --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
-k, --keyfile <path> - Path to keys.bin or aes_keys.txt
-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.
**Note:** This overwrites the input files, so make backups if you're working on your original, personal dumps.
### Additional Notes
**Note:** 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.
- 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...
## I feel like something is missing
You (possibly*) are! In fact, you may be asking, "Hey, what was that `keys.bin` you mentioned??". I'm glad you asked. It's used only for Nintendo 3DS and New 3DS files. Since some people don't like reading code, you need the 9 16-bit keys in little endian format (most common extraction methods produce big endian, so keep that in mind). It's recommended that you fill with 0x00 if you don't have access to a particular value so it doesn't mess up the read. They need to be in the following order:
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.
| Name | `aes_keys.txt` Entry |
| --- | --- |
| Hardware constant | `generator` |
| KeyX0x18 | `slot0x18KeyX` |
| KeyX0x1B | `slot0x1BKeyX` |
| KeyX0x25 | `slot0x25KeyX` |
| KeyX0x2C | `slot0x2CKeyX` |
| DevKeyX0x18 | **UNMAPPED** |
| DevKeyX0x1B | **UNMAPPED** |
| DevKeyX0x25 | **UNMAPPED** |
| DevKeyX0x2C | **UNMAPPED** |
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.
The last 4 are only required if you use the `-dev` flag. Once again, don't ask for these, please. If you're missing a required key, then things won't work. Don't blame me, blame society. Or something. And yes, I'll fix this being required across the board at some point.
| `config.json` | `keys.bin` order | `aes_keys.txt` | rom-properties `keys.conf` |
| --- | --- | --- | --- |
| `NitroEncryptionData` | **N/A** | **UNMAPPED** | **UNMAPPED** |
| `AESHardwareConstant` | 1 | `generator` | `ctr-scrambler` |
| `KeyX0x18` | 2 | `slot0x18KeyX` | `ctr-Slot0x18KeyX` |
| `KeyX0x1B` | 3 | `slot0x1BKeyX` | `ctr-Slot0x1BKeyX` |
| `KeyX0x25` | 4 | `slot0x25KeyX` | `ctr-Slot0x25KeyX` |
| `KeyX0x2C` | 5 | `slot0x2CKeyX` | `ctr-Slot0x2CKeyX` |
| `DevKeyX0x18` | 6 | **UNMAPPED** | `ctr-dev-Slot0x18KeyX` |
| `DevKeyX0x1B` | 7 | **UNMAPPED** | `ctr-dev-Slot0x1BKeyX` |
| `DevKeyX0x25` | 8 | **UNMAPPED** | `ctr-dev-Slot0x25KeyX` |
| `DevKeyX0x2C` | 9 | **UNMAPPED** | `ctr-dev-Slot0x2CKeyX` |
*If you choose to use the `-c` option, you can instead provide your personally filled out `aes_keys.txt` file in the same folder as NDecrypt and that can be used instead. Please note that if you choose to use this file, you will not be able to use the `-dev` flag. If you forget and happen to use them together, NDecrypt will disable that flag for you. You're welcome.
**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`.
**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.
### `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`
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`
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.
## But does it work?

View File

@@ -1,26 +0,0 @@
# version format
version: 0.2.5-{build}
# pull request template
pull_requests:
do_not_increment_build_number: true
# vm template
image: Visual Studio 2022
# install dependencies
install:
- cd %APPVEYOR_BUILD_FOLDER%
- git submodule update --init --recursive
# build step
build_script:
- dotnet build
# success/failure tracking
on_success:
- ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1
- ps: ./send.ps1 success $env:WEBHOOK_URL
on_failure:
- ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1
- ps: ./send.ps1 failure $env:WEBHOOK_URL

12
config-default.json Normal file
View File

@@ -0,0 +1,12 @@
{
"NitroEncryptionData": null,
"AESHardwareConstant": null,
"KeyX0x18": null,
"KeyX0x1B": null,
"KeyX0x25": null,
"KeyX0x2C": null,
"DevKeyX0x18": null,
"DevKeyX0x1B": null,
"DevKeyX0x25": null,
"DevKeyX0x2C": null
}

24
publish-nix.sh Normal file → Executable file
View File

@@ -10,13 +10,17 @@
# Optional parameters
USE_ALL=false
INCLUDE_DEBUG=false
NO_BUILD=false
NO_ARCHIVE=false
while getopts "uba" OPTION; do
while getopts "udba" OPTION; do
case $OPTION in
u)
USE_ALL=true
;;
d)
INCLUDE_DEBUG=true
;;
b)
NO_BUILD=true
;;
@@ -39,6 +43,7 @@ COMMIT=$(git log --pretty=%H -1)
# Output the selected options
echo "Selected Options:"
echo " Use all frameworks (-u) $USE_ALL"
echo " Include debug builds (-d) $INCLUDE_DEBUG"
echo " No build (-b) $NO_BUILD"
echo " No archive (-a) $NO_ARCHIVE"
echo " "
@@ -49,7 +54,7 @@ RUNTIMES=("win-x86" "win-x64" "win-arm64" "linux-x64" "linux-arm64" "osx-x64" "o
# Use expanded lists, if requested
if [ $USE_ALL = true ]; then
FRAMEWORKS=("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")
fi
# Create the filter arrays
@@ -64,6 +69,9 @@ if [ $NO_BUILD = false ]; then
echo "Restoring Nuget packages"
dotnet restore
# Create Nuget Packages
dotnet pack NDecrypt.Core/NDecrypt.Core.csproj --output $BUILD_FOLDER
# Build Program
for FRAMEWORK in "${FRAMEWORKS[@]}"; do
for RUNTIME in "${RUNTIMES[@]}"; do
@@ -88,14 +96,14 @@ if [ $NO_BUILD = false ]; then
# Only .NET 5 and above can publish to a single file
if [[ $(echo ${SINGLE_FILE_CAPABLE[@]} | fgrep -w $FRAMEWORK) ]]; then
# Only include Debug if building all
if [ $USE_ALL = true ]; then
# Only include Debug if set
if [ $INCLUDE_DEBUG = true ]; then
dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true
fi
dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false
else
# Only include Debug if building all
if [ $USE_ALL = true ]; then
# Only include Debug if set
if [ $INCLUDE_DEBUG = true ]; then
dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT
fi
dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:DebugType=None -p:DebugSymbols=false
@@ -128,8 +136,8 @@ if [ $NO_ARCHIVE = false ]; then
fi
fi
# Only include Debug if building all
if [ $USE_ALL = true ]; then
# Only include Debug if set
if [ $INCLUDE_DEBUG = true ]; then
cd $BUILD_FOLDER/NDecrypt/bin/Debug/${FRAMEWORK}/${RUNTIME}/publish/
zip -r $BUILD_FOLDER/NDecrypt_${FRAMEWORK}_${RUNTIME}_debug.zip .
fi

View File

@@ -12,6 +12,10 @@ param(
[Alias("UseAll")]
[switch]$USE_ALL,
[Parameter(Mandatory = $false)]
[Alias("IncludeDebug")]
[switch]$INCLUDE_DEBUG,
[Parameter(Mandatory = $false)]
[Alias("NoBuild")]
[switch]$NO_BUILD,
@@ -30,6 +34,7 @@ $COMMIT = git log --pretty=format:"%H" -1
# Output the selected options
Write-Host "Selected Options:"
Write-Host " Use all frameworks (-UseAll) $USE_ALL"
Write-Host " Include debug builds (-IncludeDebug) $INCLUDE_DEBUG"
Write-Host " No build (-NoBuild) $NO_BUILD"
Write-Host " No archive (-NoArchive) $NO_ARCHIVE"
Write-Host " "
@@ -40,7 +45,7 @@ $RUNTIMES = @('win-x86', 'win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'os
# Use expanded lists, if requested
if ($USE_ALL.IsPresent) {
$FRAMEWORKS = @('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')
}
# Create the filter arrays
@@ -55,6 +60,9 @@ if (!$NO_BUILD.IsPresent) {
Write-Host "Restoring Nuget packages"
dotnet restore
# Create Nuget Package
dotnet pack NDecrypt.Core\NDecrypt.Core.csproj --output $BUILD_FOLDER
# Build Program
foreach ($FRAMEWORK in $FRAMEWORKS) {
foreach ($RUNTIME in $RUNTIMES) {
@@ -75,15 +83,15 @@ if (!$NO_BUILD.IsPresent) {
# Only .NET 5 and above can publish to a single file
if ($SINGLE_FILE_CAPABLE -contains $FRAMEWORK) {
# Only include Debug if building all
if ($USE_ALL.IsPresent) {
# Only include Debug if set
if ($INCLUDE_DEBUG.IsPresent) {
dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true
}
dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false
}
else {
# Only include Debug if building all
if ($USE_ALL.IsPresent) {
# Only include Debug if set
if ($INCLUDE_DEBUG.IsPresent) {
dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT
}
dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:DebugType=None -p:DebugSymbols=false
@@ -112,8 +120,8 @@ if (!$NO_ARCHIVE.IsPresent) {
continue
}
# Only include Debug if building all
if ($USE_ALL.IsPresent) {
# Only include Debug if set
if ($INCLUDE_DEBUG.IsPresent) {
Set-Location -Path $BUILD_FOLDER\NDecrypt\bin\Debug\${FRAMEWORK}\${RUNTIME}\publish\
7z a -tzip $BUILD_FOLDER\NDecrypt_${FRAMEWORK}_${RUNTIME}_debug.zip *
}