59 Commits
0.2.1 ... 0.2.5

Author SHA1 Message Date
Matt Nadareski
1159b677bc Bump version to 0.2.5 2023-01-06 10:14:31 -08:00
Matt Nadareski
6527d85f29 Commit hash only 2023-01-06 10:01:23 -08:00
Matt Nadareski
ff815299fb Try using a different variable 2023-01-06 09:56:43 -08:00
Matt Nadareski
c04de5700a Use different formatting for version string 2023-01-06 09:52:38 -08:00
Matt Nadareski
ab9d32f56a Attempt to use commit hash in naming 2023-01-06 09:51:40 -08:00
Matt Nadareski
8151f306ec Experiment adding version numbers 2023-01-06 09:46:37 -08:00
Matt Nadareski
fd9b94d428 Fix artifact naming 2023-01-06 09:41:43 -08:00
Matt Nadareski
9cc2b6befe Replace assembly location 2023-01-06 09:40:52 -08:00
Matt Nadareski
8c2d4d701a Move AppVeyor script, oops 2023-01-06 09:36:31 -08:00
Matt Nadareski
593e8b2b4d Add build status to README 2023-01-06 09:33:45 -08:00
Matt Nadareski
71f3295a2b Add AppVeyor config 2023-01-06 09:32:55 -08:00
Matt Nadareski
a29071e697 Modernize NDecrypt 2023-01-06 09:31:42 -08:00
Matt Nadareski
96c3c3732c Set license properly to MIT
MIT most closely matches the statement that was included previously in the README about an extremely permissive license.
2023-01-06 09:27:41 -08:00
Matt Nadareski
c8f8a7d99c Bump version to 0.2.4 2022-10-31 10:30:52 -07:00
Matt Nadareski
3ee8c2e79b Update csproj files with full info 2022-10-31 10:27:56 -07:00
Matt Nadareski
7d263886da Force reader buffer refresh to omit stale data 2022-10-27 16:41:22 -07:00
Matt Nadareski
6d1b23ffec Be less clever in code to make debugging easier 2022-10-27 15:28:34 -07:00
Matt Nadareski
92d1c3c5e5 Split NDecrypt.Library; add build targets 2022-10-27 11:27:13 -07:00
Matt Nadareski
855a353e90 Reverse library and exe naming 2022-10-27 11:06:06 -07:00
Matt Nadareski
6dac45f973 Update BouncyCastle to 1.9.0 2022-09-27 13:35:07 -07:00
Matt Nadareski
a6337e0864 Drop .NET 5 support 2022-09-27 12:27:16 -07:00
Matt Nadareski
778d5d4ffc Fix hashing because of .NET 6 2022-09-27 12:19:53 -07:00
Matt Nadareski
16a84f0dc6 .NET 6.0, .NET Standard 2.0, Cleanup 2022-04-17 13:10:55 -07:00
Matt Nadareski
7fbe044ab8 Read ContentIndex data, just in case 2021-09-28 22:23:04 -07:00
Matt Nadareski
7c9a035d7c Cleanup and add note 2021-09-28 22:19:23 -07:00
Matt Nadareski
964e6f243c Fill in all methods... with a lot of TODOs 2021-09-28 22:14:40 -07:00
Matt Nadareski
b454e42f8e Start filling in more methods for CIA 2021-09-28 21:54:31 -07:00
Matt Nadareski
9f70eae8be Fix reading NCCH data in CIA header 2021-09-28 21:48:58 -07:00
Matt Nadareski
20987573f4 Fix build 2021-09-28 21:37:12 -07:00
Matt Nadareski
2fb05b577e Fix reading CIA header info 2021-09-28 21:32:30 -07:00
Matt Nadareski
f72b8c3506 Rename NDS -> Nitro 2021-09-27 10:56:49 -07:00
Matt Nadareski
8b32b21475 Better reading of Ticket limits 2021-09-27 10:51:44 -07:00
Matt Nadareski
2779f5ef72 Better certificate chain reading 2021-09-27 10:42:54 -07:00
Matt Nadareski
bbbf71a603 Fill in more CIA-specific objects 2021-09-27 10:26:22 -07:00
Matt Nadareski
bea979dca5 Add CIA notes 2021-09-26 23:31:39 -07:00
Matt Nadareski
5a0169310b Better wording 2021-09-26 23:20:06 -07:00
Matt Nadareski
67c83bd126 Make note a little more apparent 2021-09-26 22:52:12 -07:00
Matt Nadareski
6fdcc2effe Add Citra names to README 2021-09-26 22:38:14 -07:00
Matt Nadareski
9ff0205e15 Add keyfile path input 2021-09-26 22:35:10 -07:00
Matt Nadareski
c91d7f2708 Separate out CMD portion
This also introduces a new helper class for holding all of the decryption args. This makes it so that the method signatures don't have to change so much.
2021-09-26 22:22:17 -07:00
Matt Nadareski
d9b333e3ba Start adding Citra keyfile support 2021-09-26 21:36:55 -07:00
Matt Nadareski
ef5a01edf7 Add CIA header (nw) 2021-07-22 22:36:05 -07:00
Matt Nadareski
012787a5b1 Hook up hashing, fix string output 2021-02-08 22:13:30 -08:00
Matt Nadareski
df5664fa1f Add hashing code (not hooked up) 2021-02-08 21:54:23 -08:00
Matt Nadareski
bef252f57f Merge pull request #4 from Icyelut/fix-README
Fix typos in README that suggests the wrong keyslots
2021-01-26 21:04:01 -08:00
Icyelut
b78db22464 Fix typo in README that suggests the wrong dev keyslot
Signed-off-by: Icyelut <PanetGhoul@protonmail.com>
2021-01-11 20:46:29 +09:00
Icyelut
6d980c94fd Fix typo in README that suggests a keyslot that doesn't exist
Signed-off-by: Icyelut <PanetGhoul@protonmail.com>
2021-01-11 19:27:38 +09:00
Matt Nadareski
8452cb4fc0 Update compatibilty numers (100%!!!) 2020-12-19 21:43:56 -08:00
Matt Nadareski
0deaa0aed3 Tools to proper namespaces 2020-12-17 22:06:30 -08:00
Matt Nadareski
9b5f90a750 A bit of code cleanup, better communication 2020-12-17 22:02:04 -08:00
Matt Nadareski
7415a21d5c Try/catch blocks around file processing 2020-12-17 11:30:34 -08:00
Matt Nadareski
f349389994 keys.bin is only needed for 3DS, better docs 2020-12-17 11:08:53 -08:00
Matt Nadareski
a046bd4152 Remove forced pause 2020-12-17 10:39:30 -08:00
Matt Nadareski
51a8f0c9df Report new compatibility information 2020-12-17 10:37:34 -08:00
Matt Nadareski
0449c16a01 Yet another empty one 2020-12-16 19:39:33 -08:00
Matt Nadareski
7638d7dbf8 One more decrypted empty secure area 2020-12-16 16:27:39 -08:00
Matt Nadareski
387bf46e5a Fix incorrect matching criteria 2020-12-16 16:07:45 -08:00
Matt Nadareski
6d257dc1e3 Add iQue extension for NDS 2020-12-16 13:15:49 -08:00
Matt Nadareski
91d816e359 Handle PoP cases, add compatibility to README 2020-12-16 11:57:55 -08:00
51 changed files with 3060 additions and 355 deletions

2
.vscode/launch.json vendored
View File

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

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2018-2023 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:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,293 @@
using System;
using System.IO;
using System.Linq;
using System.Numerics;
using NDecrypt.Core.Tools;
namespace NDecrypt.Core
{
public class DecryptArgs
{
#region Common Fields
/// <summary>
/// Flag to indicate operation
/// </summary>
public bool Encrypt { get; set; } = true;
/// <summary>
/// Flag to indicate forcing the operation
/// </summary>
public bool Force { get; set; } = false;
/// <summary>
/// Represents if all of the keys have been initialized properly
/// </summary>
public bool? IsReady { get; private set; }
#endregion
#region 3DS-Specific Fields
/// <summary>
/// Flag to indicate key types to use [3DS only]
/// </summary>
public bool Development { get; set; } = false;
/// <summary>
/// Path to the keyfile [3DS only]
/// </summary>
public string KeyFile { get; set; }
/// <summary>
/// Flag to indicate keyfile format to use [3DS only]
/// </summary>
public bool UseCitraKeyFile { get; set; } = false;
/// <summary>
/// AES Hardware Constant
/// </summary>
public BigInteger AESHardwareConstant { get; private set; }
#region Retail Keys
/// <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
/// <summary>
/// Dev KeyX 0x18 (New 3DS 9.3)
/// </summary>
public BigInteger DevKeyX0x18 { get; private set; }
/// <summary>
/// Dev KeyX 0x1B New 3DS 9.6)
/// </summary>
public BigInteger DevKeyX0x1B { get; private set; }
/// <summary>
/// Dev KeyX 0x25 (> 7.x)
/// </summary>
public BigInteger DevKeyX0x25 { get; private set; }
/// <summary>
/// Dev KeyX 0x2C (< 6.x)
/// </summary>
public BigInteger DevKeyX0x2C { get; private set; }
#endregion
#endregion
/// <summary>
/// Setup all of the necessary constants
/// </summary>
public void Initialize()
{
// If we're already attempted to set the constants, don't try to again
if (IsReady != null)
return;
// Read the proper keyfile format
if (UseCitraKeyFile)
InitAesKeysTxt(KeyFile);
else
InitKeysBin(KeyFile);
}
/// <summary>
/// Setup all of the necessary constants from aes_keys.txt
/// </summary>
/// <param name="keyfile">Path to aes_keys.txt</param>
private void InitAesKeysTxt(string keyfile)
{
if (!File.Exists(keyfile))
{
IsReady = false;
return;
}
try
{
using (IniReader reader = new IniReader(keyfile))
{
// This is required to preserve sign for BigInteger
byte[] signByte = new byte[] { 0x00 };
while (reader.ReadNextLine())
{
if (reader.KeyValuePair == null || string.IsNullOrWhiteSpace(reader.KeyValuePair?.Key))
break;
var kvp = reader.KeyValuePair.Value;
byte[] value = StringToByteArray(kvp.Value).Reverse().ToArray();
byte[] valueWithSign = value.Concat(signByte).ToArray();
switch (kvp.Key)
{
// Hardware constant
case "generator":
AESHardwareConstant = new BigInteger(value);
break;
// Retail Keys
case "slot0x18KeyX":
KeyX0x18 = new BigInteger(valueWithSign);
break;
case "slot0x1BKeyX":
KeyX0x1B = new BigInteger(valueWithSign);
break;
case "slot0x25KeyX":
KeyX0x25 = new BigInteger(valueWithSign);
break;
case "slot0x2CKeyX":
KeyX0x2C = new BigInteger(valueWithSign);
break;
// Currently Unused KeyX
case "slot0x03KeyX":
case "slot0x19KeyX":
case "slot0x1AKeyX":
case "slot0x1CKeyX":
case "slot0x1DKeyX":
case "slot0x1EKeyX":
case "slot0x1FKeyX":
case "slot0x2DKeyX":
case "slot0x2EKeyX":
case "slot0x2FKeyX":
case "slot0x30KeyX":
case "slot0x31KeyX":
case "slot0x32KeyX":
case "slot0x33KeyX":
case "slot0x34KeyX":
case "slot0x35KeyX":
case "slot0x36KeyX":
case "slot0x37KeyX":
case "slot0x38KeyX":
case "slot0x3AKeyX":
case "slot0x3BKeyX":
break;
// Currently Unused KeyY
case "slot0x03KeyY":
case "slot0x06KeyY":
case "slot0x07KeyY":
case "slot0x2EKeyY":
case "slot0x2FKeyY":
case "slot0x31KeyY":
break;
// Currently Unused KeyN
case "slot0x0DKeyN":
case "slot0x15KeyN":
case "slot0x16KeyN":
case "slot0x19KeyN":
case "slot0x1AKeyN":
case "slot0x1BKeyN":
case "slot0x1CKeyN":
case "slot0x1DKeyN":
case "slot0x1EKeyN":
case "slot0x1FKeyN":
case "slot0x24KeyN":
case "slot0x2DKeyN":
case "slot0x2EKeyN":
case "slot0x2FKeyN":
case "slot0x31KeyN":
case "slot0x32KeyN":
case "slot0x36KeyN":
case "slot0x37KeyN":
case "slot0x38KeyN":
case "slot0x3BKeyN":
break;
}
}
}
}
catch
{
IsReady = false;
return;
}
IsReady = true;
}
/// <summary>
/// Setup all of the necessary constants from keys.bin
/// </summary>
/// <param name="keyfile">Path to keys.bin</param>
/// <remarks>keys.bin should be in little endian format</remarks>
private void InitKeysBin(string keyfile)
{
if (!File.Exists(keyfile))
{
IsReady = false;
return;
}
try
{
using (BinaryReader reader = new BinaryReader(File.Open(keyfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
// This is required to preserve sign for BigInteger
byte[] signByte = new byte[] { 0x00 };
// Hardware constant
AESHardwareConstant = new BigInteger(reader.ReadBytes(16));
// Retail keys
KeyX0x18 = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
KeyX0x1B = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
KeyX0x25 = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
KeyX0x2C = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
// Development keys
DevKeyX0x18 = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
DevKeyX0x1B = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
DevKeyX0x25 = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
DevKeyX0x2C = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
}
}
catch
{
IsReady = false;
return;
}
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)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
}
}

View File

@@ -5,7 +5,7 @@ using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
namespace NDecrypt
namespace NDecrypt.Core
{
public static class Helper
{

7
NDecrypt.Core/ITool.cs Normal file
View File

@@ -0,0 +1,7 @@
namespace NDecrypt.Core
{
public interface ITool
{
bool ProcessFile();
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net6.0</TargetFrameworks>
<Authors>Matt Nadareski</Authors>
<Copyright>Copyright (c)2018-2023 Matt Nadareski</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/SabreTools/NDecrypt</RepositoryUrl>
<Version>0.2.5</Version>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<IncludeSource>true</IncludeSource>
<IncludeSymbols>true</IncludeSymbols>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.NetCore" Version="1.9.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,36 @@
using System;
namespace NDecrypt.Core.Tools
{
/// <summary>
/// Available hashing types
/// </summary>
[Flags]
public enum Hash
{
CRC = 1 << 0,
MD5 = 1 << 1,
SHA1 = 1 << 2,
SHA256 = 1 << 3,
SHA384 = 1 << 4,
SHA512 = 1 << 5,
// Special combinations
Standard = CRC | MD5 | SHA1,
DeepHashes = SHA256 | SHA384 | SHA512,
SecureHashes = MD5 | SHA1 | SHA256 | SHA384 | SHA512,
All = CRC | MD5 | SHA1 | SHA256 | SHA384 | SHA512,
}
/// <summary>
/// Different types of INI rows being parsed
/// </summary>
public enum IniRowType
{
None,
SectionHeader,
KeyValue,
Comment,
Invalid,
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Linq;
using System.Security.Cryptography;
namespace NDecrypt.Core.Tools
{
/// <summary>
/// Async hashing class wraper
/// </summary>
public class Hasher
{
public Hash HashType { get; private set; }
private IDisposable _hasher;
public Hasher(Hash hashType)
{
this.HashType = hashType;
GetHasher();
}
/// <summary>
/// Generate the correct hashing class based on the hash type
/// </summary>
private void GetHasher()
{
switch (HashType)
{
case Hash.CRC:
_hasher = new OptimizedCRC.OptimizedCRC();
break;
case Hash.MD5:
_hasher = MD5.Create();
break;
case Hash.SHA1:
_hasher = SHA1.Create();
break;
case Hash.SHA256:
_hasher = SHA256.Create();
break;
case Hash.SHA384:
_hasher = SHA384.Create();
break;
case Hash.SHA512:
_hasher = SHA512.Create();
break;
}
}
public void Dispose()
{
_hasher.Dispose();
}
/// <summary>
/// Process a buffer of some length with the internal hash algorithm
/// </summary>
public void Process(byte[] buffer, int size)
{
switch (HashType)
{
case Hash.CRC:
(_hasher as OptimizedCRC.OptimizedCRC).Update(buffer, 0, size);
break;
case Hash.MD5:
case Hash.SHA1:
case Hash.SHA256:
case Hash.SHA384:
case Hash.SHA512:
(_hasher as HashAlgorithm).TransformBlock(buffer, 0, size, null, 0);
break;
}
}
/// <summary>
/// Terminate the internal hash algorigthm
/// </summary>
public void Terminate()
{
byte[] emptyBuffer = new byte[0];
switch (HashType)
{
case Hash.CRC:
(_hasher as OptimizedCRC.OptimizedCRC).Update(emptyBuffer, 0, 0);
break;
case Hash.MD5:
case Hash.SHA1:
case Hash.SHA256:
case Hash.SHA384:
case Hash.SHA512:
(_hasher as HashAlgorithm).TransformFinalBlock(emptyBuffer, 0, 0);
break;
}
}
/// <summary>
/// Get internal hash as a byte array
/// </summary>
public byte[] GetHash()
{
switch (HashType)
{
case Hash.CRC:
return BitConverter.GetBytes((_hasher as OptimizedCRC.OptimizedCRC).Value).Reverse().ToArray();
case Hash.MD5:
case Hash.SHA1:
case Hash.SHA256:
case Hash.SHA384:
case Hash.SHA512:
return (_hasher as HashAlgorithm).Hash;
}
return null;
}
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace NDecrypt.Core.Tools
{
public class IniReader : IDisposable
{
/// <summary>
/// Internal stream reader for inputting
/// </summary>
private readonly StreamReader sr;
/// <summary>
/// Get if at end of stream
/// </summary>
public bool EndOfStream
{
get
{
return sr?.EndOfStream ?? true;
}
}
/// <summary>
/// Contents of the currently read line as a key value pair
/// </summary>
public KeyValuePair<string, string>? KeyValuePair { get; private set; } = null;
/// <summary>
/// Contents of the current line, unprocessed
/// </summary>
public string CurrentLine { get; private set; } = string.Empty;
/// <summary>
/// Get the current line number
/// </summary>
public long LineNumber { get; private set; } = 0;
/// <summary>
/// Current row type
/// </summary>
public IniRowType RowType { get; private set; } = IniRowType.None;
/// <summary>
/// Current section being read
/// </summary>
public string Section { get; private set; } = string.Empty;
/// <summary>
/// Validate that rows are in key=value format
/// </summary>
public bool ValidateRows { get; set; } = true;
/// <summary>
/// Constructor for reading from a file
/// </summary>
public IniReader(string filename)
{
sr = new StreamReader(filename);
}
/// <summary>
/// Constructor for reading from a stream
/// </summary>
public IniReader(Stream stream, Encoding encoding)
{
sr = new StreamReader(stream, encoding);
}
/// <summary>
/// Read the next line in the INI file
/// </summary>
public bool ReadNextLine()
{
if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream)
return false;
CurrentLine = sr.ReadLine().Trim();
LineNumber++;
ProcessLine();
return true;
}
/// <summary>
/// Process the current line and extract out values
/// </summary>
private void ProcessLine()
{
// Comment
if (CurrentLine.StartsWith(";"))
{
KeyValuePair = null;
RowType = IniRowType.Comment;
}
// Section
else if (CurrentLine.StartsWith("[") && CurrentLine.EndsWith("]"))
{
KeyValuePair = null;
RowType = IniRowType.SectionHeader;
Section = CurrentLine.TrimStart('[').TrimEnd(']');
}
// KeyValuePair
else if (CurrentLine.Contains("="))
{
// Split the line by '=' for key-value pairs
string[] data = CurrentLine.Split('=');
// If the value field contains an '=', we need to put them back in
string key = data[0].Trim();
string value = string.Join("=", data.Skip(1)).Trim();
KeyValuePair = new KeyValuePair<string, string>(key, value);
RowType = IniRowType.KeyValue;
}
// Empty
else if (string.IsNullOrEmpty(CurrentLine))
{
KeyValuePair = null;
CurrentLine = string.Empty;
RowType = IniRowType.None;
}
// Invalid
else
{
KeyValuePair = null;
RowType = IniRowType.Invalid;
if (ValidateRows)
throw new InvalidDataException($"Invalid INI row found, cannot continue: {CurrentLine}");
}
}
/// <summary>
/// Dispose of the underlying reader
/// </summary>
public void Dispose()
{
sr.Dispose();
}
}
}

View File

@@ -0,0 +1,153 @@
/*
Copyright (c) 2012-2015 Eugene Larchenko (spct@mail.ru)
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:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
using System;
namespace OptimizedCRC
{
internal class OptimizedCRC : IDisposable
{
private const uint kCrcPoly = 0xEDB88320;
private const uint kInitial = 0xFFFFFFFF;
private const int CRC_NUM_TABLES = 8;
private static readonly uint[] Table;
static OptimizedCRC()
{
unchecked
{
Table = new uint[256 * CRC_NUM_TABLES];
int i;
for (i = 0; i < 256; i++)
{
uint r = (uint)i;
for (int j = 0; j < 8; j++)
{
r = (r >> 1) ^ (kCrcPoly & ~((r & 1) - 1));
}
Table[i] = r;
}
for (; i < 256 * CRC_NUM_TABLES; i++)
{
uint r = Table[i - 256];
Table[i] = Table[r & 0xFF] ^ (r >> 8);
}
}
}
public uint UnsignedValue;
public OptimizedCRC()
{
Init();
}
/// <summary>
/// Reset CRC
/// </summary>
public void Init()
{
UnsignedValue = kInitial;
}
public int Value
{
get { return (int)~UnsignedValue; }
}
public void Update(byte[] data, int offset, int count)
{
new ArraySegment<byte>(data, offset, count); // check arguments
if (count == 0)
{
return;
}
var table = OptimizedCRC.Table;
uint crc = UnsignedValue;
for (; (offset & 7) != 0 && count != 0; count--)
{
crc = (crc >> 8) ^ table[(byte)crc ^ data[offset++]];
}
if (count >= 8)
{
/*
* Idea from 7-zip project sources (http://7-zip.org/sdk.html)
*/
int end = (count - 8) & ~7;
count -= end;
end += offset;
while (offset != end)
{
crc ^= (uint)(data[offset] + (data[offset + 1] << 8) + (data[offset + 2] << 16) + (data[offset + 3] << 24));
uint high = (uint)(data[offset + 4] + (data[offset + 5] << 8) + (data[offset + 6] << 16) + (data[offset + 7] << 24));
offset += 8;
crc = table[(byte)crc + 0x700]
^ table[(byte)(crc >>= 8) + 0x600]
^ table[(byte)(crc >>= 8) + 0x500]
^ table[/*(byte)*/(crc >> 8) + 0x400]
^ table[(byte)(high) + 0x300]
^ table[(byte)(high >>= 8) + 0x200]
^ table[(byte)(high >>= 8) + 0x100]
^ table[/*(byte)*/(high >> 8) + 0x000];
}
}
while (count-- != 0)
{
crc = (crc >> 8) ^ table[(byte)crc ^ data[offset++]];
}
UnsignedValue = crc;
}
static public int Compute(byte[] data, int offset, int count)
{
var crc = new OptimizedCRC();
crc.Update(data, offset, count);
return crc.Value;
}
static public int Compute(byte[] data)
{
return Compute(data, 0, data.Length);
}
static public int Compute(ArraySegment<byte> block)
{
return Compute(block.Array, block.Offset, block.Count);
}
public void Dispose()
{
UnsignedValue = 0;
}
}
}

576
NDecrypt.N3DS/CIATool.cs Normal file
View File

@@ -0,0 +1,576 @@
using System;
using System.IO;
using System.Linq;
using System.Numerics;
using NDecrypt.Core;
using NDecrypt.N3DS.Headers;
using static NDecrypt.Core.Helper;
namespace NDecrypt.N3DS
{
// https://www.3dbrew.org/wiki/CIA
public class CIATool : ITool
{
/// <summary>
/// Name of the input CIA file
/// </summary>
private readonly string filename;
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs decryptArgs;
public CIATool(string filename, DecryptArgs decryptArgs)
{
this.filename = filename;
this.decryptArgs = decryptArgs;
}
#region Common Methods
/// <summary>
/// Process an input file given the input values
/// </summary>
public bool ProcessFile()
{
// 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 (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
{
CIAHeader header = CIAHeader.Read(reader);
if (header == null)
{
Console.WriteLine("Error: Not a 3DS CIA!");
return false;
}
// Process all NCCH partitions
ProcessAllPartitions(header, reader, writer);
}
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;
}
}
/// <summary>
/// Process all partitions in the content file data of a CIA header
/// </summary>
/// <param name="ciaHeader">CIA header representing the 3DS CIA file</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void ProcessAllPartitions(CIAHeader ciaHeader, BinaryReader reader, BinaryWriter writer)
{
// Iterate over all NCCH partitions
for (int p = 0; p < ciaHeader.Partitions.Length; p++)
{
NCCHHeader ncchHeader = ciaHeader.Partitions[0];
ProcessPartition(ciaHeader, ncchHeader, reader, writer);
}
}
/// <summary>
/// Process a single partition
/// </summary>
/// <param name="ciaHeader">CIA header representing the 3DS CIA file</param>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void ProcessPartition(CIAHeader ciaHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// If we're forcing the operation, tell the user
if (decryptArgs.Force)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} is not verified due to force flag being set.");
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (ncchHeader.Flags.PossblyDecrypted ^ decryptArgs.Encrypt)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber}: Already " + (decryptArgs.Encrypt ? "Encrypted" : "Decrypted") + "?...");
return;
}
// Determine the Keys to be used
SetEncryptionKeys(ciaHeader, ncchHeader);
// Process the extended header
ProcessExtendedHeader(ncchHeader, reader, writer);
// If we're encrypting, encrypt the filesystems and update the flags
if (decryptArgs.Encrypt)
{
EncryptExeFS(ncchHeader, reader, writer);
EncryptRomFS(ncchHeader, reader, writer);
UpdateEncryptCryptoAndMasks(ciaHeader, ncchHeader, writer);
}
// If we're decrypting, decrypt the filesystems and update the flags
else
{
DecryptExeFS(ncchHeader, reader, writer);
DecryptRomFS(ncchHeader, reader, writer);
UpdateDecryptCryptoAndMasks(ncchHeader, writer);
}
}
/// <summary>
/// Determine the set of keys to be used for encryption or decryption
/// </summary>
/// <param name="ciaHeader">NCSD header representing the 3DS CIA file</param>
/// <param name="ncchHeader">NCCH header representing the partition</param>
private void SetEncryptionKeys(CIAHeader ciaHeader, NCCHHeader ncchHeader)
{
ncchHeader.KeyX = 0;
ncchHeader.KeyX2C = decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C;
// Backup headers can't have a KeyY value set
if (ncchHeader.RSA2048Signature != null)
ncchHeader.KeyY = new BigInteger(ncchHeader.RSA2048Signature.Take(16).Reverse().ToArray());
else
ncchHeader.KeyY = new BigInteger(0);
ncchHeader.NormalKey = 0;
ncchHeader.NormalKey2C = RotateLeft((RotateLeft(ncchHeader.KeyX2C, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128);
// TODO: Figure out what sane defaults for these values are
// Set the header to use based on mode
BitMasks masks = BitMasks.NoCrypto;
CryptoMethod method = CryptoMethod.Original;
if (decryptArgs.Encrypt)
{
// TODO: Can we actually re-encrypt a CIA?
//masks = ciaHeader.BackupHeader.Flags.BitMasks;
//method = ciaHeader.BackupHeader.Flags.CryptoMethod;
}
else
{
masks = ncchHeader.Flags.BitMasks;
method = ncchHeader.Flags.CryptoMethod;
}
if (masks.HasFlag(BitMasks.FixedCryptoKey))
{
ncchHeader.NormalKey = 0x00;
ncchHeader.NormalKey2C = 0x00;
Console.WriteLine("Encryption Method: Zero Key");
}
else
{
if (method == CryptoMethod.Original)
{
ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C;
Console.WriteLine("Encryption Method: Key 0x2C");
}
else if (method == CryptoMethod.Seven)
{
ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x25 : decryptArgs.KeyX0x25;
Console.WriteLine("Encryption Method: Key 0x25");
}
else if (method == CryptoMethod.NineThree)
{
ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x18 : decryptArgs.KeyX0x18;
Console.WriteLine("Encryption Method: Key 0x18");
}
else if (method == CryptoMethod.NineSix)
{
ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x1B : decryptArgs.KeyX0x1B;
Console.WriteLine("Encryption Method: Key 0x1B");
}
ncchHeader.NormalKey = RotateLeft((RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128);
}
}
/// <summary>
/// Process the extended header, if it exists
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private bool ProcessExtendedHeader(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value?
uint mediaUnitSize = 0x200; // mediaUnitSize;
if (ncchHeader.ExtendedHeaderSizeInBytes > 0)
{
reader.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + ": ExHeader");
var cipher = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.PlainIV, decryptArgs.Encrypt);
writer.Write(cipher.ProcessBytes(reader.ReadBytes(Constants.CXTExtendedDataHeaderLength)));
writer.Flush();
return true;
}
else
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: No Extended Header... Skipping...");
return false;
}
return false;
}
/// <summary>
/// Process the extended header, if it exists
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void ProcessExeFSFileEntries(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value?
uint mediaUnitSize = 0x200; // mediaUnitSize;
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
ExeFSHeader exefsHeader = ExeFSHeader.Read(reader);
// If the header failed to read, log and return
if (exefsHeader == null)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS header could not be read. Skipping...");
return;
}
foreach (ExeFSFileHeader fileHeader in exefsHeader.FileHeaders)
{
// Only decrypt a file if it's a code binary
if (!fileHeader.IsCodeBinary)
continue;
uint datalenM = ((fileHeader.FileSize) / (1024 * 1024));
uint datalenB = ((fileHeader.FileSize) % (1024 * 1024));
uint ctroffset = ((fileHeader.FileOffset + mediaUnitSize) / 0x10);
byte[] exefsIVWithOffsetForHeader = AddToByteArray(ncchHeader.ExeFSIV, (int)ctroffset);
var firstCipher = CreateAESCipher(ncchHeader.NormalKey, exefsIVWithOffsetForHeader, decryptArgs.Encrypt);
var secondCipher = CreateAESCipher(ncchHeader.NormalKey2C, exefsIVWithOffsetForHeader, !decryptArgs.Encrypt);
reader.BaseStream.Seek((((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin);
writer.BaseStream.Seek((((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin);
if (datalenM > 0)
{
for (int i = 0; i < datalenM; i++)
{
writer.Write(secondCipher.ProcessBytes(firstCipher.ProcessBytes(reader.ReadBytes(1024 * 1024))));
writer.Flush();
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {i} / {datalenM + 1} mb...");
}
}
if (datalenB > 0)
{
writer.Write(secondCipher.DoFinal(firstCipher.DoFinal(reader.ReadBytes((int)datalenB))));
writer.Flush();
}
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {datalenM + 1} / {datalenM + 1} mb... Done!\r\n");
}
}
/// <summary>
/// Process the ExeFS Filename Table
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void ProcessExeFSFilenameTable(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value?
uint mediaUnitSize = 0x200; // mediaUnitSize;
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table");
var exeFSFilenameTable = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.ExeFSIV, decryptArgs.Encrypt);
writer.Write(exeFSFilenameTable.ProcessBytes(reader.ReadBytes((int)mediaUnitSize)));
writer.Flush();
}
/// <summary>
/// Process the ExeFS, if it exists
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void ProcessExeFS(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value?
uint mediaUnitSize = 0x200; // mediaUnitSize;
int exefsSizeM = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * mediaUnitSize) / (1024 * 1024));
int exefsSizeB = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * mediaUnitSize) % (1024 * 1024));
int ctroffsetE = (int)(mediaUnitSize / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(ncchHeader.ExeFSIV, ctroffsetE);
var exeFS = CreateAESCipher(ncchHeader.NormalKey2C, exefsIVWithOffset, decryptArgs.Encrypt);
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * mediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * mediaUnitSize, SeekOrigin.Begin);
if (exefsSizeM > 0)
{
for (int i = 0; i < exefsSizeM; i++)
{
writer.Write(exeFS.ProcessBytes(reader.ReadBytes(1024 * 1024)));
writer.Flush();
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {i} / {exefsSizeM + 1} mb");
}
}
if (exefsSizeB > 0)
{
writer.Write(exeFS.DoFinal(reader.ReadBytes(exefsSizeB)));
writer.Flush();
}
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {exefsSizeM + 1} / {exefsSizeM + 1} mb... Done!\r\n");
}
#endregion
#region Decrypt
/// <summary>
/// Decrypt the ExeFS, if it exists
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void DecryptExeFS(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// If the ExeFS size is 0, we log and return
if (ncchHeader.ExeFSSizeInMediaUnits == 0)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: No Data... Skipping...");
return;
}
// Decrypt the filename table
ProcessExeFSFilenameTable(ncchHeader, reader, writer);
// For all but the original crypto method, process each of the files in the table
if (ncchHeader.Flags.CryptoMethod != CryptoMethod.Original)
ProcessExeFSFileEntries(ncchHeader, reader, writer);
// Decrypt the rest of the ExeFS
ProcessExeFS(ncchHeader, reader, writer);
}
/// <summary>
/// Decrypt the RomFS, if it exists
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// TODO: See how much can be extracted into a common method with Encrypt
private void DecryptRomFS(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value?
uint mediaUnitSize = 0x200; // ncsdHeader.MediaUnitSize;
// If the RomFS offset is 0, we log and return
if (ncchHeader.RomFSOffsetInMediaUnits == 0)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} RomFS: No Data... Skipping...");
return;
}
long romfsSizeM = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * mediaUnitSize) / (1024 * 1024));
int romfsSizeB = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * mediaUnitSize) % (1024 * 1024));
var cipher = CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, decryptArgs.Encrypt);
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
if (romfsSizeM > 0)
{
for (int i = 0; i < romfsSizeM; i++)
{
writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024)));
writer.Flush();
Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Decrypting: {i} / {romfsSizeM + 1} mb");
}
}
if (romfsSizeB > 0)
{
writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB)));
writer.Flush();
}
Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Decrypting: {romfsSizeM + 1} / {romfsSizeM + 1} mb... Done!\r\n");
}
/// <summary>
/// Update the CryptoMethod and BitMasks for the decrypted partition
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void UpdateDecryptCryptoAndMasks(NCCHHeader ncchHeader, BinaryWriter writer)
{
// 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
writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x18B, SeekOrigin.Begin);
writer.Write((byte)CryptoMethod.Original);
writer.Flush();
// Write the new BitMasks flag
writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = ncchHeader.Flags.BitMasks;
flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag |= BitMasks.NoCrypto;
writer.Write((byte)flag);
writer.Flush();
}
#endregion
#region Encrypt
/// <summary>
/// Encrypt the ExeFS, if it exists
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void EncryptExeFS(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// If the ExeFS size is 0, we log and return
if (ncchHeader.ExeFSSizeInMediaUnits == 0)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: No Data... Skipping...");
return;
}
// 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)
// ProcessExeFSFileEntries(ncchHeader, reader, writer);
// Encrypt the filename table
ProcessExeFSFilenameTable(ncchHeader, reader, writer);
// Encrypt the rest of the ExeFS
ProcessExeFS(ncchHeader, reader, writer);
}
/// <summary>
/// Encrypt the RomFS, if it exists
/// </summary>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// TODO: See how much can be extracted into a common method with Decrypt
private void EncryptRomFS(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value?
uint mediaUnitSize = 0x200; // ncsdHeader.MediaUnitSize;
// If the RomFS offset is 0, we log and return
if (ncchHeader.RomFSOffsetInMediaUnits == 0)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} RomFS: No Data... Skipping...");
return;
}
long romfsSizeM = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * mediaUnitSize) / (1024 * 1024));
int romfsSizeB = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * mediaUnitSize) % (1024 * 1024));
// Encrypting RomFS for partitions 1 and up always use Key0x2C
if (ncchHeader.PartitionNumber > 0)
{
// TODO: Determine how to figure out the original crypto method, if possible
//if (ciaHeader.BackupHeader.Flags?.BitMasks.HasFlag(BitMasks.FixedCryptoKey) == true) // except if using zero-key
//{
// ncchHeader.NormalKey = 0x00;
//}
//else
//{
ncchHeader.KeyX = (decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C);
ncchHeader.NormalKey = RotateLeft((RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128);
//}
}
var cipher = CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, decryptArgs.Encrypt);
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
if (romfsSizeM > 0)
{
for (int i = 0; i < romfsSizeM; i++)
{
writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024)));
writer.Flush();
Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Encrypting: {i} / {romfsSizeM + 1} mb");
}
}
if (romfsSizeB > 0)
{
writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB)));
writer.Flush();
}
Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Encrypting: {romfsSizeM + 1} / {romfsSizeM + 1} mb... Done!\r\n");
}
/// <summary>
/// Update the CryptoMethod and BitMasks for the encrypted partition
/// </summary>
/// <param name="ciaHeader">CIA header representing the 3DS CIA file</param>
/// <param name="ncchHeader">NCCH header representing the partition</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
private void UpdateEncryptCryptoAndMasks(CIAHeader ciaHeader, NCCHHeader ncchHeader, BinaryWriter writer)
{
// 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
writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x18B, SeekOrigin.Begin);
// For partitions 1 and up, set crypto-method to 0x00
if (ncchHeader.PartitionNumber > 0)
writer.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);
writer.Flush();
// Write the new BitMasks flag
writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = ncchHeader.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;
writer.Write((byte)flag);
writer.Flush();
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
namespace NDecrypt.N3DS
{
internal class Constants
{
// Setup Keys and IVs
public static byte[] PlainCounter = new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static byte[] ExefsCounter = new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static byte[] RomfsCounter = new byte[] { 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public const int CXTExtendedDataHeaderLength = 0x800;
}
}

View File

@@ -3,7 +3,7 @@
namespace NDecrypt.N3DS
{
[Flags]
public enum ARM9AccessControlDescriptors : byte
internal enum ARM9AccessControlDescriptors : byte
{
MountNandRoot = 0x01,
MountNandroWriteAccess = 0x02,
@@ -18,7 +18,7 @@ namespace NDecrypt.N3DS
}
[Flags]
public enum ARM11LSCFlag0 : byte
internal enum ARM11LSCFlag0 : byte
{
IdealProcessor = 0x01 | 0x02,
AffinityMask = 0x04 | 0x08,
@@ -37,14 +37,14 @@ namespace NDecrypt.N3DS
}
[Flags]
public enum ARM11LSCFlag1 : byte
internal enum ARM11LSCFlag1 : byte
{
EnableL2Cache = 0x01,
Cpuspeed_804MHz = 0x02,
}
[Flags]
public enum ARM11LSCFlag2 : byte
internal enum ARM11LSCFlag2 : byte
{
/// <summary>
/// Value Description
@@ -58,7 +58,7 @@ namespace NDecrypt.N3DS
}
[Flags]
public enum BitMasks : byte
internal enum BitMasks : byte
{
FixedCryptoKey = 0x01,
NoMountRomFs = 0x02,
@@ -66,14 +66,32 @@ namespace NDecrypt.N3DS
NewKeyYGenerator = 0x20,
}
public enum ContentPlatform : byte
internal enum ContentIndex : ushort
{
/// <summary>
/// Main Content (.CXI for 3DS executable content/.CFA for 3DS Data Archives/.SRL for TWL content)
/// </summary>
MainContent = 0x0000,
/// <summary>
/// Home Menu Manual (.CFA)
/// </summary>
HomeMenuManual = 0x0001,
/// <summary>
/// DLP Child Container (.CFA)
/// </summary>
DLPChildContainer = 0x0002,
}
internal enum ContentPlatform : byte
{
CTR = 0x01,
Snake = 0x02, // New3DS
}
[Flags]
public enum ContentType : byte
internal enum ContentType : byte
{
Data = 0x01,
Executable = 0x02,
@@ -83,7 +101,7 @@ namespace NDecrypt.N3DS
Trial = 0x10,
}
public enum CryptoMethod : byte
internal enum CryptoMethod : byte
{
Original = 0x00,
Seven = 0x01,
@@ -92,7 +110,7 @@ namespace NDecrypt.N3DS
}
[Flags]
public enum FilesystemAccessInfo : ulong
internal enum FilesystemAccessInfo : ulong
{
CategorySystemApplication = 0x1,
CategoryHardwareCheck = 0x2,
@@ -118,7 +136,7 @@ namespace NDecrypt.N3DS
SeedDB = 0x200000, // Introduced in 9.6.0-X FIRM. Home Menu has this bit set starting with 9.6.0-X.
}
public enum FilesystemType : ulong
internal enum FilesystemType : ulong
{
None = 0,
Normal = 1,
@@ -126,19 +144,19 @@ namespace NDecrypt.N3DS
AGB_FIRMSave = 4,
}
public enum MediaCardDeviceType : byte
internal enum MediaCardDeviceType : byte
{
NORFlash = 0x01,
None = 0x02,
BT = 0x03,
}
public enum MediaPlatformIndex : byte
internal enum MediaPlatformIndex : byte
{
CTR = 0x01,
}
public enum MediaTypeIndex : byte
internal enum MediaTypeIndex : byte
{
InnerDevice = 0x00,
Card1 = 0x01,
@@ -146,7 +164,7 @@ namespace NDecrypt.N3DS
ExtendedDevice = 0x03,
}
public enum NCCHFlags
internal enum NCCHFlags
{
CryptoMethod = 0x03,
ContentPlatform = 0x04,
@@ -155,7 +173,7 @@ namespace NDecrypt.N3DS
BitMasks = 0x07,
}
public enum NCSDFlags
internal enum NCSDFlags
{
BackupWriteWaitTime = 0x00,
MediaCardDevice3X = 0x03,
@@ -165,7 +183,14 @@ namespace NDecrypt.N3DS
MediaCardDevice2X = 0x07,
}
public enum ResourceLimitCategory
internal enum PublicKeyType : uint
{
RSA_4096 = 0x00000000,
RSA_2048 = 0x01000000,
ECDSA = 0x02000000,
}
internal enum ResourceLimitCategory
{
APPLICATION = 0,
SYS_APPLET = 1,
@@ -173,10 +198,31 @@ namespace NDecrypt.N3DS
OTHER = 3,
}
// Note: These are reversed because of how C# reads values
internal enum SignatureType : uint
{
RSA_4096_SHA1 = 0x00000100,
RSA_2048_SHA1 = 0x01000100,
ECDSA_SHA1 = 0x02000100,
RSA_4096_SHA256 = 0x03000100,
RSA_2048_SHA256 = 0x04000100,
ECDSA_SHA256 = 0x05000100,
}
[Flags]
public enum StorageInfoOtherAttributes : byte
internal enum StorageInfoOtherAttributes : byte
{
NotUseROMFS = 0x01,
UseExtendedSavedataAccess = 0x02,
}
[Flags]
internal enum TMDContentType : ushort
{
Encrypted = 0x0001,
Disc = 0x0002,
CFM = 0x0004,
Optional = 0x4000,
Shared = 0x8000,
}
}

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class ARM11KernelCapabilities
internal class ARM11KernelCapabilities
{
/// <summary>
/// Descriptors

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class ARM11LocalSystemCapabilities
internal class ARM11LocalSystemCapabilities
{
/// <summary>
/// Program ID

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class ARM9AccessControl
internal class ARM9AccessControl
{
/// <summary>
/// Descriptors

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class AccessControlInfo
internal class AccessControlInfo
{
/// <summary>
/// ARM11 local system capabilities

View File

@@ -0,0 +1,149 @@
using System.Collections.Generic;
using System.IO;
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/CIA
internal class CIAHeader
{
/// <summary>
/// Archive header size, usually 0x2020 bytes
/// </summary>
public int HeaderSize { get; private set; }
/// <summary>
/// Type
/// </summary>
public ushort Type { get; private set; }
/// <summary>
/// Version
/// </summary>
public ushort Version { get; private set; }
/// <summary>
/// Certificate chain size
/// </summary>
public int CertificateChainSize { get; private set; }
/// <summary>
/// Ticket size
/// </summary>
public int TicketSize { get; private set; }
/// <summary>
/// TMD file size
/// </summary>
public int TMDFileSize { get; private set; }
/// <summary>
/// Meta size (0 if no Meta data is present)
/// </summary>
public int MetaSize { get; private set; }
/// <summary>
/// Content size
/// </summary>
public long ContentSize { get; private set; }
/// <summary>
/// Content Index
/// </summary>
public byte[] ContentIndex { get; private set; }
#region Content Index
/// <summary>
/// Certificate chain
/// </summary>
/// <remarks>
/// https://www.3dbrew.org/wiki/CIA#Certificate_Chain
/// </remarks>
public Certificate[] CertificateChain { get; set; }
/// <summary>
/// Ticket
/// </summary>
public Ticket Ticket { get; set; }
/// <summary>
/// TMD file data
/// </summary>
public TitleMetadata TMDFileData { get; set; }
/// <summary>
/// Content file data
/// </summary>
public NCCHHeader[] Partitions { get; set; }
/// <summary>
/// Meta file data (Not a necessary component)
/// </summary>
public MetaFile MetaFileData { get; set; }
#endregion
/// <summary>
/// Read from a stream and get a CIA header, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <returns>CIA header object, null on error</returns>
public static CIAHeader Read(BinaryReader reader)
{
CIAHeader header = new CIAHeader();
try
{
header.HeaderSize = reader.ReadInt32();
header.Type = reader.ReadUInt16();
header.Version = reader.ReadUInt16();
header.CertificateChainSize = reader.ReadInt32();
header.TicketSize = reader.ReadInt32();
header.TMDFileSize = reader.ReadInt32();
header.MetaSize = reader.ReadInt32();
header.ContentSize = reader.ReadInt64();
header.ContentIndex = reader.ReadBytes(0x2000);
if (reader.BaseStream.Position % 64 != 0)
reader.BaseStream.Seek(64 - (reader.BaseStream.Position % 64), SeekOrigin.Current);
header.CertificateChain = new Certificate[3];
header.CertificateChain[0] = Certificate.Read(reader); // CA
header.CertificateChain[1] = Certificate.Read(reader); // Ticket
header.CertificateChain[2] = Certificate.Read(reader); // TMD
if (reader.BaseStream.Position % 64 != 0)
reader.BaseStream.Seek(64 - (reader.BaseStream.Position % 64), SeekOrigin.Current);
header.Ticket = Ticket.Read(reader, header.TicketSize);
if (reader.BaseStream.Position % 64 != 0)
reader.BaseStream.Seek(64 - (reader.BaseStream.Position % 64), SeekOrigin.Current);
header.TMDFileData = TitleMetadata.Read(reader, header.TMDFileSize);
if (reader.BaseStream.Position % 64 != 0)
reader.BaseStream.Seek(64 - (reader.BaseStream.Position % 64), SeekOrigin.Current);
long startingPosition = reader.BaseStream.Position;
List<NCCHHeader> headers = new List<NCCHHeader>();
while (reader.BaseStream.Position < startingPosition + header.ContentSize)
{
long initPosition = reader.BaseStream.Position;
NCCHHeader ncchHeader = NCCHHeader.Read(reader, readSignature: true);
if (ncchHeader == null)
break;
headers.Add(ncchHeader);
reader.BaseStream.Seek(initPosition + ncchHeader.ContentSizeInMediaUnits * 0x200, SeekOrigin.Begin);
}
header.Partitions = headers.ToArray();
if (header.MetaSize > 0)
header.MetaFileData = MetaFile.Read(reader);
return header;
}
catch
{
return null;
}
}
}
}

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class CXIExtendedHeader
internal class CXIExtendedHeader
{
/// <summary>
/// SCI

View File

@@ -0,0 +1,156 @@
using System.IO;
using System.Text;
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/Certificates
internal class Certificate
{
/// <summary>
/// Signature Type
/// </summary>
public SignatureType SignatureType { get; private set; }
/// <summary>
/// Signature size
/// </summary>
public ushort SignatureSize { get; private set; }
/// <summary>
/// Padding size
/// </summary>
public byte PaddingSize { get; private set; }
/// <summary>
/// Signature
/// </summary>
public byte[] Signature { get; private set; }
/// <summary>
/// Issuer
/// </summary>
public byte[] Issuer { get; private set; }
/// <summary>
/// Issuer as a trimmed string
/// </summary>
public string IssuerString => Issuer != null && Issuer.Length > 0
? Encoding.ASCII.GetString(Issuer)?.TrimEnd('\0')
: null;
/// <summary>
/// Key Type
/// </summary>
public PublicKeyType KeyType { get; private set; }
/// <summary>
/// Name
/// </summary>
public byte[] Name { get; private set; }
/// <summary>
/// Name as a trimmed string
/// </summary>
public string NameString => Name != null && Name.Length > 0
? Encoding.ASCII.GetString(Name)?.TrimEnd('\0')
: null;
/// <summary>
/// Expiration time as UNIX Timestamp, used at least for CTCert
/// </summary>
public uint ExpirationTime { get; private set; }
// This contains the Public Key(i.e. Modulus & Public Exponent)
#region RSA
/// <summary>
/// Modulus
/// </summary>
public byte[] Modulus { get; private set; }
/// <summary>
/// Public Exponent
/// </summary>
public uint PublicExponent { get; private set; }
#endregion
// This contains the ECC public key, and is as follows:
#region ECC
/// <summary>
/// Public Key
/// </summary>
public byte[] PublicKey { get; private set; }
#endregion
/// <summary>
/// Read from a stream and get certificate, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <returns>Certificate object, null on error</returns>
public static Certificate Read(BinaryReader reader)
{
Certificate ct = new Certificate();
try
{
ct.SignatureType = (SignatureType)reader.ReadUInt32();
switch (ct.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
case SignatureType.RSA_4096_SHA256:
ct.SignatureSize = 0x200;
ct.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
case SignatureType.RSA_2048_SHA256:
ct.SignatureSize = 0x100;
ct.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
case SignatureType.ECDSA_SHA256:
ct.SignatureSize = 0x03C;
ct.PaddingSize = 0x40;
break;
default:
return null;
}
ct.Signature = reader.ReadBytes(ct.SignatureSize);
reader.ReadBytes(ct.PaddingSize); // Padding
ct.Issuer = reader.ReadBytes(0x40);
ct.KeyType = (PublicKeyType)reader.ReadUInt32();
ct.Name = reader.ReadBytes(0x40);
ct.ExpirationTime = reader.ReadUInt32();
switch (ct.KeyType)
{
case PublicKeyType.RSA_4096:
ct.Modulus = reader.ReadBytes(0x200);
ct.PublicExponent = reader.ReadUInt32();
reader.ReadBytes(0x34); // Padding
break;
case PublicKeyType.RSA_2048:
ct.Modulus = reader.ReadBytes(0x100);
ct.PublicExponent = reader.ReadUInt32();
reader.ReadBytes(0x34); // Padding
break;
case PublicKeyType.ECDSA:
ct.PublicKey = reader.ReadBytes(0x3C);
reader.ReadBytes(0x3C); // Padding
break;
default:
return null;
}
return ct;
}
catch
{
return null;
}
}
}
}

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class CodeSetInfo
internal class CodeSetInfo
{
/// <summary>
/// Address

View File

@@ -0,0 +1,59 @@
using System.IO;
namespace NDecrypt.N3DS.Headers
{
internal class ContentChunkRecord
{
/// <summary>
/// Content id
/// </summary>
public uint ContentId { get; private set; }
/// <summary>
/// Content index
/// </summary>
/// <remarks>
/// This does not apply to DLC.
/// </remarks>
public ContentIndex ContentIndex { get; private set; }
/// <summary>
/// Content type
/// </summary>
public TMDContentType ContentType { get; private set; }
/// <summary>
/// Content size
/// </summary>
public ulong ContentSize { get; private set; }
/// <summary>
/// SHA-256 hash
/// </summary>
public byte[] SHA256Hash { get; private set; }
/// <summary>
/// Read from a stream and get content chunk record, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <returns>Content chunk record object, null on error</returns>
public static ContentChunkRecord Read(BinaryReader reader)
{
ContentChunkRecord ccr = new ContentChunkRecord();
try
{
ccr.ContentId = reader.ReadUInt32();
ccr.ContentIndex = (ContentIndex)reader.ReadUInt16();
ccr.ContentType = (TMDContentType)reader.ReadUInt16();
ccr.ContentSize = reader.ReadUInt64();
ccr.SHA256Hash = reader.ReadBytes(0x20);
return ccr;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,44 @@
using System.IO;
namespace NDecrypt.N3DS.Headers
{
internal class ContentInfoRecord
{
/// <summary>
/// Content index offset
/// </summary>
public ushort ContentIndexOffset { get; private set; }
/// <summary>
/// Content command count [k]
/// </summary>
public ushort ContentCommandCount { get; private set; }
/// <summary>
/// SHA-256 hash of the next k content records that have not been hashed yet
/// </summary>
public byte[] UnhashedContentRecordsSHA256Hash { get; private set; }
/// <summary>
/// Read from a stream and get content info record, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <returns>Content info record object, null on error</returns>
public static ContentInfoRecord Read(BinaryReader reader)
{
ContentInfoRecord cir = new ContentInfoRecord();
try
{
cir.ContentIndexOffset = reader.ReadUInt16();
cir.ContentCommandCount = reader.ReadUInt16();
cir.UnhashedContentRecordsSHA256Hash = reader.ReadBytes(0x20);
return cir;
}
catch
{
return null;
}
}
}
}

View File

@@ -4,7 +4,7 @@ using System.Text;
namespace NDecrypt.N3DS.Headers
{
public class ExeFSFileHeader
internal class ExeFSFileHeader
{
// .code\0\0\0
private readonly byte[] codeSegmentBytes = new byte[] { 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x00, 0x00, 0x00 };

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class ExeFSHeader
internal class ExeFSHeader
{
/// <summary>
/// File headers (10 headers maximum, 16 bytes each)

View File

@@ -0,0 +1,57 @@
using System.IO;
namespace NDecrypt.N3DS.Headers
{
internal class MetaFile
{
/// <summary>
/// Title ID dependency list - Taken from the application's ExHeader
/// </summary>
public byte[] TitleIDDependencyList { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved1 { get; private set; }
/// <summary>
/// Core Version
/// </summary>
public uint CoreVersion { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved2 { get; private set; }
/// <summary>
/// Icon Data(.ICN) - Taken from the application's ExeFS
/// </summary>
public byte[] IconData { get; private set; }
/// <summary>
/// Read from a stream and get the Metafile data, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <returns>Metafile data object, null on error</returns>
public static MetaFile Read(BinaryReader reader)
{
MetaFile metaFile = new MetaFile();
try
{
metaFile.TitleIDDependencyList = reader.ReadBytes(0x180);
metaFile.Reserved1 = reader.ReadBytes(0x180);
metaFile.CoreVersion = reader.ReadUInt32();
metaFile.Reserved2 = reader.ReadBytes(0xFC);
metaFile.IconData = reader.ReadBytes(0x36C0);
return metaFile;
}
catch
{
return null;
}
}
}
}

View File

@@ -4,7 +4,7 @@ using System.Numerics;
namespace NDecrypt.N3DS.Headers
{
public class NCCHHeader
internal class NCCHHeader
{
private const string NCCHMagicNumber = "NCCH";
@@ -64,12 +64,12 @@ namespace NDecrypt.N3DS.Headers
/// <summary>
/// Maker code
/// </summary>
public byte[] MakerCode { get; private set; }
public ushort MakerCode { get; private set; }
/// <summary>
/// Version
/// </summary>
public byte[] Version { get; private set; }
public ushort Version { get; private set; }
/// <summary>
/// When ncchflag[7] = 0x20 starting with FIRM 9.6.0-X, this is compared with the first output u32 from a
@@ -77,7 +77,7 @@ namespace NDecrypt.N3DS.Headers
/// [programID from NCCH + 0x118]. This hash is only used for verification of the content lock seed, and
/// is not the actual keyY.
/// </summary>
public byte[] VerificationHash { get; private set; }
public uint VerificationHash { get; private set; }
/// <summary>
/// Program ID
@@ -211,9 +211,9 @@ namespace NDecrypt.N3DS.Headers
header.ContentSizeInMediaUnits = reader.ReadUInt32();
header.PartitionId = reader.ReadBytes(8).Reverse().ToArray();
header.MakerCode = reader.ReadBytes(2);
header.Version = reader.ReadBytes(2);
header.VerificationHash = reader.ReadBytes(4);
header.MakerCode = reader.ReadUInt16();
header.Version = reader.ReadUInt16();
header.VerificationHash = reader.ReadUInt32();
header.ProgramId = reader.ReadBytes(8);
header.Reserved1 = reader.ReadBytes(0x10);
header.LogoRegionHash = reader.ReadBytes(0x20);

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class NCCHHeaderFlags
internal class NCCHHeaderFlags
{
/// <summary>
/// Reserved

View File

@@ -3,7 +3,7 @@ using System.IO;
namespace NDecrypt.N3DS.Headers
{
public class NCSDHeader
internal class NCSDHeader
{
private const string NCSDMagicNumber = "NCSD";
@@ -299,7 +299,7 @@ namespace NDecrypt.N3DS.Headers
header.CardSeedAESMAC = reader.ReadBytes(0x10);
header.CardSeedNonce = reader.ReadBytes(0xC);
header.Reserved5 = reader.ReadBytes(0xC4);
header.BackupHeader = NCCHHeader.Read(reader, false);
header.BackupHeader = NCCHHeader.Read(reader, readSignature: false);
if (development)
{

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class PartitionTableEntry
internal class PartitionTableEntry
{
/// <summary>
/// Offset

View File

@@ -3,7 +3,7 @@
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/RomFS
public class RomFSHeader
internal class RomFSHeader
{
private const string RomFSMagicNumber = "IVFC";
private const uint RomFSSecondMagicNumber = 0x10000;

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class StorageInfo
internal class StorageInfo
{
/// <summary>
/// Extdata ID

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class SystemControlInfo
internal class SystemControlInfo
{
/// <summary>
/// Application title (default is "CtrApp")

View File

@@ -2,7 +2,7 @@
namespace NDecrypt.N3DS.Headers
{
public class SystemInfo
internal class SystemInfo
{
/// <summary>
/// SaveData Size

View File

@@ -0,0 +1,279 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/Ticket
internal class Ticket
{
/// <summary>
/// Signature Type
/// </summary>
public SignatureType SignatureType { get; private set; }
/// <summary>
/// Signature size
/// </summary>
public ushort SignatureSize { get; private set; }
/// <summary>
/// Padding size
/// </summary>
public byte PaddingSize { get; private set; }
/// <summary>
/// Signature
/// </summary>
public byte[] Signature { get; private set; }
/// <summary>
/// Issuer
/// </summary>
public byte[] Issuer { get; private set; }
/// <summary>
/// Issuer as a trimmed string
/// </summary>
public string IssuerString => Issuer != null && Issuer.Length > 0
? Encoding.ASCII.GetString(Issuer)?.TrimEnd('\0')
: null;
/// <summary>
/// ECC PublicKey
/// </summary>
public byte[] ECCPublicKey { get; private set; }
/// <summary>
/// Version (For 3DS this is always 1)
/// </summary>
public byte Version { get; private set; }
/// <summary>
/// CaCrlVersion
/// </summary>
public byte CaCrlVersion { get; private set; }
/// <summary>
/// SignerCrlVersion
/// </summary>
public byte SignerCrlVersion { get; private set; }
/// <summary>
/// TitleKey (normal-key encrypted using one of the common keyYs; see below)
/// </summary>
/// <remarks>
/// The titlekey is decrypted by using the AES engine with the ticket common-key keyslot.
/// The keyY is selected through an index (ticket offset 0xB1) into a plaintext array
/// of 6 keys ("common keyYs") stored in the data section of Process9. AES-CBC mode is used
/// where the IV is the big-endian titleID. Note that on a retail unit index0 is a retail keyY,
/// while on a dev-unit index0 is the dev common-key which is a normal-key.
/// (On retail for these keyYs, the hardware key-scrambler is used)
///
/// The titlekey is used to decrypt content downloaded from the CDN using 128-bit AES-CBC with
/// the content index (as big endian u16, padded with trailing zeroes) as the IV.
/// </remarks>
public byte[] TitleKey { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte Reserved1 { get; private set; }
/// <summary>
/// TicketID
/// </summary>
public ulong TicketID { get; private set; }
/// <summary>
/// ConsoleID
/// </summary>
public uint ConsoleID { get; private set; }
/// <summary>
/// TitleID
/// </summary>
public ulong TitleID { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public ushort Reserved2 { get; private set; }
/// <summary>
/// Ticket title version
/// </summary>
/// <remarks>
/// The Ticket Title Version is generally the same as the title version stored in the
/// Title Metadata. Although it doesn't have to match the TMD version to be valid.
/// </remarks>
public ushort TicketTitleVersion { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public ulong Reserved3 { get; private set; }
/// <summary>
/// License Type
/// </summary>
public byte LicenseType { get; private set; }
/// <summary>
/// Index to the common keyY used for this ticket, usually 0x1 for retail system titles;
/// see below.
/// </summary>
public byte CommonKeyYIndex { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved4 { get; private set; }
/// <summary>
/// eShop Account ID?
/// </summary>
public uint eShopAccountID { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte Reserved5 { get; private set; }
/// <summary>
/// Audit
/// </summary>
public byte Audit { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved6 { get; private set; }
/// <summary>
/// Limits
/// </summary>
/// <remarks>
/// In demos, the first u32 in the "Limits" section is 0x4, then the second u32 is the max-playcount.
/// </remarks>
public int[] Limits { get; private set; }
/// <summary>
/// Denotes if the ticket denotes a demo or not
/// </summary>
public bool IsDemo => Limits != null && Limits.Length > 0 ? Limits[0] == 0x0004 : false;
/// <summary>
/// Denotes if the max playcount for a demo
/// </summary>
public int PlayCount => Limits != null && Limits.Length > 1 ? Limits[1] : 0;
/// <summary>
/// The Content Index of a ticket has its own size defined within itself,
/// with seemingly a minimal of 20 bytes, the second u32 in big endian defines
/// the full value of X.
/// </summary>
public int ContentIndexSize { get; private set; }
/// <summary>
/// Content Index
/// </summary>
public byte[] ContentIndex { get; private set; }
/// <summary>
/// Certificate chain
/// </summary>
/// <remarks>
/// https://www.3dbrew.org/wiki/Ticket#Certificate_Chain
/// </remarks>
public Certificate[] CertificateChain { get; set; }
/// <summary>
/// Read from a stream and get ticket, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="ticketSize">Ticket size from the header</param>
/// <returns>Ticket object, null on error</returns>
public static Ticket Read(BinaryReader reader, int ticketSize)
{
Ticket tk = new Ticket();
try
{
long startingPosition = reader.BaseStream.Position;
tk.SignatureType = (SignatureType)reader.ReadUInt32();
switch (tk.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
case SignatureType.RSA_4096_SHA256:
tk.SignatureSize = 0x200;
tk.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
case SignatureType.RSA_2048_SHA256:
tk.SignatureSize = 0x100;
tk.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
case SignatureType.ECDSA_SHA256:
tk.SignatureSize = 0x03C;
tk.PaddingSize = 0x40;
break;
default:
return null;
}
tk.Signature = reader.ReadBytes(tk.SignatureSize);
reader.ReadBytes(tk.PaddingSize); // Padding
tk.Issuer = reader.ReadBytes(0x40);
tk.ECCPublicKey = reader.ReadBytes(0x3C);
tk.Version = reader.ReadByte();
tk.CaCrlVersion = reader.ReadByte();
tk.SignerCrlVersion = reader.ReadByte();
tk.TitleKey = reader.ReadBytes(0x10);
tk.Reserved1 = reader.ReadByte();
tk.TicketID = reader.ReadUInt64();
tk.ConsoleID = reader.ReadUInt32();
tk.TitleID = reader.ReadUInt64();
tk.Reserved2 = reader.ReadUInt16();
tk.TicketTitleVersion = reader.ReadUInt16();
tk.Reserved3 = reader.ReadUInt64();
tk.LicenseType = reader.ReadByte();
tk.CommonKeyYIndex = reader.ReadByte();
tk.Reserved4 = reader.ReadBytes(0x2A);
tk.eShopAccountID = reader.ReadUInt32();
tk.Reserved5 = reader.ReadByte();
tk.Audit = reader.ReadByte();
tk.Reserved6 = reader.ReadBytes(0x42);
tk.Limits = new int[0x10];
for (int i = 0; i < 0x10; i++)
{
tk.Limits[i] = reader.ReadInt32();
}
reader.ReadBytes(4); // Seek to size in Content Index
tk.ContentIndexSize = BitConverter.ToInt32(reader.ReadBytes(4).Reverse().ToArray(), 0);
reader.BaseStream.Seek(-8, SeekOrigin.Current);
tk.ContentIndex = reader.ReadBytes(tk.ContentIndexSize);
if (reader.BaseStream.Position % 64 != 0)
reader.BaseStream.Seek(64 - (reader.BaseStream.Position % 64), SeekOrigin.Current);
if (ticketSize > (reader.BaseStream.Position - startingPosition) + (2 * 0x200))
{
tk.CertificateChain = new Certificate[2];
tk.CertificateChain[0] = Certificate.Read(reader); // Ticket
tk.CertificateChain[1] = Certificate.Read(reader); // CA
}
return tk;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,241 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
namespace NDecrypt.N3DS.Headers
{
// https://www.3dbrew.org/wiki/Title_metadata
internal class TitleMetadata
{
/// <summary>
/// Signature Type
/// </summary>
public SignatureType SignatureType { get; private set; }
/// <summary>
/// Signature size
/// </summary>
public ushort SignatureSize { get; private set; }
/// <summary>
/// Padding size
/// </summary>
public byte PaddingSize { get; private set; }
/// <summary>
/// Signature
/// </summary>
public byte[] Signature { get; private set; }
/// <summary>
/// Signature Issuer
/// </summary>
public byte[] SignatureIssuer { get; private set; }
/// <summary>
/// Signature Issuer as a trimmed string
/// </summary>
public string SignatureIssuerString => SignatureIssuer != null && SignatureIssuer.Length > 0
? Encoding.ASCII.GetString(SignatureIssuer)?.TrimEnd('\0')
: null;
/// <summary>
/// Version
/// </summary>
public byte Version { get; private set; }
/// <summary>
/// CaCrlVersion
/// </summary>
public byte CaCrlVersion { get; private set; }
/// <summary>
/// SignerCrlVersion
/// </summary>
public byte SignerCrlVersion { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte Reserved1 { get; private set; }
/// <summary>
/// System Version
/// </summary>
public ulong SystemVersion { get; private set; }
/// <summary>
/// TitleID
/// </summary>
public ulong TitleID { get; private set; }
/// <summary>
/// Title Type
/// </summary>
public uint TitleType { get; private set; }
/// <summary>
/// Group ID
/// </summary>
public ushort GroupID { get; private set; }
/// <summary>
/// Save Data Size in Little Endian (Bytes) (Also SRL Public Save Data Size)
/// </summary>
public uint SaveDataSize { get; private set; }
/// <summary>
/// SRL Private Save Data Size in Little Endian (Bytes)
/// </summary>
public uint SRLPrivateSaveDataSize { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public uint Reserved2 { get; private set; }
/// <summary>
/// SRL Flag
/// </summary>
public byte SRLFlag { get; private set; }
/// <summary>
/// Reserved
/// </summary>
public byte[] Reserved3 { get; private set; }
/// <summary>
/// Access Rights
/// </summary>
public uint AccessRights { get; private set; }
/// <summary>
/// Title Version
/// </summary>
public ushort TitleVersion { get; private set; }
/// <summary>
/// Content Count
/// </summary>
public ushort ContentCount { get; private set; }
/// <summary>
/// Boot Content
/// </summary>
public ushort BootContent { get; private set; }
/// <summary>
/// Padding
/// </summary>
public ushort Padding { get; private set; }
/// <summary>
/// SHA-256 Hash of the Content Info Records
/// </summary>
public byte[] SHA256HashContentInfoRecords { get; private set; }
/// <summary>
/// There are 64 of these records, usually only the first is used.
/// </summary>
public ContentInfoRecord[] ContentInfoRecords { get; private set; }
/// <summary>
/// There is one of these for each content contained in this title.
/// (Determined by "Content Count" in the TMD Header).
/// </summary>
public ContentChunkRecord[] ContentChunkRecords { get; private set; }
/// <summary>
/// Certificate chain
/// </summary>
/// <remarks>
/// https://www.3dbrew.org/wiki/Title_metadata#Certificate_Chain
/// </remarks>
public Certificate[] CertificateChain { get; set; }
/// <summary>
/// Read from a stream and get ticket metadata, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="metadataSize">Metadata size from the header</param>
/// <returns>Title metadata object, null on error</returns>
public static TitleMetadata Read(BinaryReader reader, int metadataSize)
{
TitleMetadata tm = new TitleMetadata();
try
{
long startingPosition = reader.BaseStream.Position;
tm.SignatureType = (SignatureType)reader.ReadUInt32();
switch (tm.SignatureType)
{
case SignatureType.RSA_4096_SHA1:
case SignatureType.RSA_4096_SHA256:
tm.SignatureSize = 0x200;
tm.PaddingSize = 0x3C;
break;
case SignatureType.RSA_2048_SHA1:
case SignatureType.RSA_2048_SHA256:
tm.SignatureSize = 0x100;
tm.PaddingSize = 0x3C;
break;
case SignatureType.ECDSA_SHA1:
case SignatureType.ECDSA_SHA256:
tm.SignatureSize = 0x03C;
tm.PaddingSize = 0x40;
break;
}
tm.Signature = reader.ReadBytes(tm.SignatureSize);
reader.ReadBytes(tm.PaddingSize); // Padding
tm.SignatureIssuer = reader.ReadBytes(0x40);
tm.Version = reader.ReadByte();
tm.CaCrlVersion = reader.ReadByte();
tm.SignerCrlVersion = reader.ReadByte();
tm.Reserved1 = reader.ReadByte();
tm.SystemVersion = reader.ReadUInt64();
tm.TitleID = reader.ReadUInt64();
tm.TitleType = reader.ReadUInt32();
tm.GroupID = reader.ReadUInt16();
tm.SaveDataSize = reader.ReadUInt32();
tm.SRLPrivateSaveDataSize = reader.ReadUInt32();
tm.Reserved2 = reader.ReadUInt32();
tm.SRLFlag = reader.ReadByte();
tm.Reserved3 = reader.ReadBytes(0x31);
tm.AccessRights = reader.ReadUInt32();
tm.TitleVersion = reader.ReadUInt16();
tm.ContentCount = BitConverter.ToUInt16(reader.ReadBytes(2).Reverse().ToArray(), 0);
tm.BootContent = reader.ReadUInt16();
tm.Padding = reader.ReadUInt16();
tm.SHA256HashContentInfoRecords = reader.ReadBytes(0x20);
tm.ContentInfoRecords = new ContentInfoRecord[64];
for (int i = 0; i < 64; i++)
{
tm.ContentInfoRecords[i] = ContentInfoRecord.Read(reader);
}
tm.ContentChunkRecords = new ContentChunkRecord[tm.ContentCount];
for (int i = 0; i < tm.ContentCount; i++)
{
tm.ContentChunkRecords[i] = ContentChunkRecord.Read(reader);
}
if (metadataSize > (reader.BaseStream.Position - startingPosition) + (2 * 0x200))
{
tm.CertificateChain = new Certificate[2];
tm.CertificateChain[0] = Certificate.Read(reader); // TMD
tm.CertificateChain[1] = Certificate.Read(reader); // CA
}
return tm;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net6.0</TargetFrameworks>
<Authors>Matt Nadareski</Authors>
<Copyright>Copyright (c)2018-2023 Matt Nadareski</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/SabreTools/NDecrypt</RepositoryUrl>
<Version>0.2.5</Version>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<IncludeSource>true</IncludeSource>
<IncludeSymbols>true</IncludeSymbols>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NDecrypt.Core\NDecrypt.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,10 +2,11 @@
using System.IO;
using System.Linq;
using System.Numerics;
using NDecrypt.N3DS;
using NDecrypt.Core;
using NDecrypt.N3DS.Headers;
using static NDecrypt.Core.Helper;
namespace NDecrypt
namespace NDecrypt.N3DS
{
public class ThreeDSTool : ITool
{
@@ -15,26 +16,14 @@ namespace NDecrypt
private readonly string filename;
/// <summary>
/// Flag to detrmine if development keys should be used
/// Decryption args to use while processing
/// </summary>
private readonly bool development;
private readonly DecryptArgs decryptArgs;
/// <summary>
/// Flag to determine if encrypting or decrypting
/// </summary>
private readonly bool encrypt;
/// <summary>
/// Flag to determine if forcing operations
/// </summary>
private readonly bool force;
public ThreeDSTool(string filename, bool development, bool encrypt, bool force)
public ThreeDSTool(string filename, DecryptArgs decryptArgs)
{
this.filename = filename;
this.development = development;
this.encrypt = encrypt;
this.force = force;
this.decryptArgs = decryptArgs;
}
#region Common Methods
@@ -44,27 +33,38 @@ namespace NDecrypt
/// </summary>
public bool ProcessFile()
{
// Make sure we have a file to process first
Console.WriteLine(filename);
if (!File.Exists(filename))
return false;
// Open the read and write on the same file for inplace processing
using (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
// Ensure the constants are all set
if (decryptArgs.IsReady != true)
{
NCSDHeader header = NCSDHeader.Read(reader, development);
if (header == null)
{
Console.WriteLine("Error: Not a 3DS Rom!");
return false;
}
// Process all 8 NCCH partitions
ProcessAllPartitions(header, reader, writer);
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
return true;
try
{
// Open the read and write on the same file for inplace processing
using (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
{
NCSDHeader header = NCSDHeader.Read(reader, decryptArgs.Development);
if (header == null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
}
// Process all 8 NCCH partitions
ProcessAllPartitions(header, reader, writer);
}
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>
@@ -104,7 +104,7 @@ namespace NDecrypt
// Seek to the beginning of the NCCH partition
reader.BaseStream.Seek((ncsdHeader.PartitionsTable[partitionNumber].Offset * ncsdHeader.MediaUnitSize), SeekOrigin.Begin);
NCCHHeader partitionHeader = NCCHHeader.Read(reader, true);
NCCHHeader partitionHeader = NCCHHeader.Read(reader, readSignature: true);
if (partitionHeader == null)
{
Console.WriteLine($"Partition {partitionNumber} Unable to read NCCH header");
@@ -126,14 +126,14 @@ namespace NDecrypt
private void ProcessPartition(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
{
// If we're forcing the operation, tell the user
if (force)
if (decryptArgs.Force)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} is not verified due to force flag being set.");
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (ncchHeader.Flags.PossblyDecrypted ^ encrypt)
else if (ncchHeader.Flags.PossblyDecrypted ^ decryptArgs.Encrypt)
{
Console.WriteLine($"Partition {ncchHeader.PartitionNumber}: Already " + (encrypt ? "Encrypted" : "Decrypted") + "?...");
Console.WriteLine($"Partition {ncchHeader.PartitionNumber}: Already " + (decryptArgs.Encrypt ? "Encrypted" : "Decrypted") + "?...");
return;
}
@@ -144,7 +144,7 @@ namespace NDecrypt
ProcessExtendedHeader(ncsdHeader, ncchHeader, reader, writer);
// If we're encrypting, encrypt the filesystems and update the flags
if (encrypt)
if (decryptArgs.Encrypt)
{
EncryptExeFS(ncsdHeader, ncchHeader, reader, writer);
EncryptRomFS(ncsdHeader, ncchHeader, reader, writer);
@@ -168,7 +168,7 @@ namespace NDecrypt
private void SetEncryptionKeys(NCSDHeader ncsdHeader, NCCHHeader ncchHeader)
{
ncchHeader.KeyX = 0;
ncchHeader.KeyX2C = development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C;
ncchHeader.KeyX2C = decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C;
// Backup headers can't have a KeyY value set
if (ncchHeader.RSA2048Signature != null)
@@ -177,12 +177,12 @@ namespace NDecrypt
ncchHeader.KeyY = new BigInteger(0);
ncchHeader.NormalKey = 0;
ncchHeader.NormalKey2C = Helper.RotateLeft((Helper.RotateLeft(ncchHeader.KeyX2C, 2, 128) ^ ncchHeader.KeyY) + Constants.AESHardwareConstant, 87, 128);
ncchHeader.NormalKey2C = RotateLeft((RotateLeft(ncchHeader.KeyX2C, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128);
// Set the header to use based on mode
BitMasks masks;
CryptoMethod method;
if (encrypt)
if (decryptArgs.Encrypt)
{
masks = ncsdHeader.BackupHeader.Flags.BitMasks;
method = ncsdHeader.BackupHeader.Flags.CryptoMethod;
@@ -203,26 +203,26 @@ namespace NDecrypt
{
if (method == CryptoMethod.Original)
{
ncchHeader.KeyX = development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C;
ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C;
Console.WriteLine("Encryption Method: Key 0x2C");
}
else if (method == CryptoMethod.Seven)
{
ncchHeader.KeyX = development ? Constants.DevKeyX0x25 : Constants.KeyX0x25;
ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x25 : decryptArgs.KeyX0x25;
Console.WriteLine("Encryption Method: Key 0x25");
}
else if (method == CryptoMethod.NineThree)
{
ncchHeader.KeyX = development ? Constants.DevKeyX0x18 : Constants.KeyX0x18;
ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x18 : decryptArgs.KeyX0x18;
Console.WriteLine("Encryption Method: Key 0x18");
}
else if (method == CryptoMethod.NineSix)
{
ncchHeader.KeyX = development ? Constants.DevKeyX0x1B : Constants.KeyX0x1B;
ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x1B : decryptArgs.KeyX0x1B;
Console.WriteLine("Encryption Method: Key 0x1B");
}
ncchHeader.NormalKey = Helper.RotateLeft((Helper.RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + Constants.AESHardwareConstant, 87, 128);
ncchHeader.NormalKey = RotateLeft((RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128);
}
}
@@ -240,10 +240,12 @@ namespace NDecrypt
reader.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x200, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + ": ExHeader");
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + ": ExHeader");
var cipher = Helper.CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.PlainIV, encrypt);
writer.Write(cipher.ProcessBytes(reader.ReadBytes(Constants.CXTExtendedDataHeaderLength)));
var cipher = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.PlainIV, decryptArgs.Encrypt);
byte[] readBytes = reader.ReadBytes(Constants.CXTExtendedDataHeaderLength);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
writer.Write(processedBytes);
writer.Flush();
return true;
}
@@ -283,10 +285,10 @@ namespace NDecrypt
uint datalenB = ((fileHeader.FileSize) % (1024 * 1024));
uint ctroffset = ((fileHeader.FileOffset + ncsdHeader.MediaUnitSize) / 0x10);
byte[] exefsIVWithOffsetForHeader = Helper.AddToByteArray(ncchHeader.ExeFSIV, (int)ctroffset);
byte[] exefsIVWithOffsetForHeader = AddToByteArray(ncchHeader.ExeFSIV, (int)ctroffset);
var firstCipher = Helper.CreateAESCipher(ncchHeader.NormalKey, exefsIVWithOffsetForHeader, encrypt);
var secondCipher = Helper.CreateAESCipher(ncchHeader.NormalKey2C, exefsIVWithOffsetForHeader, !encrypt);
var firstCipher = CreateAESCipher(ncchHeader.NormalKey, exefsIVWithOffsetForHeader, decryptArgs.Encrypt);
var secondCipher = CreateAESCipher(ncchHeader.NormalKey2C, exefsIVWithOffsetForHeader, !decryptArgs.Encrypt);
reader.BaseStream.Seek((((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * ncsdHeader.MediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin);
writer.BaseStream.Seek((((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * ncsdHeader.MediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin);
@@ -295,19 +297,25 @@ namespace NDecrypt
{
for (int i = 0; i < datalenM; i++)
{
writer.Write(secondCipher.ProcessBytes(firstCipher.ProcessBytes(reader.ReadBytes(1024 * 1024))));
byte[] readBytes = reader.ReadBytes(1024 * 1024);
byte[] firstProcessedBytes = firstCipher.ProcessBytes(readBytes);
byte[] secondProcessedBytes = secondCipher.ProcessBytes(firstProcessedBytes);
writer.Write(secondProcessedBytes);
writer.Flush();
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {i} / {datalenM + 1} mb...");
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {i} / {datalenM + 1} mb...");
}
}
if (datalenB > 0)
{
writer.Write(secondCipher.DoFinal(firstCipher.DoFinal(reader.ReadBytes((int)datalenB))));
byte[] readBytes = reader.ReadBytes((int)datalenB);
byte[] firstFinalBytes = firstCipher.DoFinal(readBytes);
byte[] secondFinalBytes = secondCipher.DoFinal(firstFinalBytes);
writer.Write(secondFinalBytes);
writer.Flush();
}
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {datalenM + 1} / {datalenM + 1} mb... Done!\r\n");
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {datalenM + 1} / {datalenM + 1} mb... Done!\r\n");
}
}
@@ -323,10 +331,17 @@ namespace NDecrypt
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table");
Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table");
var exeFSFilenameTable = Helper.CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.ExeFSIV, encrypt);
writer.Write(exeFSFilenameTable.ProcessBytes(reader.ReadBytes((int)ncsdHeader.MediaUnitSize)));
var cipher = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.ExeFSIV, decryptArgs.Encrypt);
byte[] readBytes = reader.ReadBytes((int)ncsdHeader.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
writer.Write(processedBytes);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
reader.BaseStream.Seek(0, SeekOrigin.Begin);
#endif
writer.Flush();
}
@@ -343,9 +358,9 @@ namespace NDecrypt
int exefsSizeB = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * ncsdHeader.MediaUnitSize) % (1024 * 1024));
int ctroffsetE = (int)(ncsdHeader.MediaUnitSize / 0x10);
byte[] exefsIVWithOffset = Helper.AddToByteArray(ncchHeader.ExeFSIV, ctroffsetE);
byte[] exefsIVWithOffset = AddToByteArray(ncchHeader.ExeFSIV, ctroffsetE);
var exeFS = Helper.CreateAESCipher(ncchHeader.NormalKey2C, exefsIVWithOffset, encrypt);
var cipher = CreateAESCipher(ncchHeader.NormalKey2C, exefsIVWithOffset, decryptArgs.Encrypt);
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
@@ -353,18 +368,22 @@ namespace NDecrypt
{
for (int i = 0; i < exefsSizeM; i++)
{
writer.Write(exeFS.ProcessBytes(reader.ReadBytes(1024 * 1024)));
byte[] readBytes = reader.ReadBytes(1024 * 1024);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
writer.Write(processedBytes);
writer.Flush();
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {i} / {exefsSizeM + 1} mb");
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {i} / {exefsSizeM + 1} mb");
}
}
if (exefsSizeB > 0)
{
writer.Write(exeFS.DoFinal(reader.ReadBytes(exefsSizeB)));
byte[] readBytes = reader.ReadBytes(exefsSizeB);
byte[] finalBytes = cipher.DoFinal(readBytes);
writer.Write(finalBytes);
writer.Flush();
}
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {exefsSizeM + 1} / {exefsSizeM + 1} mb... Done!\r\n");
Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {exefsSizeM + 1} / {exefsSizeM + 1} mb... Done!\r\n");
}
#endregion
@@ -418,7 +437,7 @@ namespace NDecrypt
long romfsSizeM = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * ncsdHeader.MediaUnitSize) / (1024 * 1024));
int romfsSizeB = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * ncsdHeader.MediaUnitSize) % (1024 * 1024));
var cipher = Helper.CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, encrypt);
var cipher = CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, decryptArgs.Encrypt);
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
@@ -426,14 +445,18 @@ namespace NDecrypt
{
for (int i = 0; i < romfsSizeM; i++)
{
writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024)));
byte[] readBytes = reader.ReadBytes(1024 * 1024);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
writer.Write(processedBytes);
writer.Flush();
Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Decrypting: {i} / {romfsSizeM + 1} mb");
}
}
if (romfsSizeB > 0)
{
writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB)));
byte[] readBytes = reader.ReadBytes(romfsSizeB);
byte[] finalBytes = cipher.DoFinal(readBytes);
writer.Write(finalBytes);
writer.Flush();
}
@@ -522,12 +545,12 @@ namespace NDecrypt
}
else
{
ncchHeader.KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
ncchHeader.NormalKey = Helper.RotateLeft((Helper.RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + Constants.AESHardwareConstant, 87, 128);
ncchHeader.KeyX = (decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C);
ncchHeader.NormalKey = RotateLeft((RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128);
}
}
var cipher = Helper.CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, encrypt);
var cipher = CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, decryptArgs.Encrypt);
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
@@ -535,14 +558,18 @@ namespace NDecrypt
{
for (int i = 0; i < romfsSizeM; i++)
{
writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024)));
byte[] readBytes = reader.ReadBytes(1024 * 1024);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
writer.Write(processedBytes);
writer.Flush();
Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Encrypting: {i} / {romfsSizeM + 1} mb");
}
}
if (romfsSizeB > 0)
{
writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB)));
byte[] readBytes = reader.ReadBytes(romfsSizeB);
byte[] finalBytes = cipher.DoFinal(readBytes);
writer.Write(finalBytes);
writer.Flush();
}

View File

@@ -1,6 +1,6 @@
namespace NDecrypt.NDS
namespace NDecrypt.Nitro
{
public class Constants
internal class Constants
{
public static byte[] NDSEncryptionData = new byte[]
{

View File

@@ -1,10 +1,10 @@
using System;
using System.IO;
using System.Linq;
using NDecrypt.NDS;
using NDecrypt.NDS.Headers;
using NDecrypt.Core;
using NDecrypt.Nitro.Headers;
namespace NDecrypt
namespace NDecrypt.Nitro
{
public class DSTool : ITool
{
@@ -14,14 +14,9 @@ namespace NDecrypt
private readonly string filename;
/// <summary>
/// Flag to determine if encrypting or decrypting
/// Decryption args to use while processing
/// </summary>
private readonly bool encrypt;
/// <summary>
/// Flag to determine if forcing operations
/// </summary>
private readonly bool force;
private readonly DecryptArgs decryptArgs;
#region Encryption process variables
@@ -30,11 +25,10 @@ namespace NDecrypt
#endregion
public DSTool(string filename, bool encrypt, bool force)
public DSTool(string filename, DecryptArgs decryptArgs)
{
this.filename = filename;
this.encrypt = encrypt;
this.force = force;
this.decryptArgs = decryptArgs;
}
/// <summary>
@@ -42,27 +36,31 @@ namespace NDecrypt
/// </summary>
public bool ProcessFile()
{
// Make sure we have a file to process first
Console.WriteLine(filename);
if (!File.Exists(filename))
return false;
// Open the read and write on the same file for inplace processing
using (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
try
{
NDSHeader header = NDSHeader.Read(reader);
if (header == null)
// Open the read and write on the same file for inplace processing
using (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
NDSHeader header = NDSHeader.Read(reader);
if (header == null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
}
// Process the secure area
ProcessSecureArea(header, reader, writer);
}
// Process the secure area
ProcessSecureArea(header, reader, writer);
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 DS or DSi file and try again.");
return false;
}
return true;
}
/// <summary>
@@ -74,7 +72,7 @@ namespace NDecrypt
private void ProcessSecureArea(NDSHeader ndsHeader, BinaryReader reader, BinaryWriter writer)
{
// If we're forcing the operation, tell the user
if (force)
if (decryptArgs.Force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
@@ -87,16 +85,16 @@ namespace NDecrypt
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (encrypt ^ isDecrypted.Value)
else if (decryptArgs.Encrypt ^ isDecrypted.Value)
{
Console.WriteLine("File is already " + (encrypt ? "encrypted" : "decrypted"));
Console.WriteLine("File is already " + (decryptArgs.Encrypt ? "encrypted" : "decrypted"));
return;
}
}
ProcessARM9(ndsHeader, reader, writer);
Console.WriteLine("File has been " + (encrypt ? "encrypted" : "decrypted"));
Console.WriteLine("File has been " + (decryptArgs.Encrypt ? "encrypted" : "decrypted"));
}
/// <summary>
@@ -119,7 +117,10 @@ namespace NDecrypt
// Improperly decrypted empty secure area (decrypt empty with woodsec)
else if ((firstValue == 0xE386C397 && secondValue == 0x82775B7E)
|| (firstValue == 0xF98415B8 && secondValue == 0x698068FC))
|| (firstValue == 0xF98415B8 && secondValue == 0x698068FC)
|| (firstValue == 0xA71329EE && secondValue == 0x2A1D4C38)
|| (firstValue == 0xC44DCC48 && secondValue == 0x38B6F8CB)
|| (firstValue == 0x3A9323B5 && secondValue == 0xC0387241))
{
Console.WriteLine("Improperly decrypted empty secure area found. Should be encrypted to get proper value.");
return true;
@@ -136,7 +137,9 @@ namespace NDecrypt
// Properly decrypted nonstandard value (mastering issue)
else if ((firstValue == 0xD0D48B67 && secondValue == 0x39392F23) // Dragon Quest 5 (EU)
|| (firstValue == 0x014A191A && secondValue == 0xA5C470B9) // Dragon Quest 5 (USA)
|| (firstValue == 0x7829BC8D && secondValue == 0x9968EF44)) // Dragon Quest 5 (JP)
|| (firstValue == 0x7829BC8D && secondValue == 0x9968EF44) // Dragon Quest 5 (JP)
|| (firstValue == 0xC4A15AB8 && secondValue == 0xD2E667C8) // Prince of Persia (EU)
|| (firstValue == 0xD5E97D20 && secondValue == 0x21B2A159)) // Prince of Persia (USA)
{
Console.WriteLine("Decrypted secure area for known, nonstandard value found.");
return true;
@@ -149,13 +152,6 @@ namespace NDecrypt
return true;
}
// Properly encrypted prototype value
else if (firstValue == 0xA71329EE && secondValue == 0x2A1D4C38)
{
Console.WriteLine("Encrypted secure area for prototype found.");
return false;
}
// Standard decryption values
return firstValue == 0xE7FFDEFF && secondValue == 0xE7FFDEFF;
}
@@ -178,13 +174,13 @@ namespace NDecrypt
// Perform the initialization steps
Init1(ndsHeader);
if (!encrypt) Decrypt(ref p1, ref p0);
if (!decryptArgs.Encrypt) Decrypt(ref p1, ref p0);
arg2[1] <<= 1;
arg2[2] >>= 1;
Init2();
// If we're decrypting, set the proper flags
if (!encrypt)
if (!decryptArgs.Encrypt)
{
Decrypt(ref p1, ref p0);
if (p0 == Constants.MAGIC30 && p1 == Constants.MAGIC34)
@@ -208,7 +204,7 @@ namespace NDecrypt
p0 = reader.ReadUInt32();
p1 = reader.ReadUInt32();
if (encrypt)
if (decryptArgs.Encrypt)
Encrypt(ref p1, ref p0);
else
Decrypt(ref p1, ref p0);
@@ -220,7 +216,7 @@ namespace NDecrypt
}
// Replace the header explicitly if we're encrypting
if (encrypt)
if (decryptArgs.Encrypt)
{
reader.BaseStream.Seek(0x4000, SeekOrigin.Begin);
writer.BaseStream.Seek(0x4000, SeekOrigin.Begin);

View File

@@ -1,8 +1,8 @@
using System;
namespace NDecrypt.NDS
namespace NDecrypt.Nitro
{
public enum NDSUnitcode : byte
internal enum NDSUnitcode : byte
{
NDS = 0x00,
NDSPlusDSi = 0x02,

View File

@@ -1,8 +1,8 @@
using System.IO;
namespace NDecrypt.NDS.Headers
namespace NDecrypt.Nitro.Headers
{
public class NDSHeader
internal class NDSHeader
{
#region Common

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net6.0</TargetFrameworks>
<Authors>Matt Nadareski</Authors>
<Copyright>Copyright (c)2018-2023 Matt Nadareski</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/SabreTools/NDecrypt</RepositoryUrl>
<Version>0.2.5</Version>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<IncludeSource>true</IncludeSource>
<IncludeSymbols>true</IncludeSymbols>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NDecrypt.Core\NDecrypt.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,14 +1,20 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28803.156
# Visual Studio Version 17
VisualStudioVersion = 17.3.32922.545
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{31CE0445-F693-4C9A-B6CD-499C38CFF7FE}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NDecrypt", "NDecrypt\NDecrypt.csproj", "{91C54370-5741-4742-B2E9-EC498551AD1C}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDecrypt", "NDecrypt\NDecrypt.csproj", "{05566793-831F-4AE1-A6D2-F9214F36618E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDecrypt.Core", "NDecrypt.Core\NDecrypt.Core.csproj", "{91C54370-5741-4742-B2E9-EC498551AD1C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NDecrypt.N3DS", "NDecrypt.N3DS\NDecrypt.N3DS.csproj", "{F0A33533-B248-4D62-95CC-47DFC9721A11}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NDecrypt.Nitro", "NDecrypt.Nitro\NDecrypt.Nitro.csproj", "{1D2AE261-86FE-44E4-A7D3-C43AA5FB8523}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -16,10 +22,22 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{05566793-831F-4AE1-A6D2-F9214F36618E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05566793-831F-4AE1-A6D2-F9214F36618E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05566793-831F-4AE1-A6D2-F9214F36618E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05566793-831F-4AE1-A6D2-F9214F36618E}.Release|Any CPU.Build.0 = Release|Any CPU
{91C54370-5741-4742-B2E9-EC498551AD1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91C54370-5741-4742-B2E9-EC498551AD1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91C54370-5741-4742-B2E9-EC498551AD1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91C54370-5741-4742-B2E9-EC498551AD1C}.Release|Any CPU.Build.0 = Release|Any CPU
{F0A33533-B248-4D62-95CC-47DFC9721A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0A33533-B248-4D62-95CC-47DFC9721A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0A33533-B248-4D62-95CC-47DFC9721A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F0A33533-B248-4D62-95CC-47DFC9721A11}.Release|Any CPU.Build.0 = Release|Any CPU
{1D2AE261-86FE-44E4-A7D3-C43AA5FB8523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D2AE261-86FE-44E4-A7D3-C43AA5FB8523}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D2AE261-86FE-44E4-A7D3-C43AA5FB8523}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D2AE261-86FE-44E4-A7D3-C43AA5FB8523}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

119
NDecrypt/HashingHelper.cs Normal file
View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NDecrypt.Core.Tools;
namespace NDecrypt
{
internal static class HashingHelper
{
/// <summary>
/// Retrieve file information for a single file
/// </summary>
/// <param name="input">Filename to get information from</param>
/// <returns>Formatted string representing the hashes, null on error</returns>
public static string GetInfo(string input)
{
// If the file doesn't exist, return null
if (!File.Exists(input))
return null;
// Get the file length
long size = new FileInfo(input).Length;
// Open the file
Stream inputStream = File.OpenRead(input);
try
{
// Get a list of hashers to run over the buffer
List<Hasher> hashers = new List<Hasher>
{
new Hasher(Hash.CRC),
new Hasher(Hash.MD5),
new Hasher(Hash.SHA1),
new Hasher(Hash.SHA256),
};
// Initialize the hashing helpers
int buffersize = 3 * 1024 * 1024;
byte[] buffer = new byte[buffersize];
/*
Please note that some of the following code is adapted from
RomVault. This is a modified version of how RomVault does
threaded hashing. As such, some of the terminology and code
is the same, though variable names and comments may have
been tweaked to better fit this code base.
*/
// Pre load the buffer
int next = buffersize > size ? (int)size : buffersize;
int current = inputStream.Read(buffer, 0, next);
long refsize = size;
while (refsize > 0)
{
// Run hashes in parallel
if (current > 0)
Parallel.ForEach(hashers, h => h.Process(buffer, current));
// Load the next buffer
refsize -= current;
next = buffersize > refsize ? (int)refsize : buffersize;
if (next > 0)
current = inputStream.Read(buffer, 0, next);
}
// Finalize all hashing helpers
Parallel.ForEach(hashers, h => h.Terminate());
// Get the results
string result = $"Size: {size}\n"
+ $"CRC32: {ByteArrayToString(hashers.First(h => h.HashType == Hash.CRC).GetHash()) ?? ""}\n"
+ $"MD5: {ByteArrayToString(hashers.First(h => h.HashType == Hash.MD5).GetHash()) ?? ""}\n"
+ $"SHA1: {ByteArrayToString(hashers.First(h => h.HashType == Hash.SHA1).GetHash()) ?? ""}\n"
+ $"SHA256: {ByteArrayToString(hashers.First(h => h.HashType == Hash.SHA256).GetHash()) ?? ""}\n";
// Dispose of the hashers
hashers.ForEach(h => h.Dispose());
return result;
}
catch
{
return null;
}
finally
{
inputStream.Dispose();
}
}
/// <summary>
/// Convert a byte array to a hex string
/// </summary>
/// <param name="bytes">Byte array to convert</param>
/// <returns>Hex string representing the byte array</returns>
/// <link>http://stackoverflow.com/questions/311165/how-do-you-convert-byte-array-to-hexadecimal-string-and-vice-versa</link>
private static string ByteArrayToString(byte[] bytes)
{
// If we get null in, we send null out
if (bytes == null)
return null;
try
{
string hex = BitConverter.ToString(bytes);
return hex.Replace("-", string.Empty).ToLowerInvariant();
}
catch
{
return null;
}
}
}
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NDecrypt
{
public interface ITool
{
bool ProcessFile();
}
}

View File

@@ -1,101 +0,0 @@
using System.IO;
using System.Linq;
using System.Numerics;
using System.Reflection;
namespace NDecrypt.N3DS
{
public class Constants
{
// Setup Keys and IVs
public static byte[] PlainCounter = new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static byte[] ExefsCounter = new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static byte[] RomfsCounter = new byte[] { 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static BigInteger AESHardwareConstant { get; private set; }
#region Retail 3DS keys
// KeyX 0x18 (New 3DS 9.3)
public static BigInteger KeyX0x18 { get; private set; }
// KeyX 0x1B (New 3DS 9.6)
public static BigInteger KeyX0x1B { get; private set; }
// KeyX 0x25 (> 7.x)
public static BigInteger KeyX0x25 { get; private set; }
// KeyX 0x2C (< 6.x)
public static BigInteger KeyX0x2C { get; private set; }
#endregion
#region Dev 3DS Keys
// Dev KeyX 0x18 (New 3DS 9.3)
public static BigInteger DevKeyX0x18 { get; private set; }
// Dev KeyX 0x1B New 3DS 9.6)
public static BigInteger DevKeyX0x1B { get; private set; }
// Dev KeyX 0x25 (> 7.x)
public static BigInteger DevKeyX0x25 { get; private set; }
// Dev KeyX 0x2C (< 6.x)
public static BigInteger DevKeyX0x2C { get; private set; }
#endregion
public const int CXTExtendedDataHeaderLength = 0x800;
/// <summary>
/// Represents if all of the keys have been initialized properly
/// </summary>
public static bool IsReady { get; private set; }
/// <summary>
/// Setup all of the necessary constants
/// </summary>
/// <remarks>keys.bin should be in little endian format</remarks>
static Constants()
{
string keyfile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "keys.bin");
if (!File.Exists(keyfile))
{
IsReady = false;
return;
}
try
{
using (BinaryReader reader = new BinaryReader(File.Open(keyfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
// This is required to preserve sign for BigInteger
byte[] signByte = new byte[] { 0x00 };
// Hardware constant
AESHardwareConstant = new BigInteger(reader.ReadBytes(16));
// Retail keys
KeyX0x18 = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
KeyX0x1B = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
KeyX0x25 = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
KeyX0x2C = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
// Development keys
DevKeyX0x18 = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
DevKeyX0x1B = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
DevKeyX0x25 = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
DevKeyX0x2C = new BigInteger(reader.ReadBytes(16).Concat(signByte).ToArray());
}
}
catch
{
IsReady = false;
return;
}
IsReady = true;
}
}
}

View File

@@ -1,15 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net6.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<OutputType>Exe</OutputType>
<TargetFrameworks>net48;netcoreapp3.1;net5.0</TargetFrameworks>
<RuntimeIdentifiers>win10-x64;win7-x86</RuntimeIdentifiers>
<Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU;x64</Platforms>
<Title>NDecrypt</Title>
<AssemblyName>NDecrypt</AssemblyName>
<Description>DS/3DS Encryption Tool</Description>
<Authors>Matt Nadareski</Authors>
<Product>NDecrypt</Product>
<Copyright>Copyright (c)2018-2023 Matt Nadareski</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/SabreTools/NDecrypt</RepositoryUrl>
<Version>0.2.5</Version>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<IncludeSource>true</IncludeSource>
<IncludeSymbols>true</IncludeSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.NetCore" Version="1.8.8" />
<ProjectReference Include="..\NDecrypt.Core\NDecrypt.Core.csproj" />
<ProjectReference Include="..\NDecrypt.N3DS\NDecrypt.N3DS.csproj" />
<ProjectReference Include="..\NDecrypt.Nitro\NDecrypt.Nitro.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,11 +1,28 @@
using System;
using System.IO;
using System.Reflection;
using NDecrypt.Core;
using NDecrypt.N3DS;
using NDecrypt.Nitro;
namespace NDecrypt
{
class Program
{
/// <summary>
/// Type of the detected file
/// </summary>
private enum FileType
{
NULL,
NDS,
NDSi,
iQueDS,
N3DS,
iQue3DS,
N3DSCIA,
}
public static void Main(string[] args)
{
if (args.Length < 2)
@@ -14,14 +31,14 @@ namespace NDecrypt
return;
}
bool? encrypt;
var decryptArgs = new DecryptArgs();
if (args[0] == "decrypt" || args[0] == "d")
{
encrypt = false;
decryptArgs.Encrypt = false;
}
else if (args[0] == "encrypt" || args[0] == "e")
{
encrypt = true;
decryptArgs.Encrypt = true;
}
else
{
@@ -29,49 +46,101 @@ namespace NDecrypt
return;
}
bool development = false, force = false;
bool outputHashes = false;
int start = 1;
for ( ; start < args.Length; start++)
{
if (args[start] == "-dev" || args[start] == "--development")
development = true;
if (args[start] == "-c" || args[start] == "--citra")
{
decryptArgs.UseCitraKeyFile = true;
}
else if (args[start] == "-dev" || args[start] == "--development")
{
decryptArgs.Development = true;
}
else if (args[start] == "-f" || args[start] == "--force")
force = true;
{
decryptArgs.Force = true;
}
else if (args[start] == "-h" || args[start] == "--hash")
{
outputHashes = true;
}
else if (args[start] == "-k" || args[start] == "--keyfile")
{
if (start == args.Length - 1)
Console.WriteLine("Invalid keyfile path: no additional arguments found!");
start++;
string tempPath = args[start];
if (string.IsNullOrWhiteSpace(tempPath))
Console.WriteLine($"Invalid keyfile path: null or empty path found!");
tempPath = Path.GetFullPath(tempPath);
if (!File.Exists(tempPath))
Console.WriteLine($"Invalid keyfile path: file {tempPath} not found!");
else
decryptArgs.KeyFile = tempPath;
}
else
{
break;
}
}
// Ensure the constants are all set
new Constants();
if (!Constants.IsReady)
// Derive the keyfile path based on the runtime folder if not already set
if (string.IsNullOrWhiteSpace(decryptArgs.KeyFile))
{
Console.WriteLine("Could not read keys from keys.bin. Please make sure the file exists and try again");
return;
if (decryptArgs.UseCitraKeyFile)
decryptArgs.KeyFile = Path.Combine(Path.GetDirectoryName(AppContext.BaseDirectory), "aes_keys.txt");
else
decryptArgs.KeyFile = Path.Combine(Path.GetDirectoryName(AppContext.BaseDirectory), "keys.bin");
}
// If we are using a Citra keyfile, there are no development keys
if (decryptArgs.Development && decryptArgs.UseCitraKeyFile)
{
Console.WriteLine("Citra keyfiles don't contain development keys; disabling the option...");
decryptArgs.Development = false;
}
// Initialize the constants, if possible
decryptArgs.Initialize();
for (int i = start; i < args.Length; i++)
{
if (File.Exists(args[i]))
{
ITool tool = DeriveTool(args[i], encrypt.Value, development, force);
if (tool?.ProcessFile() != true)
Console.WriteLine("Processing failed!");
ProcessPath(args[i], decryptArgs, outputHashes);
}
else if (Directory.Exists(args[i]))
{
foreach (string file in Directory.EnumerateFiles(args[i], "*", SearchOption.AllDirectories))
{
ITool tool = DeriveTool(file, encrypt.Value, development, force);
if (tool?.ProcessFile() != true)
Console.WriteLine("Processing failed!");
ProcessPath(file, decryptArgs, outputHashes);
}
}
else
{
Console.WriteLine($"{args[i]} is not a file or folder. Please check your spelling and formatting and try again.");
}
}
}
Console.WriteLine("Press Enter to Exit...");
Console.Read();
/// <summary>
/// Display a basic help text
/// </summary>
/// <param name="path">Path to the file to process</param>
/// <param name="decryptArgs">DecryptArgs to use during processing</param>
/// <param name="outputHashes">True to write out a hashfile, false otherwise</param>
private static void ProcessPath(string path, DecryptArgs decryptArgs, bool outputHashes)
{
Console.WriteLine(path);
ITool tool = DeriveTool(path, decryptArgs);
if (tool?.ProcessFile() != true)
Console.WriteLine("Processing failed!");
else if (outputHashes)
WriteHashes(path);
}
/// <summary>
@@ -83,36 +152,50 @@ namespace NDecrypt
if (!string.IsNullOrWhiteSpace(err))
Console.WriteLine($"Error: {err}");
Console.WriteLine("Usage: NDecrypt.exe (decrypt|encrypt) [-dev] [-f] <file|dir> ...");
}
Console.WriteLine(@"Usage: NDecrypt.exe <opeation> [flags] <path> ...
private enum RomType
{
NULL,
NDS,
NDSi,
N3DS,
Possible values for <operation>:
e, encrypt - Encrypt the input files
d, decrypt - Decrypt the input files
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
<path> can be any file or folder that contains uncompressed items.
More than one path can be specified at a time.");
}
/// <summary>
/// Derive the encryption tool to be used for the given file
/// </summary>
/// <param name="filename">Filename to derive the tool from</param>
/// <param name="encrypt">True if we are encrypting the file, false otherwise</param>
/// <param name="development">True if we are using development keys, false otherwise</param>
/// <param name="force">True if operations should be forced, false otherwise</param>
/// <param name="decryptArgs">Arguments to pass to the tools on creation</param>
/// <returns></returns>
private static ITool DeriveTool(string filename, bool encrypt, bool development, bool force)
private static ITool DeriveTool(string filename, DecryptArgs decryptArgs)
{
RomType type = DetermineRomType(filename);
FileType type = DetermineFileType(filename);
switch(type)
{
case RomType.NDS:
case RomType.NDSi:
return new DSTool(filename, encrypt, force);
case RomType.N3DS:
return new ThreeDSTool(filename, development, encrypt, force);
case RomType.NULL:
case FileType.NDS:
Console.WriteLine("File recognized as Nintendo DS");
return new DSTool(filename, decryptArgs);
case FileType.NDSi:
Console.WriteLine("File recognized as Nintendo DS");
return new DSTool(filename, decryptArgs);
case FileType.iQueDS:
Console.WriteLine("File recognized as iQue DS");
return new DSTool(filename, decryptArgs);
case FileType.N3DS:
Console.WriteLine("File recognized as Nintendo 3DS");
return new ThreeDSTool(filename, decryptArgs);
case FileType.N3DSCIA:
Console.WriteLine("File recognized as Nintendo 3DS CIA [CAUTION: NOT WORKING CURRENTLY]");
return new CIATool(filename, decryptArgs);
case FileType.NULL:
default:
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds");
return null;
@@ -120,23 +203,52 @@ namespace NDecrypt
}
/// <summary>
/// Determine the rom type from the filename extension
/// Determine the file type from the filename extension
/// </summary>
/// <param name="filename">Filename to derive the type from</param>
/// <returns>RomType value, if possible</returns>
private static RomType DetermineRomType(string filename)
/// <returns>FileType value, if possible</returns>
private static FileType DetermineFileType(string filename)
{
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase)
|| filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase))
return RomType.NDS;
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase)) // Development carts/images
return FileType.NDS;
else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
return RomType.NDSi;
return FileType.NDSi;
else if (filename.EndsWith(".ids", StringComparison.OrdinalIgnoreCase))
return FileType.iQueDS;
else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase))
return RomType.N3DS;
return FileType.N3DS;
else if (filename.EndsWith(".cia", StringComparison.OrdinalIgnoreCase))
return FileType.N3DSCIA;
return RomType.NULL;
return FileType.NULL;
}
/// <summary>
/// Write out the hashes of a file to a named file
/// </summary>
/// <param name="filename">Filename to get hashes for/param>
private static void WriteHashes(string filename)
{
// If the file doesn't exist, don't try anything
if (!File.Exists(filename))
return;
// Get the hash string from the file
string hashString = HashingHelper.GetInfo(filename);
if (hashString == null)
return;
// Open the output file and write the hashes
using (var fs = File.Create(Path.GetFullPath(filename) + ".hash"))
using (var sw = new StreamWriter(fs))
{
sw.WriteLine(hashString);
}
}
}
}

View File

@@ -1,5 +1,7 @@
# NDecrypt
[![Build status](https://ci.appveyor.com/api/projects/status/cc1n298syn6r50mq?svg=true)](https://ci.appveyor.com/project/mnadareski/ndecrypt)
A simple tool for simple people.
## What is this?
@@ -16,39 +18,60 @@ This tool allows you to encrypt and decrypt your personally dumped NDS and N3DS
## So how do I use this?
NDecrypt.exe <flag> [-dev] <file|dir> ...
NDecrypt.exe <operation> [flags] <path> ...
Possible values for <flag>:
encrypt, e - Encrypt the incoming files
decrypt, d - Decrypt the incoming files
Possible values for <operation>:
e, encrypt - Encrypt the input files
d, decrypt - Decrypt the input files
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
<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 incoming files, so make backups if you're working on your original, personal dumps.
**Note:** This overwrites the input files, so make backups if you're working on your original, personal dumps.
**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.
## I feel like something is missing...
You 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 N3DS 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:
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:
- Hardware constant
- KeyX0x10
- KeyX0x1B
- KeyX0x25
- KeyX0x2C
- DevKeyX0x10
- KeyX0x18 / slot0x18KeyX
- KeyX0x1B / slot0x1BKeyX
- KeyX0x25 / slot0x25KeyX
- KeyX0x2C / slot0x2CKeyX
- DevKeyX0x18
- DevKeyX0x1B
- DevKeyX0x25
- DevKeyX0x2C
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.
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.
*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.
## But does it work?
As much as I'd like to think that this program is entirely without flaws, numbers need to speak for themselves sometimes. Here's a list of the supported sets and their current compatibility percentages with woodsec and the Python scripts (as of 2020-12-19):
- **Nintendo DS** - >99% compatible (Both encryption and decryption)
- **Nintendo DSi** - 100% compatible (Both encryption and decryption)
- **Nintendo 3DS** - 100% compatible (Both encryption and decryption)
- **Nintendo New 3DS** - 100% compatible (Both encryption and decryption)
Please note the above numbers are based on the current, documented values. The notable exceptions to this tend to be unlicensed carts which may be dumped incorrectly or have odd information stored in their secure area.
## Anything else?
I'd like to thank the developers of the original programs for doing the actual hard work to figure things out. I'd also like to thank everyone who helped to test this against the original programs and made code suggestions.
Unofficially, this is entirely, 100% FOSS, no strings attached. I keep forgetting what license that is.
## Disclaimer
This program is **ONLY** for use with personally dumped files and keys and is not meant for enabling illegal activity. I do not condone using this program for anything other than personal use and research. If this program is used for anything other than that, I cannot be held liable for anything that happens.

61
appveyor.yml Normal file
View File

@@ -0,0 +1,61 @@
# version format
version: 0.2.5-{build}
# pull request template
pull_requests:
do_not_increment_build_number: true
# vm template
image: Visual Studio 2022
# msbuild configuration
configuration:
- Debug
# install dependencies
install:
- cd %APPVEYOR_BUILD_FOLDER%
- git submodule update --init --recursive
# build step
build_script:
- cmd: dotnet restore
- cmd: dotnet build NDecrypt\NDecrypt.csproj --framework net48
- cmd: dotnet publish NDecrypt\NDecrypt.csproj --framework net6.0 --runtime win-x86 --self-contained true -p:PublishSingleFile=true
- cmd: dotnet publish NDecrypt\NDecrypt.csproj --framework net6.0 --runtime win-x64 --self-contained true -p:PublishSingleFile=true
- cmd: dotnet publish NDecrypt\NDecrypt.csproj --framework net6.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true
- cmd: dotnet publish NDecrypt\NDecrypt.csproj --framework net6.0 --runtime osx-x64 --self-contained true -p:PublishSingleFile=true
# post-build script
after_build:
- cmd: cd %APPVEYOR_BUILD_FOLDER%\NDecrypt\bin\Debug\net48
- cmd: 7z a -tzip %APPVEYOR_BUILD_FOLDER%\NDecrypt-%APPVEYOR_REPO_COMMIT%_net48.zip *
- cmd: cd %APPVEYOR_BUILD_FOLDER%\NDecrypt\bin\Debug\net6.0\win-x86\publish\
- cmd: 7z a -tzip %APPVEYOR_BUILD_FOLDER%\NDecrypt-%APPVEYOR_REPO_COMMIT%_net6.0_win-x86.zip *
- cmd: cd %APPVEYOR_BUILD_FOLDER%\NDecrypt\bin\Debug\net6.0\win-x64\publish\
- cmd: 7z a -tzip %APPVEYOR_BUILD_FOLDER%\NDecrypt-%APPVEYOR_REPO_COMMIT%_net6.0_win-x64.zip *
- cmd: cd %APPVEYOR_BUILD_FOLDER%\NDecrypt\bin\Debug\net6.0\linux-x64\publish\
- cmd: 7z a -tzip %APPVEYOR_BUILD_FOLDER%\NDecrypt-%APPVEYOR_REPO_COMMIT%_net6.0_linux-x64.zip *
- cmd: cd %APPVEYOR_BUILD_FOLDER%\NDecrypt\bin\Debug\net6.0\osx-x64\publish\
- cmd: 7z a -tzip %APPVEYOR_BUILD_FOLDER%\NDecrypt-%APPVEYOR_REPO_COMMIT%_net6.0_osx-x64.zip *
# 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
# artifact linking
artifacts:
- path: NDecrypt-%APPVEYOR_REPO_COMMIT%_net48.zip
name: NDecrypt (.NET Framework 4.8)
- path: NDecrypt-%APPVEYOR_REPO_COMMIT%_net6.0_win-x86.zip
name: NDecrypt (.NET 6.0, Windows x86)
- path: NDecrypt-%APPVEYOR_REPO_COMMIT%_net6.0_win-x64.zip
name: NDecrypt (.NET 6.0, Windows x64)
- path: NDecrypt-%APPVEYOR_REPO_COMMIT%_net6.0_linux-x64.zip
name: NDecrypt (.NET 6.0, Linux x64)
- path: NDecrypt-%APPVEYOR_REPO_COMMIT%_net6.0_osx-x64.zip
name: NDecrypt (.NET 6.0, OSX x64)