mirror of
https://github.com/SabreTools/NDecrypt.git
synced 2026-02-06 13:54:47 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16a84f0dc6 | ||
|
|
7fbe044ab8 | ||
|
|
7c9a035d7c | ||
|
|
964e6f243c | ||
|
|
b454e42f8e | ||
|
|
9f70eae8be | ||
|
|
20987573f4 | ||
|
|
2fb05b577e | ||
|
|
f72b8c3506 | ||
|
|
8b32b21475 | ||
|
|
2779f5ef72 | ||
|
|
bbbf71a603 | ||
|
|
bea979dca5 | ||
|
|
5a0169310b | ||
|
|
67c83bd126 | ||
|
|
6fdcc2effe | ||
|
|
9ff0205e15 | ||
|
|
c91d7f2708 | ||
|
|
d9b333e3ba | ||
|
|
ef5a01edf7 | ||
|
|
012787a5b1 | ||
|
|
df5664fa1f | ||
|
|
bef252f57f | ||
|
|
b78db22464 | ||
|
|
6d980c94fd | ||
|
|
8452cb4fc0 | ||
|
|
0deaa0aed3 | ||
|
|
9b5f90a750 | ||
|
|
7415a21d5c | ||
|
|
f349389994 | ||
|
|
a046bd4152 | ||
|
|
51a8f0c9df | ||
|
|
0449c16a01 | ||
|
|
7638d7dbf8 | ||
|
|
387bf46e5a | ||
|
|
6d257dc1e3 | ||
|
|
91d816e359 | ||
|
|
e90f7a76af | ||
|
|
9750ae5a1e | ||
|
|
fa518ed5a5 | ||
|
|
350acd7be4 | ||
|
|
dde0c96e6c | ||
|
|
36e6e803cc | ||
|
|
e669936e08 | ||
|
|
dc55511a84 | ||
|
|
b9b8c76e84 | ||
|
|
10fcd51e10 | ||
|
|
a0878d0bf4 | ||
|
|
ed55f76d1e | ||
|
|
a726fc26d9 | ||
|
|
414df7808e | ||
|
|
d61e03687b | ||
|
|
cccb4e6261 | ||
|
|
df7ff2b95a | ||
|
|
52a928638b | ||
|
|
5962fc6072 | ||
|
|
5807d4ddc2 | ||
|
|
6fa8867f93 | ||
|
|
ed45d2d14e | ||
|
|
69ecb0d379 | ||
|
|
5974e2371e | ||
|
|
1e5e3badbc |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -258,4 +258,7 @@ paket-files/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyc
|
||||
|
||||
# VSCode
|
||||
/NDecrypt/Properties/launchSettings.json
|
||||
|
||||
27
.vscode/launch.json
vendored
Normal file
27
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": ".NET Core Launch (console)",
|
||||
"type": "coreclr",
|
||||
"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",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/NDecrypt",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
.vscode/tasks.json
vendored
Normal file
24
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "shell",
|
||||
"args": [
|
||||
"build",
|
||||
// Ask dotnet build to generate full paths for file names.
|
||||
"/property:GenerateFullPaths=true",
|
||||
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27004.2006
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "3DSDecrypt", "3DSDecrypt\3DSDecrypt.csproj", "{2E30006A-3C60-4576-A262-937B21C83C06}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2E30006A-3C60-4576-A262-937B21C83C06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2E30006A-3C60-4576-A262-937B21C83C06}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2E30006A-3C60-4576-A262-937B21C83C06}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2E30006A-3C60-4576-A262-937B21C83C06}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {5AB50D9B-BA18-4F96-804B-52E7E0845B37}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,80 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{2E30006A-3C60-4576-A262-937B21C83C06}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>NDecrypt</RootNamespace>
|
||||
<AssemblyName>3DSDecrypt</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="BouncyCastle.Crypto, Version=1.8.4.0, Culture=neutral, PublicKeyToken=0e99375e54769942">
|
||||
<HintPath>..\packages\BouncyCastle.1.8.4\lib\BouncyCastle.Crypto.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Numerics" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Data\Constants.cs" />
|
||||
<Compile Include="DSTool.cs" />
|
||||
<Compile Include="Headers\NCCHHeaderFlags.cs" />
|
||||
<Compile Include="Headers\NDSHeader.cs" />
|
||||
<Compile Include="Headers\RomFSHeader.cs" />
|
||||
<Compile Include="Helper.cs" />
|
||||
<Compile Include="ITool.cs" />
|
||||
<Compile Include="ThreeDSTool.cs" />
|
||||
<Compile Include="Data\Enums.cs" />
|
||||
<Compile Include="Headers\AccessControlInfo.cs" />
|
||||
<Compile Include="Headers\ARM11KernelCapabilities.cs" />
|
||||
<Compile Include="Headers\ARM11LocalSystemCapabilities.cs" />
|
||||
<Compile Include="Headers\ARM9AccessControl.cs" />
|
||||
<Compile Include="Headers\CodeSetInfo.cs" />
|
||||
<Compile Include="Headers\CXIExtendedHeader.cs" />
|
||||
<Compile Include="Headers\ExeFSFileHeader.cs" />
|
||||
<Compile Include="Headers\ExeFSHeader.cs" />
|
||||
<Compile Include="Headers\NCCHHeader.cs" />
|
||||
<Compile Include="Headers\NCSDHeader.cs" />
|
||||
<Compile Include="Headers\PartitionTableEntry.cs" />
|
||||
<Compile Include="Headers\StorageInfo.cs" />
|
||||
<Compile Include="Headers\SystemControlInfo.cs" />
|
||||
<Compile Include="Headers\SystemInfo.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using NDecrypt.Headers;
|
||||
|
||||
namespace NDecrypt
|
||||
{
|
||||
public class DSTool : ITool
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the input DS/DSi file
|
||||
/// </summary>
|
||||
private readonly string filename;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to determine if encrypting or decrypting
|
||||
/// </summary>
|
||||
private readonly bool encrypt;
|
||||
|
||||
public DSTool(string filename, bool encrypt)
|
||||
{
|
||||
this.filename = filename;
|
||||
this.encrypt = encrypt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process an input file given the input values
|
||||
/// </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)))
|
||||
{
|
||||
NDSHeader header = NDSHeader.Read(reader);
|
||||
if (header == null)
|
||||
{
|
||||
Console.WriteLine("Error: Not a DS or DSi Rom!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process the secure area
|
||||
header.ProcessSecureArea(reader, writer, encrypt);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,601 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using NDecrypt.Data;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
{
|
||||
public class NCCHHeader
|
||||
{
|
||||
private const string NCCHMagicNumber = "NCCH";
|
||||
|
||||
/// <summary>
|
||||
/// Partition number for the current partition
|
||||
/// </summary>
|
||||
public int PartitionNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Partition table entry for the current partition
|
||||
/// </summary>
|
||||
public PartitionTableEntry Entry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// RSA-2048 signature of the NCCH header, using SHA-256.
|
||||
/// </summary>
|
||||
public byte[] RSA2048Signature { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Content size, in media units (1 media unit = 0x200 bytes)
|
||||
/// </summary>
|
||||
public uint ContentSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Partition ID
|
||||
/// </summary>
|
||||
public byte[] PartitionId { get; private set; }
|
||||
public byte[] PlainIV { get { return PartitionId.Concat(Constants.PlainCounter).ToArray(); } }
|
||||
public byte[] ExeFSIV { get { return PartitionId.Concat(Constants.ExefsCounter).ToArray(); } }
|
||||
public byte[] RomFSIV { get { return PartitionId.Concat(Constants.RomfsCounter).ToArray(); } }
|
||||
|
||||
/// <summary>
|
||||
/// Boot rom key
|
||||
/// </summary>
|
||||
private BigInteger KeyX;
|
||||
|
||||
/// <summary>
|
||||
/// NCCH boot rom key
|
||||
/// </summary>
|
||||
private BigInteger KeyX2C;
|
||||
|
||||
/// <summary>
|
||||
/// Kernel9/Process9 key
|
||||
/// </summary>
|
||||
private BigInteger KeyY;
|
||||
|
||||
/// <summary>
|
||||
/// Normal AES key
|
||||
/// </summary>
|
||||
private BigInteger NormalKey;
|
||||
|
||||
/// <summary>
|
||||
/// NCCH AES key
|
||||
/// </summary>
|
||||
private BigInteger NormalKey2C;
|
||||
|
||||
/// <summary>
|
||||
/// Maker code
|
||||
/// </summary>
|
||||
public byte[] MakerCode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version
|
||||
/// </summary>
|
||||
public byte[] 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
|
||||
/// SHA256 hash. The data used for that hash is 0x18-bytes: [0x10-long title-unique content lock seed]
|
||||
/// [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; }
|
||||
|
||||
/// <summary>
|
||||
/// Program ID
|
||||
/// </summary>
|
||||
public byte[] ProgramId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
/// </summary>
|
||||
public byte[] Reserved1 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo Region SHA-256 hash. (For applications built with SDK 5+) (Supported from firmware: 5.0.0-11)
|
||||
/// </summary>
|
||||
public byte[] LogoRegionHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Product code
|
||||
/// </summary>
|
||||
public byte[] ProductCode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Extended header SHA-256 hash (SHA256 of 2x Alignment Size, beginning at 0x0 of ExHeader)
|
||||
/// </summary>
|
||||
public byte[] ExtendedHeaderHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Extended header size, in bytes
|
||||
/// </summary>
|
||||
public uint ExtendedHeaderSizeInBytes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
/// </summary>
|
||||
public byte[] Reserved2 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flags
|
||||
/// </summary>
|
||||
public NCCHHeaderFlags Flags { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plain region offset, in media units
|
||||
/// </summary>
|
||||
public uint PlainRegionOffsetInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plain region size, in media units
|
||||
/// </summary>
|
||||
public uint PlainRegionSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo Region offset, in media units (For applications built with SDK 5+) (Supported from firmware: 5.0.0-11)
|
||||
/// </summary>
|
||||
public uint LogoRegionOffsetInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo Region size, in media units (For applications built with SDK 5+) (Supported from firmware: 5.0.0-11)
|
||||
/// </summary>
|
||||
public uint LogoRegionSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ExeFS offset, in media units
|
||||
/// </summary>
|
||||
public uint ExeFSOffsetInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ExeFS size, in media units
|
||||
/// </summary>
|
||||
public uint ExeFSSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ExeFS hash region size, in media units
|
||||
/// </summary>
|
||||
public uint ExeFSHashRegionSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
/// </summary>
|
||||
public byte[] Reserved3 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// RomFS offset, in media units
|
||||
/// </summary>
|
||||
public uint RomFSOffsetInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// RomFS size, in media units
|
||||
/// </summary>
|
||||
public uint RomFSSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// RomFS hash region size, in media units
|
||||
/// </summary>
|
||||
public uint RomFSHashRegionSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
/// </summary>
|
||||
public byte[] Reserved4 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ExeFS superblock SHA-256 hash - (SHA-256 hash, starting at 0x0 of the ExeFS over the number of
|
||||
/// media units specified in the ExeFS hash region size)
|
||||
/// </summary>
|
||||
public byte[] ExeFSSuperblockHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// RomFS superblock SHA-256 hash - (SHA-256 hash, starting at 0x0 of the RomFS over the number
|
||||
/// of media units specified in the RomFS hash region size)
|
||||
/// </summary>
|
||||
public byte[] RomFSSuperblockHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Read from a stream and get an NCCH header, if possible
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="readSignature">True if the RSA signature is read, false otherwise</param>
|
||||
/// <returns>NCCH header object, null on error</returns>
|
||||
public static NCCHHeader Read(BinaryReader reader, bool readSignature)
|
||||
{
|
||||
NCCHHeader header = new NCCHHeader();
|
||||
|
||||
try
|
||||
{
|
||||
if (readSignature)
|
||||
header.RSA2048Signature = reader.ReadBytes(0x100);
|
||||
|
||||
if (new string(reader.ReadChars(4)) != NCCHMagicNumber)
|
||||
return null;
|
||||
|
||||
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.ProgramId = reader.ReadBytes(8);
|
||||
header.Reserved1 = reader.ReadBytes(0x10);
|
||||
header.LogoRegionHash = reader.ReadBytes(0x20);
|
||||
header.ProductCode = reader.ReadBytes(0x10);
|
||||
header.ExtendedHeaderHash = reader.ReadBytes(0x20);
|
||||
header.ExtendedHeaderSizeInBytes = reader.ReadUInt32();
|
||||
header.Reserved2 = reader.ReadBytes(4);
|
||||
header.Flags = NCCHHeaderFlags.Read(reader);
|
||||
header.PlainRegionOffsetInMediaUnits = reader.ReadUInt32();
|
||||
header.PlainRegionSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.LogoRegionOffsetInMediaUnits = reader.ReadUInt32();
|
||||
header.LogoRegionSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.ExeFSOffsetInMediaUnits = reader.ReadUInt32();
|
||||
header.ExeFSSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.ExeFSHashRegionSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.Reserved3 = reader.ReadBytes(4);
|
||||
header.RomFSOffsetInMediaUnits = reader.ReadUInt32();
|
||||
header.RomFSSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.RomFSHashRegionSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.Reserved4 = reader.ReadBytes(4);
|
||||
header.ExeFSSuperblockHash = reader.ReadBytes(0x20);
|
||||
header.RomFSSuperblockHash = reader.ReadBytes(0x20);
|
||||
|
||||
return header;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a single partition
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
/// <param name="header">NCSD header representing the 3DS file</param>
|
||||
/// <param name="encrypt">True if we want to encrypt the partitions, false otherwise</param>
|
||||
/// <param name="development">True if development keys should be used, false otherwise</param>
|
||||
public void ProcessPartition(BinaryReader reader, BinaryWriter writer, NCSDHeader header, bool encrypt, bool development)
|
||||
{
|
||||
// Check if the 'NoCrypto' bit is set
|
||||
if (Flags.PossblyDecrypted ^ encrypt)
|
||||
{
|
||||
Console.WriteLine($"Partition {PartitionNumber}: Already " + (encrypt ? "Encrypted" : "Decrypted") + "?...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the Keys to be used
|
||||
SetEncryptionKeys(header.BackupHeader.Flags, encrypt, development);
|
||||
|
||||
// Process each of the pieces if they exist
|
||||
ProcessExtendedHeader(reader, writer, header.MediaUnitSize, encrypt);
|
||||
ProcessExeFS(reader, writer, header.MediaUnitSize, encrypt);
|
||||
ProcessRomFS(reader, writer, header.MediaUnitSize, header.BackupHeader.Flags, encrypt, development);
|
||||
|
||||
// Write out new CryptoMethod and BitMask flags
|
||||
UpdateCryptoAndMasks(reader, writer, header, encrypt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the set of keys to be used for encryption or decryption
|
||||
/// </summary>
|
||||
/// <param name="backupFlags">File backup flags for encryption</param>
|
||||
/// <param name="encrypt">True if we're encrypting the file, false otherwise</param>
|
||||
/// <param name="development">True if development keys should be used, false otherwise</param>
|
||||
private void SetEncryptionKeys(NCCHHeaderFlags backupFlags, bool encrypt, bool development)
|
||||
{
|
||||
KeyX = 0;
|
||||
KeyX2C = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
|
||||
|
||||
// Backup headers can't have a KeyY value set
|
||||
if (RSA2048Signature != null)
|
||||
KeyY = new BigInteger(RSA2048Signature.Take(16).Reverse().ToArray());
|
||||
else
|
||||
KeyY = new BigInteger(0);
|
||||
|
||||
NormalKey = 0;
|
||||
NormalKey2C = Helper.RotateLeft((Helper.RotateLeft(KeyX2C, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
|
||||
|
||||
// Set the header to use based on mode
|
||||
BitMasks masks = 0;
|
||||
CryptoMethod method = 0;
|
||||
if (encrypt)
|
||||
{
|
||||
masks = backupFlags.BitMasks;
|
||||
method = backupFlags.CryptoMethod;
|
||||
}
|
||||
else
|
||||
{
|
||||
masks = Flags.BitMasks;
|
||||
method = Flags.CryptoMethod;
|
||||
}
|
||||
|
||||
if ((masks & BitMasks.FixedCryptoKey) != 0)
|
||||
{
|
||||
NormalKey = 0x00;
|
||||
NormalKey2C = 0x00;
|
||||
Console.WriteLine("Encryption Method: Zero Key");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (method == CryptoMethod.Original)
|
||||
{
|
||||
KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
|
||||
Console.WriteLine("Encryption Method: Key 0x2C");
|
||||
}
|
||||
else if (method == CryptoMethod.Seven)
|
||||
{
|
||||
KeyX = (development ? Constants.KeyX0x25 : Constants.KeyX0x25);
|
||||
Console.WriteLine("Encryption Method: Key 0x25");
|
||||
}
|
||||
else if (method == CryptoMethod.NineThree)
|
||||
{
|
||||
KeyX = (development ? Constants.DevKeyX0x18 : Constants.KeyX0x18);
|
||||
Console.WriteLine("Encryption Method: Key 0x18");
|
||||
}
|
||||
else if (method == CryptoMethod.NineSix)
|
||||
{
|
||||
KeyX = (development ? Constants.DevKeyX0x1B : Constants.KeyX0x1B);
|
||||
Console.WriteLine("Encryption Method: Key 0x1B");
|
||||
}
|
||||
|
||||
NormalKey = Helper.RotateLeft((Helper.RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the extended header, if it exists
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
/// <param name="mediaUnitSize">Number of bytes per media unit</param>
|
||||
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
|
||||
private bool ProcessExtendedHeader(BinaryReader reader, BinaryWriter writer, uint mediaUnitSize, bool encrypt)
|
||||
{
|
||||
if (ExtendedHeaderSizeInBytes > 0)
|
||||
{
|
||||
reader.BaseStream.Seek((Entry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek((Entry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin);
|
||||
|
||||
Console.WriteLine($"Partition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + ": ExHeader");
|
||||
|
||||
var cipher = Helper.CreateAESCipher(NormalKey2C, PlainIV, encrypt);
|
||||
writer.Write(cipher.ProcessBytes(reader.ReadBytes(Constants.CXTExtendedDataHeaderLength)));
|
||||
writer.Flush();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Partition {PartitionNumber} ExeFS: No Extended Header... Skipping...");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the ExeFS, if it exists
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
/// <param name="mediaUnitSize">Number of bytes per media unit</param>
|
||||
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
|
||||
private void ProcessExeFS(BinaryReader reader, BinaryWriter writer, uint mediaUnitSize, bool encrypt)
|
||||
{
|
||||
if (ExeFSSizeInMediaUnits > 0)
|
||||
{
|
||||
// If we're decrypting, we need to decrypt the filename table first
|
||||
if (!encrypt)
|
||||
ProcessExeFSFilenameTable(reader, writer, mediaUnitSize, encrypt);
|
||||
|
||||
// For all but the original crypto method, process each of the files in the table
|
||||
if (Flags.CryptoMethod != CryptoMethod.Original)
|
||||
{
|
||||
reader.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
|
||||
ExeFSHeader exefsHeader = ExeFSHeader.Read(reader);
|
||||
if (exefsHeader != null)
|
||||
{
|
||||
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 = Helper.AddToByteArray(ExeFSIV, (int)ctroffset);
|
||||
|
||||
var firstCipher = Helper.CreateAESCipher(NormalKey, exefsIVWithOffsetForHeader, encrypt);
|
||||
var secondCipher = Helper.CreateAESCipher(NormalKey2C, exefsIVWithOffsetForHeader, !encrypt);
|
||||
|
||||
reader.BaseStream.Seek((((Entry.Offset + ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek((((Entry.Offset + 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 {PartitionNumber} ExeFS: " + (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 {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {datalenM + 1} / {datalenM + 1} mb... Done!\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're encrypting, we need to encrypt the filename table now
|
||||
if (encrypt)
|
||||
ProcessExeFSFilenameTable(reader, writer, mediaUnitSize, encrypt);
|
||||
|
||||
// Process the ExeFS
|
||||
int exefsSizeM = (int)((ExeFSSizeInMediaUnits - 1) * mediaUnitSize) / (1024 * 1024);
|
||||
int exefsSizeB = (int)((ExeFSSizeInMediaUnits - 1) * mediaUnitSize) % (1024 * 1024);
|
||||
int ctroffsetE = (int)(mediaUnitSize / 0x10);
|
||||
|
||||
byte[] exefsIVWithOffset = Helper.AddToByteArray(ExeFSIV, ctroffsetE);
|
||||
|
||||
var exeFS = Helper.CreateAESCipher(NormalKey2C, exefsIVWithOffset, encrypt);
|
||||
|
||||
reader.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits + 1) * mediaUnitSize, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek((Entry.Offset + 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 {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {i} / {exefsSizeM + 1} mb");
|
||||
}
|
||||
}
|
||||
if (exefsSizeB > 0)
|
||||
{
|
||||
writer.Write(exeFS.DoFinal(reader.ReadBytes(exefsSizeB)));
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
Console.Write($"\rPartition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {exefsSizeM + 1} / {exefsSizeM + 1} mb... Done!\r\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Partition {PartitionNumber} ExeFS: No Data... Skipping...");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the ExeFS Filename Table
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
/// <param name="mediaUnitSize">Number of bytes per media unit</param>
|
||||
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
|
||||
private void ProcessExeFSFilenameTable(BinaryReader reader, BinaryWriter writer, uint mediaUnitSize, bool encrypt)
|
||||
{
|
||||
reader.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
|
||||
|
||||
Console.WriteLine($"Partition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table");
|
||||
|
||||
var exeFSFilenameTable = Helper.CreateAESCipher(NormalKey2C, ExeFSIV, encrypt);
|
||||
writer.Write(exeFSFilenameTable.ProcessBytes(reader.ReadBytes((int)mediaUnitSize)));
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the RomFS, if it exists
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
/// <param name="mediaUnitSize">Number of bytes per media unit</param>
|
||||
/// <param name="backupFlags">File backup flags for encryption</param>
|
||||
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
|
||||
/// <param name="development">True if development keys should be used, false otherwise</param>
|
||||
private void ProcessRomFS(BinaryReader reader, BinaryWriter writer, uint mediaUnitSize, NCCHHeaderFlags backupFlags, bool encrypt, bool development)
|
||||
{
|
||||
if (RomFSOffsetInMediaUnits != 0)
|
||||
{
|
||||
int romfsSizeM = (int)(RomFSSizeInMediaUnits * mediaUnitSize) / (1024 * 1024);
|
||||
int romfsSizeB = (int)(RomFSSizeInMediaUnits * mediaUnitSize) % (1024 * 1024);
|
||||
|
||||
// Encrypting RomFS for partitions 1 and up always use Key0x2C
|
||||
if (encrypt && PartitionNumber > 0)
|
||||
{
|
||||
// If the backup flags aren't provided and we're encrypting, assume defaults
|
||||
if (backupFlags == null)
|
||||
{
|
||||
KeyX = KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
|
||||
NormalKey = Helper.RotateLeft((Helper.RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
|
||||
}
|
||||
|
||||
if ((backupFlags.BitMasks & BitMasks.FixedCryptoKey) != 0) // except if using zero-key
|
||||
{
|
||||
NormalKey = 0x00;
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyX = KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
|
||||
NormalKey = Helper.RotateLeft((Helper.RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
|
||||
}
|
||||
}
|
||||
|
||||
var cipher = Helper.CreateAESCipher(NormalKey, RomFSIV, encrypt);
|
||||
|
||||
reader.BaseStream.Seek((Entry.Offset + RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek((Entry.Offset + 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 {PartitionNumber} RomFS: Decrypting: {i} / {romfsSizeM + 1} mb");
|
||||
}
|
||||
}
|
||||
if (romfsSizeB > 0)
|
||||
{
|
||||
writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB)));
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
Console.Write($"\rPartition {PartitionNumber} RomFS: Decrypting: {romfsSizeM + 1} / {romfsSizeM + 1} mb... Done!\r\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Partition {PartitionNumber} RomFS: No Data... Skipping...");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the CryptoMethod and BitMasks for the partition
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
/// <param name="header">NCSD header for the 3DS file</param>
|
||||
/// <param name="encrypt">True if we're writing encrypted values, false otherwise</param>
|
||||
private void UpdateCryptoAndMasks(BinaryReader reader, BinaryWriter writer, NCSDHeader header, bool encrypt)
|
||||
{
|
||||
// Write the new CryptoMethod
|
||||
writer.BaseStream.Seek((Entry.Offset * header.MediaUnitSize) + 0x18B, SeekOrigin.Begin);
|
||||
if (encrypt)
|
||||
{
|
||||
// For partitions 1 and up, set crypto-method to 0x00
|
||||
if (PartitionNumber > 0)
|
||||
writer.Write((byte)CryptoMethod.Original);
|
||||
|
||||
// If partition 0, restore crypto-method from backup flags
|
||||
else
|
||||
writer.Write((byte)header.BackupHeader.Flags.CryptoMethod);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.Write((byte)CryptoMethod.Original);
|
||||
}
|
||||
writer.Flush();
|
||||
|
||||
// Write the new BitMasks flag
|
||||
writer.BaseStream.Seek((Entry.Offset * header.MediaUnitSize) + 0x18F, SeekOrigin.Begin);
|
||||
BitMasks flag = Flags.BitMasks;
|
||||
if (encrypt)
|
||||
{
|
||||
flag = (flag & ((BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF));
|
||||
flag = (flag | (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & header.BackupHeader.Flags.BitMasks);
|
||||
}
|
||||
else
|
||||
{
|
||||
flag = flag & (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
|
||||
flag = (flag | BitMasks.NoCrypto);
|
||||
}
|
||||
|
||||
writer.Write((byte)flag);
|
||||
writer.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt
|
||||
{
|
||||
class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
DisplayHelp("Not enough arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
bool? encrypt = null;
|
||||
if (args[0] == "decrypt")
|
||||
{
|
||||
encrypt = false;
|
||||
}
|
||||
else if (args[0] == "encrypt")
|
||||
{
|
||||
encrypt = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayHelp($"Invalid operation: {args[0]}");
|
||||
return;
|
||||
}
|
||||
|
||||
bool development = false;
|
||||
int start = 1;
|
||||
if (args[1] == "-dev")
|
||||
{
|
||||
development = true;
|
||||
start = 2;
|
||||
}
|
||||
|
||||
for (int i = start; i < args.Length; i++)
|
||||
{
|
||||
if (File.Exists(args[i]))
|
||||
{
|
||||
ITool tool = DeriveTool(args[i], encrypt.Value, development);
|
||||
if (tool?.ProcessFile() != true)
|
||||
Console.WriteLine("Processing failed!");
|
||||
}
|
||||
else if (Directory.Exists(args[i]))
|
||||
{
|
||||
foreach (string file in Directory.EnumerateFiles(args[i], "*", SearchOption.AllDirectories))
|
||||
{
|
||||
ITool tool = DeriveTool(file, encrypt.Value, development);
|
||||
if (tool?.ProcessFile() != true)
|
||||
Console.WriteLine("Processing failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Press Enter to Exit...");
|
||||
Console.Read();
|
||||
}
|
||||
|
||||
private static void DisplayHelp(string err = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(err))
|
||||
Console.WriteLine($"Error: {err}");
|
||||
|
||||
Console.WriteLine("Usage: 3dsdecrypt.exe (decrypt|encrypt) [-dev] <file|dir> ...");
|
||||
}
|
||||
|
||||
private enum RomType
|
||||
{
|
||||
NULL,
|
||||
NDS,
|
||||
NDSi,
|
||||
N3DS,
|
||||
}
|
||||
|
||||
private static RomType DetermineRomType(string filename)
|
||||
{
|
||||
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase))
|
||||
return RomType.NDS;
|
||||
|
||||
else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
|
||||
return RomType.NDSi;
|
||||
|
||||
else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase))
|
||||
return RomType.N3DS;
|
||||
|
||||
return RomType.NULL;
|
||||
}
|
||||
|
||||
private static ITool DeriveTool(string filename, bool encrypt, bool development)
|
||||
{
|
||||
RomType type = DetermineRomType(filename);
|
||||
switch(type)
|
||||
{
|
||||
case RomType.NDS:
|
||||
case RomType.NDSi:
|
||||
return new DSTool(filename, encrypt);
|
||||
case RomType.N3DS:
|
||||
return new ThreeDSTool(filename, development, encrypt);
|
||||
case RomType.NULL:
|
||||
default:
|
||||
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.dsi, *.3ds");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("3DSDecrypt")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("3DSDecrypt")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2018")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("2e30006a-3c60-4576-a262-937b21c83c06")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -1,59 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using NDecrypt.Headers;
|
||||
|
||||
namespace NDecrypt
|
||||
{
|
||||
public class ThreeDSTool : ITool
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the input 3DS file
|
||||
/// </summary>
|
||||
private readonly string filename;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to detrmine if development keys should be used
|
||||
/// </summary>
|
||||
private readonly bool development;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to determine if encrypting or decrypting
|
||||
/// </summary>
|
||||
private readonly bool encrypt;
|
||||
|
||||
public ThreeDSTool(string filename, bool development, bool encrypt)
|
||||
{
|
||||
this.filename = filename;
|
||||
this.development = development;
|
||||
this.encrypt = encrypt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process an input file given the input values
|
||||
/// </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)))
|
||||
{
|
||||
NCSDHeader header = NCSDHeader.Read(reader, development);
|
||||
if (header == null)
|
||||
{
|
||||
Console.WriteLine("Error: Not a 3DS Rom!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process all 8 NCCH partitions
|
||||
header.ProcessAllPartitions(reader, writer, encrypt, development);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="BouncyCastle" version="1.8.4" targetFramework="net461" />
|
||||
</packages>
|
||||
135
NDecrypt.CMD/HashingHelper.cs
Normal file
135
NDecrypt.CMD/HashingHelper.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Compress.ThreadReaders;
|
||||
using NDecrypt.Tools;
|
||||
|
||||
namespace NDecrypt.CMD
|
||||
{
|
||||
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
|
||||
var loadBuffer = new ThreadLoadBuffer(inputStream);
|
||||
int buffersize = 3 * 1024 * 1024;
|
||||
byte[] buffer0 = new byte[buffersize];
|
||||
byte[] buffer1 = 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 first buffer
|
||||
long refsize = size;
|
||||
int next = refsize > buffersize ? buffersize : (int)refsize;
|
||||
inputStream.Read(buffer0, 0, next);
|
||||
int current = next;
|
||||
refsize -= next;
|
||||
bool bufferSelect = true;
|
||||
|
||||
while (current > 0)
|
||||
{
|
||||
// Trigger the buffer load on the second buffer
|
||||
next = refsize > buffersize ? buffersize : (int)refsize;
|
||||
if (next > 0)
|
||||
loadBuffer.Trigger(bufferSelect ? buffer1 : buffer0, next);
|
||||
|
||||
byte[] buffer = bufferSelect ? buffer0 : buffer1;
|
||||
|
||||
// Run hashes in parallel
|
||||
Parallel.ForEach(hashers, h => h.Process(buffer, current));
|
||||
|
||||
// Wait for the load buffer worker, if needed
|
||||
if (next > 0)
|
||||
loadBuffer.Wait();
|
||||
|
||||
// Setup for the next hashing step
|
||||
current = next;
|
||||
refsize -= next;
|
||||
bufferSelect = !bufferSelect;
|
||||
}
|
||||
|
||||
// Finalize all hashing helpers
|
||||
loadBuffer.Finish();
|
||||
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
|
||||
loadBuffer.Dispose();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
NDecrypt.CMD/NDecrypt.CMD.csproj
Normal file
12
NDecrypt.CMD/NDecrypt.CMD.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net48;netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NDecrypt\NDecrypt.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
253
NDecrypt.CMD/Program.cs
Normal file
253
NDecrypt.CMD/Program.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using NDecrypt.N3DS;
|
||||
using NDecrypt.Nitro;
|
||||
|
||||
namespace NDecrypt.CMD
|
||||
{
|
||||
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)
|
||||
{
|
||||
DisplayHelp("Not enough arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
var decryptArgs = new DecryptArgs();
|
||||
if (args[0] == "decrypt" || args[0] == "d")
|
||||
{
|
||||
decryptArgs.Encrypt = false;
|
||||
}
|
||||
else if (args[0] == "encrypt" || args[0] == "e")
|
||||
{
|
||||
decryptArgs.Encrypt = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayHelp($"Invalid operation: {args[0]}");
|
||||
return;
|
||||
}
|
||||
|
||||
bool outputHashes = false;
|
||||
int start = 1;
|
||||
for ( ; start < args.Length; start++)
|
||||
{
|
||||
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")
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Derive the keyfile path based on the runtime folder if not already set
|
||||
if (string.IsNullOrWhiteSpace(decryptArgs.KeyFile))
|
||||
{
|
||||
if (decryptArgs.UseCitraKeyFile)
|
||||
decryptArgs.KeyFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "aes_keys.txt");
|
||||
else
|
||||
decryptArgs.KeyFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "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]))
|
||||
{
|
||||
ProcessPath(args[i], decryptArgs, outputHashes);
|
||||
}
|
||||
else if (Directory.Exists(args[i]))
|
||||
{
|
||||
foreach (string file in Directory.EnumerateFiles(args[i], "*", SearchOption.AllDirectories))
|
||||
{
|
||||
ProcessPath(file, decryptArgs, outputHashes);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"{args[i]} is not a file or folder. Please check your spelling and formatting and try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Display a basic help text
|
||||
/// </summary>
|
||||
/// <param name="err">Additional error text to display, can be null to ignore</param>
|
||||
private static void DisplayHelp(string err = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(err))
|
||||
Console.WriteLine($"Error: {err}");
|
||||
|
||||
Console.WriteLine(@"Usage: NDecrypt.exe <opeation> [flags] <path> ...
|
||||
|
||||
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="decryptArgs">Arguments to pass to the tools on creation</param>
|
||||
/// <returns></returns>
|
||||
private static ITool DeriveTool(string filename, DecryptArgs decryptArgs)
|
||||
{
|
||||
FileType type = DetermineFileType(filename);
|
||||
switch(type)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the file type from the filename extension
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename to derive the type from</param>
|
||||
/// <returns>FileType value, if possible</returns>
|
||||
private static FileType DetermineFileType(string filename)
|
||||
{
|
||||
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase) // Standard carts
|
||||
|| filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase)) // Development carts/images
|
||||
return FileType.NDS;
|
||||
|
||||
else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
|
||||
return FileType.NDSi;
|
||||
|
||||
else if (filename.EndsWith(".ids", StringComparison.OrdinalIgnoreCase))
|
||||
return FileType.iQueDS;
|
||||
|
||||
else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase))
|
||||
return FileType.N3DS;
|
||||
|
||||
else if (filename.EndsWith(".cia", StringComparison.OrdinalIgnoreCase))
|
||||
return FileType.N3DSCIA;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
NDecrypt.sln
Normal file
36
NDecrypt.sln
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.28803.156
|
||||
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}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NDecrypt.CMD", "NDecrypt.CMD\NDecrypt.CMD.csproj", "{05566793-831F-4AE1-A6D2-F9214F36618E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{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
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {5AB50D9B-BA18-4F96-804B-52E7E0845B37}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
293
NDecrypt/DecryptArgs.cs
Normal file
293
NDecrypt/DecryptArgs.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using NDecrypt.Tools;
|
||||
|
||||
namespace NDecrypt
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using Org.BouncyCastle.Security;
|
||||
|
||||
namespace NDecrypt
|
||||
{
|
||||
public static class Helper
|
||||
internal static class Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an integer value to a number represented by a byte array
|
||||
7
NDecrypt/ITool.cs
Normal file
7
NDecrypt/ITool.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NDecrypt
|
||||
{
|
||||
public interface ITool
|
||||
{
|
||||
bool ProcessFile();
|
||||
}
|
||||
}
|
||||
575
NDecrypt/N3DS/CIATool.cs
Normal file
575
NDecrypt/N3DS/CIATool.cs
Normal file
@@ -0,0 +1,575 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using NDecrypt.N3DS.Headers;
|
||||
using static NDecrypt.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
|
||||
}
|
||||
}
|
||||
12
NDecrypt/N3DS/Constants.cs
Normal file
12
NDecrypt/N3DS/Constants.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace NDecrypt.Data
|
||||
namespace NDecrypt.N3DS
|
||||
{
|
||||
[Flags]
|
||||
public enum ARM9AccessControlDescriptors : byte
|
||||
internal enum ARM9AccessControlDescriptors : byte
|
||||
{
|
||||
MountNandRoot = 0x01,
|
||||
MountNandroWriteAccess = 0x02,
|
||||
@@ -18,7 +18,7 @@ namespace NDecrypt.Data
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ARM11LSCFlag0 : byte
|
||||
internal enum ARM11LSCFlag0 : byte
|
||||
{
|
||||
IdealProcessor = 0x01 | 0x02,
|
||||
AffinityMask = 0x04 | 0x08,
|
||||
@@ -37,14 +37,14 @@ namespace NDecrypt.Data
|
||||
}
|
||||
|
||||
[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.Data
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum BitMasks : byte
|
||||
internal enum BitMasks : byte
|
||||
{
|
||||
FixedCryptoKey = 0x01,
|
||||
NoMountRomFs = 0x02,
|
||||
@@ -66,14 +66,32 @@ namespace NDecrypt.Data
|
||||
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.Data
|
||||
Trial = 0x10,
|
||||
}
|
||||
|
||||
public enum CryptoMethod : byte
|
||||
internal enum CryptoMethod : byte
|
||||
{
|
||||
Original = 0x00,
|
||||
Seven = 0x01,
|
||||
@@ -92,7 +110,7 @@ namespace NDecrypt.Data
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum FilesystemAccessInfo : ulong
|
||||
internal enum FilesystemAccessInfo : ulong
|
||||
{
|
||||
CategorySystemApplication = 0x1,
|
||||
CategoryHardwareCheck = 0x2,
|
||||
@@ -118,7 +136,7 @@ namespace NDecrypt.Data
|
||||
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.Data
|
||||
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.Data
|
||||
ExtendedDevice = 0x03,
|
||||
}
|
||||
|
||||
public enum NCCHFlags
|
||||
internal enum NCCHFlags
|
||||
{
|
||||
CryptoMethod = 0x03,
|
||||
ContentPlatform = 0x04,
|
||||
@@ -155,7 +173,7 @@ namespace NDecrypt.Data
|
||||
BitMasks = 0x07,
|
||||
}
|
||||
|
||||
public enum NCSDFlags
|
||||
internal enum NCSDFlags
|
||||
{
|
||||
BackupWriteWaitTime = 0x00,
|
||||
MediaCardDevice3X = 0x03,
|
||||
@@ -165,14 +183,14 @@ namespace NDecrypt.Data
|
||||
MediaCardDevice2X = 0x07,
|
||||
}
|
||||
|
||||
public enum NDSUnitcode : byte
|
||||
internal enum PublicKeyType : uint
|
||||
{
|
||||
NDS = 0x00,
|
||||
NDSPlusDSi = 0x02,
|
||||
DSi = 0x03,
|
||||
RSA_4096 = 0x00000000,
|
||||
RSA_2048 = 0x01000000,
|
||||
ECDSA = 0x02000000,
|
||||
}
|
||||
|
||||
public enum ResourceLimitCategory
|
||||
internal enum ResourceLimitCategory
|
||||
{
|
||||
APPLICATION = 0,
|
||||
SYS_APPLET = 1,
|
||||
@@ -180,10 +198,31 @@ namespace NDecrypt.Data
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class ARM11KernelCapabilities
|
||||
internal class ARM11KernelCapabilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Descriptors
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.IO;
|
||||
using NDecrypt.Data;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class ARM11LocalSystemCapabilities
|
||||
internal class ARM11LocalSystemCapabilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Program ID
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.IO;
|
||||
using NDecrypt.Data;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class ARM9AccessControl
|
||||
internal class ARM9AccessControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Descriptors
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class AccessControlInfo
|
||||
internal class AccessControlInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// ARM11 local system capabilities
|
||||
149
NDecrypt/N3DS/Headers/CIAHeader.cs
Normal file
149
NDecrypt/N3DS/Headers/CIAHeader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class CXIExtendedHeader
|
||||
internal class CXIExtendedHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// SCI
|
||||
156
NDecrypt/N3DS/Headers/Certificate.cs
Normal file
156
NDecrypt/N3DS/Headers/Certificate.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class CodeSetInfo
|
||||
internal class CodeSetInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Address
|
||||
59
NDecrypt/N3DS/Headers/ContentChunkRecord.cs
Normal file
59
NDecrypt/N3DS/Headers/ContentChunkRecord.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
NDecrypt/N3DS/Headers/ContentInfoRecord.cs
Normal file
44
NDecrypt/N3DS/Headers/ContentInfoRecord.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class ExeFSFileHeader
|
||||
internal class ExeFSFileHeader
|
||||
{
|
||||
private const string codeSegment = ".code\0\0\0";
|
||||
// .code\0\0\0
|
||||
private readonly byte[] codeSegmentBytes = new byte[] { 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x00, 0x00, 0x00 };
|
||||
|
||||
/// <summary>
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class ExeFSHeader
|
||||
internal class ExeFSHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// File headers (10 headers maximum, 16 bytes each)
|
||||
57
NDecrypt/N3DS/Headers/MetaFile.cs
Normal file
57
NDecrypt/N3DS/Headers/MetaFile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
248
NDecrypt/N3DS/Headers/NCCHHeader.cs
Normal file
248
NDecrypt/N3DS/Headers/NCCHHeader.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
internal class NCCHHeader
|
||||
{
|
||||
private const string NCCHMagicNumber = "NCCH";
|
||||
|
||||
/// <summary>
|
||||
/// Partition number for the current partition
|
||||
/// </summary>
|
||||
public int PartitionNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Partition table entry for the current partition
|
||||
/// </summary>
|
||||
public PartitionTableEntry Entry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// RSA-2048 signature of the NCCH header, using SHA-256.
|
||||
/// </summary>
|
||||
public byte[] RSA2048Signature { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Content size, in media units (1 media unit = 0x200 bytes)
|
||||
/// </summary>
|
||||
public uint ContentSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Partition ID
|
||||
/// </summary>
|
||||
public byte[] PartitionId { get; private set; }
|
||||
public byte[] PlainIV { get { return PartitionId.Concat(Constants.PlainCounter).ToArray(); } }
|
||||
public byte[] ExeFSIV { get { return PartitionId.Concat(Constants.ExefsCounter).ToArray(); } }
|
||||
public byte[] RomFSIV { get { return PartitionId.Concat(Constants.RomfsCounter).ToArray(); } }
|
||||
|
||||
/// <summary>
|
||||
/// Boot rom key
|
||||
/// </summary>
|
||||
public BigInteger KeyX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// NCCH boot rom key
|
||||
/// </summary>
|
||||
public BigInteger KeyX2C { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Kernel9/Process9 key
|
||||
/// </summary>
|
||||
public BigInteger KeyY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normal AES key
|
||||
/// </summary>
|
||||
public BigInteger NormalKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// NCCH AES key
|
||||
/// </summary>
|
||||
public BigInteger NormalKey2C { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maker code
|
||||
/// </summary>
|
||||
public ushort MakerCode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version
|
||||
/// </summary>
|
||||
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
|
||||
/// SHA256 hash. The data used for that hash is 0x18-bytes: [0x10-long title-unique content lock seed]
|
||||
/// [programID from NCCH + 0x118]. This hash is only used for verification of the content lock seed, and
|
||||
/// is not the actual keyY.
|
||||
/// </summary>
|
||||
public uint VerificationHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Program ID
|
||||
/// </summary>
|
||||
public byte[] ProgramId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
/// </summary>
|
||||
public byte[] Reserved1 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo Region SHA-256 hash. (For applications built with SDK 5+) (Supported from firmware: 5.0.0-11)
|
||||
/// </summary>
|
||||
public byte[] LogoRegionHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Product code
|
||||
/// </summary>
|
||||
public byte[] ProductCode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Extended header SHA-256 hash (SHA256 of 2x Alignment Size, beginning at 0x0 of ExHeader)
|
||||
/// </summary>
|
||||
public byte[] ExtendedHeaderHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Extended header size, in bytes
|
||||
/// </summary>
|
||||
public uint ExtendedHeaderSizeInBytes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
/// </summary>
|
||||
public byte[] Reserved2 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flags
|
||||
/// </summary>
|
||||
public NCCHHeaderFlags Flags { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plain region offset, in media units
|
||||
/// </summary>
|
||||
public uint PlainRegionOffsetInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plain region size, in media units
|
||||
/// </summary>
|
||||
public uint PlainRegionSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo Region offset, in media units (For applications built with SDK 5+) (Supported from firmware: 5.0.0-11)
|
||||
/// </summary>
|
||||
public uint LogoRegionOffsetInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo Region size, in media units (For applications built with SDK 5+) (Supported from firmware: 5.0.0-11)
|
||||
/// </summary>
|
||||
public uint LogoRegionSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ExeFS offset, in media units
|
||||
/// </summary>
|
||||
public uint ExeFSOffsetInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ExeFS size, in media units
|
||||
/// </summary>
|
||||
public uint ExeFSSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ExeFS hash region size, in media units
|
||||
/// </summary>
|
||||
public uint ExeFSHashRegionSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
/// </summary>
|
||||
public byte[] Reserved3 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// RomFS offset, in media units
|
||||
/// </summary>
|
||||
public uint RomFSOffsetInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// RomFS size, in media units
|
||||
/// </summary>
|
||||
public uint RomFSSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// RomFS hash region size, in media units
|
||||
/// </summary>
|
||||
public uint RomFSHashRegionSizeInMediaUnits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
/// </summary>
|
||||
public byte[] Reserved4 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ExeFS superblock SHA-256 hash - (SHA-256 hash, starting at 0x0 of the ExeFS over the number of
|
||||
/// media units specified in the ExeFS hash region size)
|
||||
/// </summary>
|
||||
public byte[] ExeFSSuperblockHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// RomFS superblock SHA-256 hash - (SHA-256 hash, starting at 0x0 of the RomFS over the number
|
||||
/// of media units specified in the RomFS hash region size)
|
||||
/// </summary>
|
||||
public byte[] RomFSSuperblockHash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Read from a stream and get an NCCH header, if possible
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="readSignature">True if the RSA signature is read, false otherwise</param>
|
||||
/// <returns>NCCH header object, null on error</returns>
|
||||
public static NCCHHeader Read(BinaryReader reader, bool readSignature)
|
||||
{
|
||||
NCCHHeader header = new NCCHHeader();
|
||||
|
||||
try
|
||||
{
|
||||
if (readSignature)
|
||||
header.RSA2048Signature = reader.ReadBytes(0x100);
|
||||
|
||||
if (new string(reader.ReadChars(4)) != NCCHMagicNumber)
|
||||
return null;
|
||||
|
||||
header.ContentSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.PartitionId = reader.ReadBytes(8).Reverse().ToArray();
|
||||
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);
|
||||
header.ProductCode = reader.ReadBytes(0x10);
|
||||
header.ExtendedHeaderHash = reader.ReadBytes(0x20);
|
||||
header.ExtendedHeaderSizeInBytes = reader.ReadUInt32();
|
||||
header.Reserved2 = reader.ReadBytes(4);
|
||||
header.Flags = NCCHHeaderFlags.Read(reader);
|
||||
header.PlainRegionOffsetInMediaUnits = reader.ReadUInt32();
|
||||
header.PlainRegionSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.LogoRegionOffsetInMediaUnits = reader.ReadUInt32();
|
||||
header.LogoRegionSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.ExeFSOffsetInMediaUnits = reader.ReadUInt32();
|
||||
header.ExeFSSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.ExeFSHashRegionSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.Reserved3 = reader.ReadBytes(4);
|
||||
header.RomFSOffsetInMediaUnits = reader.ReadUInt32();
|
||||
header.RomFSSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.RomFSHashRegionSizeInMediaUnits = reader.ReadUInt32();
|
||||
header.Reserved4 = reader.ReadBytes(4);
|
||||
header.ExeFSSuperblockHash = reader.ReadBytes(0x20);
|
||||
header.RomFSSuperblockHash = reader.ReadBytes(0x20);
|
||||
|
||||
return header;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using NDecrypt.Data;
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class NCCHHeaderFlags
|
||||
internal class NCCHHeaderFlags
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserved
|
||||
@@ -52,7 +50,7 @@ namespace NDecrypt.Headers
|
||||
/// <summary>
|
||||
/// Get if the NoCrypto bit is set
|
||||
/// </summary>
|
||||
public bool PossblyDecrypted { get { return (BitMasks & BitMasks.NoCrypto) != 0; } }
|
||||
public bool PossblyDecrypted { get { return BitMasks.HasFlag(BitMasks.NoCrypto); } }
|
||||
|
||||
/// <summary>
|
||||
/// Read from a stream and get an NCCH header flags, if possible
|
||||
@@ -1,10 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using NDecrypt.Data;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class NCSDHeader
|
||||
internal class NCSDHeader
|
||||
{
|
||||
private const string NCSDMagicNumber = "NCSD";
|
||||
|
||||
@@ -300,7 +299,7 @@ namespace NDecrypt.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)
|
||||
{
|
||||
@@ -322,54 +321,5 @@ namespace NDecrypt.Headers
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process all partitions in the partition table
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
/// <param name="encrypt">True if we want to encrypt the partitions, false otherwise</param>
|
||||
/// <param name="development">True if development keys should be used, false otherwise</param>
|
||||
public void ProcessAllPartitions(BinaryReader reader, BinaryWriter writer, bool encrypt, bool development)
|
||||
{
|
||||
// Iterate over all 8 NCCH partitions
|
||||
for (int p = 0; p < 8; p++)
|
||||
{
|
||||
NCCHHeader partitionHeader = GetPartitionHeader(reader, p);
|
||||
if (partitionHeader == null)
|
||||
continue;
|
||||
|
||||
partitionHeader.ProcessPartition(reader, writer, this, encrypt, development);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific partition header from the partition table
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="partitionNumber">Partition number to attempt to retrieve</param>
|
||||
/// <returns>NCCH header for the partition requested, null on error</returns>
|
||||
public NCCHHeader GetPartitionHeader(BinaryReader reader, int partitionNumber)
|
||||
{
|
||||
if (!PartitionsTable[partitionNumber].IsValid())
|
||||
{
|
||||
Console.WriteLine($"Partition {partitionNumber} Not found... Skipping...");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Seek to the beginning of the NCCH partition
|
||||
reader.BaseStream.Seek((PartitionsTable[partitionNumber].Offset * MediaUnitSize), SeekOrigin.Begin);
|
||||
|
||||
NCCHHeader partitionHeader = NCCHHeader.Read(reader, true);
|
||||
if (partitionHeader == null)
|
||||
{
|
||||
Console.WriteLine($"Partition {partitionNumber} Unable to read NCCH header");
|
||||
return null;
|
||||
}
|
||||
|
||||
partitionHeader.PartitionNumber = partitionNumber;
|
||||
partitionHeader.Entry = PartitionsTable[partitionNumber];
|
||||
return partitionHeader;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class PartitionTableEntry
|
||||
internal class PartitionTableEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Offset
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
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;
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.IO;
|
||||
using NDecrypt.Data;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class StorageInfo
|
||||
internal class StorageInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Extdata ID
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class SystemControlInfo
|
||||
internal class SystemControlInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Application title (default is "CtrApp")
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.N3DS.Headers
|
||||
{
|
||||
public class SystemInfo
|
||||
internal class SystemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// SaveData Size
|
||||
279
NDecrypt/N3DS/Headers/Ticket.cs
Normal file
279
NDecrypt/N3DS/Headers/Ticket.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
NDecrypt/N3DS/Headers/TitleMetadata.cs
Normal file
241
NDecrypt/N3DS/Headers/TitleMetadata.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
583
NDecrypt/N3DS/ThreeDSTool.cs
Normal file
583
NDecrypt/N3DS/ThreeDSTool.cs
Normal file
@@ -0,0 +1,583 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using NDecrypt.N3DS.Headers;
|
||||
using static NDecrypt.Helper;
|
||||
|
||||
namespace NDecrypt.N3DS
|
||||
{
|
||||
public class ThreeDSTool : ITool
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the input 3DS file
|
||||
/// </summary>
|
||||
private readonly string filename;
|
||||
|
||||
/// <summary>
|
||||
/// Decryption args to use while processing
|
||||
/// </summary>
|
||||
private readonly DecryptArgs decryptArgs;
|
||||
|
||||
public ThreeDSTool(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)))
|
||||
{
|
||||
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>
|
||||
/// Process all partitions in the partition table of an NCSD header
|
||||
/// </summary>
|
||||
/// <param name="ncsdHeader">NCSD header representing the 3DS file</param>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
private void ProcessAllPartitions(NCSDHeader ncsdHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
// Iterate over all 8 NCCH partitions
|
||||
for (int p = 0; p < 8; p++)
|
||||
{
|
||||
NCCHHeader ncchHeader = GetPartitionHeader(ncsdHeader, reader, p);
|
||||
if (ncchHeader == null)
|
||||
continue;
|
||||
|
||||
ProcessPartition(ncsdHeader, ncchHeader, reader, writer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific partition header from the partition table
|
||||
/// </summary>
|
||||
/// <param name="ncsdHeader">NCSD header representing the 3DS file</param>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="partitionNumber">Partition number to attempt to retrieve</param>
|
||||
/// <returns>NCCH header for the partition requested, null on error</returns>
|
||||
private NCCHHeader GetPartitionHeader(NCSDHeader ncsdHeader, BinaryReader reader, int partitionNumber)
|
||||
{
|
||||
if (!ncsdHeader.PartitionsTable[partitionNumber].IsValid())
|
||||
{
|
||||
Console.WriteLine($"Partition {partitionNumber} Not found... Skipping...");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Seek to the beginning of the NCCH partition
|
||||
reader.BaseStream.Seek((ncsdHeader.PartitionsTable[partitionNumber].Offset * ncsdHeader.MediaUnitSize), SeekOrigin.Begin);
|
||||
|
||||
NCCHHeader partitionHeader = NCCHHeader.Read(reader, readSignature: true);
|
||||
if (partitionHeader == null)
|
||||
{
|
||||
Console.WriteLine($"Partition {partitionNumber} Unable to read NCCH header");
|
||||
return null;
|
||||
}
|
||||
|
||||
partitionHeader.PartitionNumber = partitionNumber;
|
||||
partitionHeader.Entry = ncsdHeader.PartitionsTable[partitionNumber];
|
||||
return partitionHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a single partition
|
||||
/// </summary>
|
||||
/// <param name="ncsdHeader">NCSD header representing the 3DS 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(NCSDHeader ncsdHeader, 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(ncsdHeader, ncchHeader);
|
||||
|
||||
// Process the extended header
|
||||
ProcessExtendedHeader(ncsdHeader, ncchHeader, reader, writer);
|
||||
|
||||
// If we're encrypting, encrypt the filesystems and update the flags
|
||||
if (decryptArgs.Encrypt)
|
||||
{
|
||||
EncryptExeFS(ncsdHeader, ncchHeader, reader, writer);
|
||||
EncryptRomFS(ncsdHeader, ncchHeader, reader, writer);
|
||||
UpdateEncryptCryptoAndMasks(ncsdHeader, ncchHeader, writer);
|
||||
}
|
||||
|
||||
// If we're decrypting, decrypt the filesystems and update the flags
|
||||
else
|
||||
{
|
||||
DecryptExeFS(ncsdHeader, ncchHeader, reader, writer);
|
||||
DecryptRomFS(ncsdHeader, ncchHeader, reader, writer);
|
||||
UpdateDecryptCryptoAndMasks(ncsdHeader, ncchHeader, writer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the set of keys to be used for encryption or decryption
|
||||
/// </summary>
|
||||
/// <param name="ncsdHeader">NCSD header representing the 3DS file</param>
|
||||
/// <param name="ncchHeader">NCCH header representing the partition</param>
|
||||
private void SetEncryptionKeys(NCSDHeader ncsdHeader, 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);
|
||||
|
||||
// Set the header to use based on mode
|
||||
BitMasks masks;
|
||||
CryptoMethod method;
|
||||
if (decryptArgs.Encrypt)
|
||||
{
|
||||
masks = ncsdHeader.BackupHeader.Flags.BitMasks;
|
||||
method = ncsdHeader.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="ncsdHeader">NCSD header representing the 3DS 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 bool ProcessExtendedHeader(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
if (ncchHeader.ExtendedHeaderSizeInBytes > 0)
|
||||
{
|
||||
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: " + (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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the extended header, if it exists
|
||||
/// </summary>
|
||||
/// <param name="ncsdHeader">NCSD header representing the 3DS 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 ProcessExeFSFileEntries(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * ncsdHeader.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 + ncsdHeader.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) * ncsdHeader.MediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek((((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * ncsdHeader.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="ncsdHeader">NCSD header representing the 3DS 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 ProcessExeFSFilenameTable(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
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: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table");
|
||||
|
||||
var exeFSFilenameTable = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.ExeFSIV, decryptArgs.Encrypt);
|
||||
writer.Write(exeFSFilenameTable.ProcessBytes(reader.ReadBytes((int)ncsdHeader.MediaUnitSize)));
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the ExeFS, if it exists
|
||||
/// </summary>
|
||||
/// <param name="ncsdHeader">NCSD header representing the 3DS 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 ProcessExeFS(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
int exefsSizeM = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * ncsdHeader.MediaUnitSize) / (1024 * 1024));
|
||||
int exefsSizeB = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * ncsdHeader.MediaUnitSize) % (1024 * 1024));
|
||||
int ctroffsetE = (int)(ncsdHeader.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) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * ncsdHeader.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="ncsdHeader">NCSD header representing the 3DS 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 DecryptExeFS(NCSDHeader ncsdHeader, 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(ncsdHeader, 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(ncsdHeader, ncchHeader, reader, writer);
|
||||
|
||||
// Decrypt the rest of the ExeFS
|
||||
ProcessExeFS(ncsdHeader, ncchHeader, reader, writer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypt the RomFS, if it exists
|
||||
/// </summary>
|
||||
/// <param name="ncsdHeader">NCSD header representing the 3DS 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>
|
||||
/// TODO: See how much can be extracted into a common method with Encrypt
|
||||
private void DecryptRomFS(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
// 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 * ncsdHeader.MediaUnitSize) / (1024 * 1024));
|
||||
int romfsSizeB = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * ncsdHeader.MediaUnitSize) % (1024 * 1024));
|
||||
|
||||
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);
|
||||
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="ncsdHeader">NCSD header representing the 3DS file</param>
|
||||
/// <param name="ncchHeader">NCCH header representing the partition</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
private void UpdateDecryptCryptoAndMasks(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryWriter writer)
|
||||
{
|
||||
// Write the new CryptoMethod
|
||||
writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x18B, SeekOrigin.Begin);
|
||||
writer.Write((byte)CryptoMethod.Original);
|
||||
writer.Flush();
|
||||
|
||||
// Write the new BitMasks flag
|
||||
writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.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="ncsdHeader">NCSD header representing the 3DS 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 EncryptExeFS(NCSDHeader ncsdHeader, 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;
|
||||
}
|
||||
|
||||
// For all but the original crypto method, process each of the files in the table
|
||||
if (ncsdHeader.BackupHeader.Flags.CryptoMethod != CryptoMethod.Original)
|
||||
ProcessExeFSFileEntries(ncsdHeader, ncchHeader, reader, writer);
|
||||
|
||||
// Encrypt the filename table
|
||||
ProcessExeFSFilenameTable(ncsdHeader, ncchHeader, reader, writer);
|
||||
|
||||
// Encrypt the rest of the ExeFS
|
||||
ProcessExeFS(ncsdHeader, ncchHeader, reader, writer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypt the RomFS, if it exists
|
||||
/// </summary>
|
||||
/// <param name="ncsdHeader">NCSD header representing the 3DS 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>
|
||||
/// TODO: See how much can be extracted into a common method with Decrypt
|
||||
private void EncryptRomFS(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
// 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 * ncsdHeader.MediaUnitSize) / (1024 * 1024));
|
||||
int romfsSizeB = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * ncsdHeader.MediaUnitSize) % (1024 * 1024));
|
||||
|
||||
// Encrypting RomFS for partitions 1 and up always use Key0x2C
|
||||
if (ncchHeader.PartitionNumber > 0)
|
||||
{
|
||||
if (ncsdHeader.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) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.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="ncsdHeader">NCSD header representing the 3DS file</param>
|
||||
/// <param name="ncchHeader">NCCH header representing the partition</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
private void UpdateEncryptCryptoAndMasks(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryWriter writer)
|
||||
{
|
||||
// Write the new CryptoMethod
|
||||
writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x18B, SeekOrigin.Begin);
|
||||
|
||||
// For partitions 1 and up, set crypto-method to 0x00
|
||||
if (ncchHeader.PartitionNumber > 0)
|
||||
writer.Write((byte)CryptoMethod.Original);
|
||||
|
||||
// If partition 0, restore crypto-method from backup flags
|
||||
else
|
||||
writer.Write((byte)ncsdHeader.BackupHeader.Flags.CryptoMethod);
|
||||
|
||||
writer.Flush();
|
||||
|
||||
// Write the new BitMasks flag
|
||||
writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x18F, SeekOrigin.Begin);
|
||||
BitMasks flag = ncchHeader.Flags.BitMasks;
|
||||
flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF;
|
||||
flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & ncsdHeader.BackupHeader.Flags.BitMasks;
|
||||
writer.Write((byte)flag);
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
11
NDecrypt/NDecrypt.csproj
Normal file
11
NDecrypt/NDecrypt.csproj
Normal file
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.NetCore" Version="1.8.10" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace NDecrypt.Data
|
||||
namespace NDecrypt.Nitro
|
||||
{
|
||||
public static class Constants
|
||||
internal class Constants
|
||||
{
|
||||
public static byte[] NDSEncryptionData = new byte[]
|
||||
{
|
||||
@@ -269,66 +267,11 @@ namespace NDecrypt.Data
|
||||
0x96,0xE7,0xC4,0x18,0x5F,0xAD,0xF5,0x19,
|
||||
};
|
||||
|
||||
// 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 };
|
||||
#region ARM9 decryption check values
|
||||
|
||||
// Note: BigInteger requires the 0x00 at the end of each string in order to preserve the sign value
|
||||
// Note: BigInteger requires that the values be in little endian format, the values in
|
||||
|
||||
// Big Endian - 0x1F, 0xF9, 0xE9, 0xAA, 0xC5, 0xFE, 0x04, 0x08, 0x02, 0x45, 0x91, 0xDC, 0x5D, 0x52, 0x76, 0x8A
|
||||
// Little Endian - 0x8A, 0x76, 0x52, 0x5D, 0xDC, 0x91, 0x45, 0x02, 0x08, 0x04, 0xFE, 0xC5, 0xAA, 0xE9, 0xF9, 0x1F
|
||||
public static BigInteger AESHardwareConstant = new BigInteger(new byte[] { 0x8A, 0x76, 0x52, 0x5D, 0xDC, 0x91, 0x45, 0x02, 0x08, 0x04, 0xFE, 0xC5, 0xAA, 0xE9, 0xF9, 0x1F });
|
||||
|
||||
#region Retail 3DS keys
|
||||
|
||||
// KeyX 0x18 (New 3DS 9.3)
|
||||
// Big Endian - 0x82, 0xE9, 0xC9, 0xBE, 0xBF, 0xB8, 0xBD, 0xB8, 0x75, 0xEC, 0xC0, 0xA0, 0x7D, 0x47, 0x43, 0x74
|
||||
// Little Endian - 0x74, 0x43, 0x47, 0x7D, 0xA0, 0xC0, 0xEC, 0x75, 0xB8, 0xBD, 0xB8, 0xBF, 0xBE, 0xC9, 0xE9, 0x82
|
||||
public static BigInteger KeyX0x18 = new BigInteger(new byte[] { 0x74, 0x43, 0x47, 0x7D, 0xA0, 0xC0, 0xEC, 0x75, 0xB8, 0xBD, 0xB8, 0xBF, 0xBE, 0xC9, 0xE9, 0x82, 0x00 });
|
||||
|
||||
// KeyX 0x1B (New 3DS 9.6)
|
||||
// Big Endian - 0x45, 0xAD, 0x04, 0x95, 0x39, 0x92, 0xC7, 0xC8, 0x93, 0x72, 0x4A, 0x9A, 0x7B, 0xCE, 0x61, 0x82
|
||||
// Little Endian - 0x82, 0x61, 0xCE, 0x7B, 0x9A, 0x4A, 0x72, 0x93, 0xC8, 0xC7, 0x92, 0x39, 0x95, 0x04, 0xAD, 0x45
|
||||
public static BigInteger KeyX0x1B = new BigInteger(new byte[] { 0x82, 0x61, 0xCE, 0x7B, 0x9A, 0x4A, 0x72, 0x93, 0xC8, 0xC7, 0x92, 0x39, 0x95, 0x04, 0xAD, 0x45, 0x00 });
|
||||
|
||||
// KeyX 0x25 (> 7.x)
|
||||
// Big Endian - 0xCE, 0xE7, 0xD8, 0xAB, 0x30, 0xC0, 0x0D, 0xAE, 0x85, 0x0E, 0xF5, 0xE3, 0x82, 0xAC, 0x5A, 0xF3
|
||||
// Little Endian - 0xF3, 0x5A, 0xAC, 0x82, 0xE3, 0xF5, 0x0E, 0x85, 0xAE, 0x0D, 0xC0, 0x30, 0xAB, 0xD8, 0xE7, 0xCE
|
||||
public static BigInteger KeyX0x25 = new BigInteger(new byte[] { 0xF3, 0x5A, 0xAC, 0x82, 0xE3, 0xF5, 0x0E, 0x85, 0xAE, 0x0D, 0xC0, 0x30, 0xAB, 0xD8, 0xE7, 0xCE, 0x00 });
|
||||
|
||||
// Dev KeyX 0x2C (< 6.x)
|
||||
// Big Endian - 0xB9, 0x8E, 0x95, 0xCE, 0xCA, 0x3E, 0x4D, 0x17, 0x1F, 0x76, 0xA9, 0x4D, 0xE9, 0x34, 0xC0, 0x53
|
||||
// Little Endian - 0x53, 0xC0, 0x34, 0xE9, 0x4D, 0xA9, 0x76, 0x1F, 0x17, 0x4D, 0x3E, 0xCA, 0xCE, 0x95, 0x8E, 0xB9
|
||||
public static BigInteger KeyX0x2C = new BigInteger(new byte[] { 0x53, 0xC0, 0x34, 0xE9, 0x4D, 0xA9, 0x76, 0x1F, 0x17, 0x4D, 0x3E, 0xCA, 0xCE, 0x95, 0x8E, 0xB9, 0x00 });
|
||||
public const uint MAGIC30 = 0x72636E65;
|
||||
public const uint MAGIC34 = 0x6A624F79;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dev 3DS Keys
|
||||
|
||||
// Dev KeyX 0x18 (New 3DS 9.3)
|
||||
// Big Endian - 0x30, 0x4B, 0xF1, 0x46, 0x83, 0x72, 0xEE, 0x64, 0x11, 0x5E, 0xBD, 0x40, 0x93, 0xD8, 0x42, 0x76
|
||||
// Little Endian - 0x76, 0x42, 0xD8, 0x93, 0x40, 0xBD, 0x5E, 0x11, 0x64, 0xEE, 0x72, 0x83, 0x46, 0xF1, 0x4B, 0x30
|
||||
public static BigInteger DevKeyX0x18 = new BigInteger(new byte[] { 0x76, 0x42, 0xD8, 0x93, 0x40, 0xBD, 0x5E, 0x11, 0x64, 0xEE, 0x72, 0x83, 0x46, 0xF1, 0x4B, 0x30, 0x00 });
|
||||
|
||||
// Dev KeyX 0x1B New 3DS 9.6)
|
||||
// Big Endian - 0x6C, 0x8B, 0x29, 0x44, 0xA0, 0x72, 0x60, 0x35, 0xF9, 0x41, 0xDF, 0xC0, 0x18, 0x52, 0x4F, 0xB6
|
||||
// Little Endian - 0xB6, 0x4F, 0x52, 0x18, 0xC0, 0xDF, 0x41, 0xF9, 0x35, 0x60, 0x72, 0xA0, 0x44, 0x29, 0x8B, 0x6C
|
||||
public static BigInteger DevKeyX0x1B = new BigInteger(new byte[] { 0xB6, 0x4F, 0x52, 0x18, 0xC0, 0xDF, 0x41, 0xF9, 0x35, 0x60, 0x72, 0xA0, 0x44, 0x29, 0x8B, 0x6C, 0x00 });
|
||||
|
||||
// Dev KeyX 0x25 (> 7.x)
|
||||
// Big Endian - 0x81, 0x90, 0x7A, 0x4B, 0x6F, 0x1B, 0x47, 0x32, 0x3A, 0x67, 0x79, 0x74, 0xCE, 0x4A, 0xD7, 0x1B
|
||||
// Little Endian - 0x1B, 0xD7, 0x4A, 0xCE, 0x74, 0x79, 0x67, 0x3A, 0x32, 0x47, 0x1B, 0x6F, 0x4B, 0x7A, 0x90, 0x81
|
||||
public static BigInteger DevKeyX0x25 = new BigInteger(new byte[] { 0x1B, 0xD7, 0x4A, 0xCE, 0x74, 0x79, 0x67, 0x3A, 0x32, 0x47, 0x1B, 0x6F, 0x4B, 0x7A, 0x90, 0x81, 0x00 });
|
||||
|
||||
// Dev KeyX 0x2C (< 6.x)
|
||||
// Big Endian - 0x51, 0x02, 0x07, 0x51, 0x55, 0x07, 0xCB, 0xB1, 0x8E, 0x24, 0x3D, 0xCB, 0x85, 0xE2, 0x3A, 0x1D
|
||||
// Little Endian - 0x1D, 0x3A, 0xE2, 0x85, 0xCB, 0x3D, 0x24, 0x8E, 0xB1, 0xCB, 0x07, 0x55, 0x51, 0x07, 0x02, 0x51
|
||||
public static BigInteger DevKeyX0x2C = new BigInteger(new byte[] { 0x1D, 0x3A, 0xE2, 0x85, 0xCB, 0x3D, 0x24, 0x8E, 0xB1, 0xCB, 0x07, 0x55, 0x51, 0x07, 0x02, 0x51, 0x00 });
|
||||
|
||||
#endregion
|
||||
|
||||
public const int CXTExtendedDataHeaderLength = 0x800;
|
||||
}
|
||||
}
|
||||
363
NDecrypt/Nitro/DSTool.cs
Normal file
363
NDecrypt/Nitro/DSTool.cs
Normal file
@@ -0,0 +1,363 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NDecrypt.Nitro.Headers;
|
||||
|
||||
namespace NDecrypt.Nitro
|
||||
{
|
||||
public class DSTool : ITool
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the input DS/DSi file
|
||||
/// </summary>
|
||||
private readonly string filename;
|
||||
|
||||
/// <summary>
|
||||
/// Decryption args to use while processing
|
||||
/// </summary>
|
||||
private readonly DecryptArgs decryptArgs;
|
||||
|
||||
#region Encryption process variables
|
||||
|
||||
private uint[] cardHash = new uint[0x412];
|
||||
private uint[] arg2 = new uint[3];
|
||||
|
||||
#endregion
|
||||
|
||||
public DSTool(string filename, DecryptArgs decryptArgs)
|
||||
{
|
||||
this.filename = filename;
|
||||
this.decryptArgs = decryptArgs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process an input file given the input values
|
||||
/// </summary>
|
||||
public bool ProcessFile()
|
||||
{
|
||||
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)))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process secure area in the DS/DSi file
|
||||
/// </summary>
|
||||
/// <param name="ndsHeader">NDS header representing the DS file</param>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
private void ProcessSecureArea(NDSHeader ndsHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
// If we're forcing the operation, tell the user
|
||||
if (decryptArgs.Force)
|
||||
{
|
||||
Console.WriteLine("File is not verified due to force flag being set.");
|
||||
}
|
||||
// If we're not forcing the operation, check to see if we should be proceeding
|
||||
else
|
||||
{
|
||||
bool? isDecrypted = CheckIfDecrypted(reader);
|
||||
if (isDecrypted == null)
|
||||
{
|
||||
Console.WriteLine("File has an empty secure area, cannot proceed");
|
||||
return;
|
||||
}
|
||||
else if (decryptArgs.Encrypt ^ isDecrypted.Value)
|
||||
{
|
||||
Console.WriteLine("File is already " + (decryptArgs.Encrypt ? "encrypted" : "decrypted"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ProcessARM9(ndsHeader, reader, writer);
|
||||
|
||||
Console.WriteLine("File has been " + (decryptArgs.Encrypt ? "encrypted" : "decrypted"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the current file is already decrypted or not (or has an empty secure area)
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <returns>True if the file has known values for a decrypted file, null if it's empty, false otherwise</returns>
|
||||
private bool? CheckIfDecrypted(BinaryReader reader)
|
||||
{
|
||||
reader.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
uint firstValue = reader.ReadUInt32();
|
||||
uint secondValue = reader.ReadUInt32();
|
||||
|
||||
// Empty secure area standard
|
||||
if (firstValue == 0x00000000 && secondValue == 0x00000000)
|
||||
{
|
||||
Console.WriteLine("Empty secure area found. Cannot be encrypted or decrypted.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Improperly decrypted empty secure area (decrypt empty with woodsec)
|
||||
else if ((firstValue == 0xE386C397 && secondValue == 0x82775B7E)
|
||||
|| (firstValue == 0xF98415B8 && secondValue == 0x698068FC)
|
||||
|| (firstValue == 0xA71329EE && secondValue == 0x2A1D4C38)
|
||||
|| (firstValue == 0xC44DCC48 && secondValue == 0x38B6F8CB)
|
||||
|| (firstValue == 0x3A9323B5 && secondValue == 0xC0387241))
|
||||
{
|
||||
Console.WriteLine("Improperly decrypted empty secure area found. Should be encrypted to get proper value.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Improperly encrypted empty secure area (encrypt empty with woodsec)
|
||||
else if ((firstValue == 0x4BCE88BE && secondValue == 0xD3662DD1)
|
||||
|| (firstValue == 0x2543C534 && secondValue == 0xCC4BE38E))
|
||||
{
|
||||
Console.WriteLine("Improperly encrypted empty secure area found. Should be decrypted to get proper value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Properly decrypted nonstandard value (mastering issue)
|
||||
else if ((firstValue == 0xD0D48B67 && secondValue == 0x39392F23) // Dragon Quest 5 (EU)
|
||||
|| (firstValue == 0x014A191A && secondValue == 0xA5C470B9) // Dragon Quest 5 (USA)
|
||||
|| (firstValue == 0x7829BC8D && secondValue == 0x9968EF44) // Dragon Quest 5 (JP)
|
||||
|| (firstValue == 0xC4A15AB8 && secondValue == 0xD2E667C8) // Prince of Persia (EU)
|
||||
|| (firstValue == 0xD5E97D20 && secondValue == 0x21B2A159)) // Prince of Persia (USA)
|
||||
{
|
||||
Console.WriteLine("Decrypted secure area for known, nonstandard value found.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Properly decrypted prototype value
|
||||
else if (firstValue == 0xBA35F813 && secondValue == 0xB691AAE8)
|
||||
{
|
||||
Console.WriteLine("Decrypted secure area for prototype found.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard decryption values
|
||||
return firstValue == 0xE7FFDEFF && secondValue == 0xE7FFDEFF;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the secure ARM9 region of the file, if possible
|
||||
/// </summary>
|
||||
/// <param name="ndsHeader">NDS header representing the DS file</param>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
private void ProcessARM9(NDSHeader ndsHeader, BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
// Seek to the beginning of the secure area
|
||||
reader.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
|
||||
// Grab the first two blocks
|
||||
uint p0 = reader.ReadUInt32();
|
||||
uint p1 = reader.ReadUInt32();
|
||||
|
||||
// Perform the initialization steps
|
||||
Init1(ndsHeader);
|
||||
if (!decryptArgs.Encrypt) Decrypt(ref p1, ref p0);
|
||||
arg2[1] <<= 1;
|
||||
arg2[2] >>= 1;
|
||||
Init2();
|
||||
|
||||
// If we're decrypting, set the proper flags
|
||||
if (!decryptArgs.Encrypt)
|
||||
{
|
||||
Decrypt(ref p1, ref p0);
|
||||
if (p0 == Constants.MAGIC30 && p1 == Constants.MAGIC34)
|
||||
{
|
||||
p0 = 0xE7FFDEFF;
|
||||
p1 = 0xE7FFDEFF;
|
||||
}
|
||||
|
||||
writer.Write(p0);
|
||||
writer.Write(p1);
|
||||
}
|
||||
|
||||
// Ensure alignment
|
||||
reader.BaseStream.Seek(0x4008, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek(0x4008, SeekOrigin.Begin);
|
||||
|
||||
// Loop throgh the main encryption step
|
||||
uint size = 0x800 - 8;
|
||||
while (size > 0)
|
||||
{
|
||||
p0 = reader.ReadUInt32();
|
||||
p1 = reader.ReadUInt32();
|
||||
|
||||
if (decryptArgs.Encrypt)
|
||||
Encrypt(ref p1, ref p0);
|
||||
else
|
||||
Decrypt(ref p1, ref p0);
|
||||
|
||||
writer.Write(p0);
|
||||
writer.Write(p1);
|
||||
|
||||
size -= 8;
|
||||
}
|
||||
|
||||
// Replace the header explicitly if we're encrypting
|
||||
if (decryptArgs.Encrypt)
|
||||
{
|
||||
reader.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
|
||||
p0 = reader.ReadUInt32();
|
||||
p1 = reader.ReadUInt32();
|
||||
|
||||
if (p0 == 0xE7FFDEFF && p1 == 0xE7FFDEFF)
|
||||
{
|
||||
p0 = Constants.MAGIC30;
|
||||
p1 = Constants.MAGIC34;
|
||||
}
|
||||
|
||||
Encrypt(ref p1, ref p0);
|
||||
Init1(ndsHeader);
|
||||
Encrypt(ref p1, ref p0);
|
||||
|
||||
writer.Write(p0);
|
||||
writer.Write(p1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First common initialization step
|
||||
/// </summary>
|
||||
/// <param name="ndsHeader">NDS header representing the DS file</param>
|
||||
private void Init1(NDSHeader ndsHeader)
|
||||
{
|
||||
Buffer.BlockCopy(Constants.NDSEncryptionData, 0, cardHash, 0, 4 * (1024 + 18));
|
||||
arg2 = new uint[] { ndsHeader.Gamecode, ndsHeader.Gamecode >> 1, ndsHeader.Gamecode << 1 };
|
||||
Init2();
|
||||
Init2();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Second common initialization step
|
||||
/// </summary>
|
||||
private void Init2()
|
||||
{
|
||||
Encrypt(ref arg2[2], ref arg2[1]);
|
||||
Encrypt(ref arg2[1], ref arg2[0]);
|
||||
|
||||
byte[] allBytes = BitConverter.GetBytes(arg2[0])
|
||||
.Concat(BitConverter.GetBytes(arg2[1]))
|
||||
.Concat(BitConverter.GetBytes(arg2[2]))
|
||||
.ToArray();
|
||||
|
||||
UpdateHashtable(allBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a decryption step
|
||||
/// </summary>
|
||||
/// <param name="arg1">First unsigned value to use in decryption</param>
|
||||
/// <param name="arg2">Second unsigned value to use in decryption</param>
|
||||
private void Decrypt(ref uint arg1, ref uint arg2)
|
||||
{
|
||||
uint a = arg1;
|
||||
uint b = arg2;
|
||||
for (int i = 17; i > 1; i--)
|
||||
{
|
||||
uint c = cardHash[i] ^ a;
|
||||
a = b ^ Lookup(c);
|
||||
b = c;
|
||||
}
|
||||
|
||||
arg1 = b ^ cardHash[0];
|
||||
arg2 = a ^ cardHash[1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform an encryption step
|
||||
/// </summary>
|
||||
/// <param name="arg1">First unsigned value to use in encryption</param>
|
||||
/// <param name="arg2">Second unsigned value to use in encryption</param>
|
||||
private void Encrypt(ref uint arg1, ref uint arg2)
|
||||
{
|
||||
uint a = arg1;
|
||||
uint b = arg2;
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
uint c = cardHash[i] ^ a;
|
||||
a = b ^ Lookup(c);
|
||||
b = c;
|
||||
}
|
||||
|
||||
arg2 = a ^ cardHash[16];
|
||||
arg1 = b ^ cardHash[17];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lookup the value from the hashtable
|
||||
/// </summary>
|
||||
/// <param name="v">Value to lookup in the hashtable</param>
|
||||
/// <returns>Processed value through the hashtable</returns>
|
||||
private uint Lookup(uint v)
|
||||
{
|
||||
uint a = (v >> 24) & 0xFF;
|
||||
uint b = (v >> 16) & 0xFF;
|
||||
uint c = (v >> 8) & 0xFF;
|
||||
uint d = (v >> 0) & 0xFF;
|
||||
|
||||
a = cardHash[a + 18 + 0];
|
||||
b = cardHash[b + 18 + 256];
|
||||
c = cardHash[c + 18 + 512];
|
||||
d = cardHash[d + 18 + 768];
|
||||
|
||||
return d + (c ^ (b + a));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the hashtable
|
||||
/// </summary>
|
||||
/// <param name="arg1">Value to update the hashtable with</param>
|
||||
private void UpdateHashtable(byte[] arg1)
|
||||
{
|
||||
for (int j = 0; j < 18; j++)
|
||||
{
|
||||
uint r3 = 0;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
r3 <<= 8;
|
||||
r3 |= arg1[(j * 4 + i) & 7];
|
||||
}
|
||||
|
||||
cardHash[j] ^= r3;
|
||||
}
|
||||
|
||||
uint tmp1 = 0;
|
||||
uint tmp2 = 0;
|
||||
for (int i = 0; i < 18; i += 2)
|
||||
{
|
||||
Encrypt(ref tmp1, ref tmp2);
|
||||
cardHash[i + 0] = tmp1;
|
||||
cardHash[i + 1] = tmp2;
|
||||
}
|
||||
for (int i = 0; i < 0x400; i += 2)
|
||||
{
|
||||
Encrypt(ref tmp1, ref tmp2);
|
||||
cardHash[i + 18 + 0] = tmp1;
|
||||
cardHash[i + 18 + 1] = tmp2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
NDecrypt/Nitro/Enums.cs
Normal file
11
NDecrypt/Nitro/Enums.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace NDecrypt.Nitro
|
||||
{
|
||||
internal enum NDSUnitcode : byte
|
||||
{
|
||||
NDS = 0x00,
|
||||
NDSPlusDSi = 0x02,
|
||||
DSi = 0x03,
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using NDecrypt.Data;
|
||||
using System.IO;
|
||||
|
||||
namespace NDecrypt.Headers
|
||||
namespace NDecrypt.Nitro.Headers
|
||||
{
|
||||
public class NDSHeader
|
||||
internal class NDSHeader
|
||||
{
|
||||
#region Common to all NDS files
|
||||
#region Common
|
||||
|
||||
/// <summary>
|
||||
/// Game Title
|
||||
@@ -576,271 +574,5 @@ namespace NDecrypt.Headers
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process secure area in the DS/DSi file
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
/// <param name="encrypt">True if we want to encrypt the partitions, false otherwise</param>
|
||||
public void ProcessSecureArea(BinaryReader reader, BinaryWriter writer, bool encrypt)
|
||||
{
|
||||
bool isDecrypted = CheckIfDecrypted(reader);
|
||||
if (encrypt ^ isDecrypted)
|
||||
{
|
||||
Console.WriteLine("File is already " + (encrypt ? "encrypted" : "decrypted"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (encrypt)
|
||||
EncryptARM9(reader, writer);
|
||||
else
|
||||
DecryptARM9(reader, writer);
|
||||
|
||||
Console.WriteLine("File has been " + (encrypt ? "encrypted" : "decrypted"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the current file is already decrypted or not
|
||||
/// </summary>
|
||||
/// <param name="reader"></param>
|
||||
/// <returns></returns>
|
||||
private bool CheckIfDecrypted(BinaryReader reader)
|
||||
{
|
||||
reader.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
uint firstValue = reader.ReadUInt32();
|
||||
uint secondValue = reader.ReadUInt32();
|
||||
|
||||
return (firstValue == 0xE7FFDEFF) && (secondValue == 0xE7FFDEFF);
|
||||
}
|
||||
|
||||
private uint[] card_hash = new uint[0x412];
|
||||
|
||||
/// <summary>
|
||||
/// Lookup the value from the magic table
|
||||
/// </summary>
|
||||
/// <param name="magic"></param>
|
||||
/// <param name="v"></param>
|
||||
/// <returns></returns>
|
||||
private uint Lookup(ref uint[] magic, uint v)
|
||||
{
|
||||
uint a = (v >> 24) & 0xFF;
|
||||
uint b = (v >> 16) & 0xFF;
|
||||
uint c = (v >> 8) & 0xFF;
|
||||
uint d = (v >> 0) & 0xFF;
|
||||
|
||||
a = magic[a + 18 + 0];
|
||||
b = magic[b + 18 + 256];
|
||||
c = magic[c + 18 + 512];
|
||||
d = magic[d + 18 + 768];
|
||||
|
||||
return d + (c ^ (b + a));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform an encryption step
|
||||
/// </summary>
|
||||
/// <param name="arg1"></param>
|
||||
/// <param name="arg2"></param>
|
||||
private void Encrypt(ref uint arg1, ref uint arg2)
|
||||
{
|
||||
uint a = arg1;
|
||||
uint b = arg2;
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
uint c = card_hash[i] ^ a;
|
||||
a = b ^ Lookup(ref card_hash, c);
|
||||
b = c;
|
||||
}
|
||||
|
||||
arg2 = a ^ card_hash[16];
|
||||
arg1 = b ^ card_hash[17];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a decryption step
|
||||
/// </summary>
|
||||
/// <param name="arg1"></param>
|
||||
/// <param name="arg2"></param>
|
||||
private void Decrypt(ref uint arg1, ref uint arg2)
|
||||
{
|
||||
uint a = arg1;
|
||||
uint b = arg2;
|
||||
for (int i = 17; i > 1; i--)
|
||||
{
|
||||
uint c = card_hash[i] ^ a;
|
||||
a = b ^ Lookup(ref card_hash, c);
|
||||
b = c;
|
||||
}
|
||||
|
||||
arg1 = b ^ card_hash[0];
|
||||
arg2 = a ^ card_hash[1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the magic hashtable
|
||||
/// </summary>
|
||||
/// <param name="arg1"></param>
|
||||
private void UpdateHashtable(byte[] arg1)
|
||||
{
|
||||
for (int j = 0; j < 18; j++)
|
||||
{
|
||||
uint r3 = 0;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
r3 <<= 8;
|
||||
r3 |= arg1[(j * 4 + i) & 7];
|
||||
}
|
||||
|
||||
card_hash[j] ^= r3;
|
||||
}
|
||||
|
||||
uint tmp1 = 0;
|
||||
uint tmp2 = 0;
|
||||
for (int i = 0; i < 18; i += 2)
|
||||
{
|
||||
Encrypt(ref tmp1, ref tmp2);
|
||||
card_hash[i + 0] = tmp1;
|
||||
card_hash[i + 1] = tmp2;
|
||||
}
|
||||
for (int i = 0; i < 0x400; i += 2)
|
||||
{
|
||||
Encrypt(ref tmp1, ref tmp2);
|
||||
card_hash[i + 18 + 0] = tmp1;
|
||||
card_hash[i + 18 + 1] = tmp2;
|
||||
}
|
||||
}
|
||||
|
||||
private uint[] arg2 = new uint[3];
|
||||
|
||||
private void Init2(uint[] a)
|
||||
{
|
||||
Encrypt(ref a[2], ref a[1]);
|
||||
Encrypt(ref a[1], ref a[0]);
|
||||
|
||||
byte[] a0bytes = BitConverter.GetBytes(a[0]);
|
||||
byte[] a1bytes = BitConverter.GetBytes(a[1]);
|
||||
byte[] a2bytes = BitConverter.GetBytes(a[2]);
|
||||
byte[] allbytes = new byte[3 * sizeof(uint)];
|
||||
a0bytes.CopyTo(allbytes, 0);
|
||||
a1bytes.CopyTo(allbytes, 4);
|
||||
a2bytes.CopyTo(allbytes, 8);
|
||||
|
||||
UpdateHashtable(allbytes);
|
||||
}
|
||||
|
||||
private void Init1()
|
||||
{
|
||||
Buffer.BlockCopy(Constants.NDSEncryptionData, 0, card_hash, 0, 4 * (1024 + 18));
|
||||
arg2[0] = Gamecode;
|
||||
arg2[1] = Gamecode >> 1;
|
||||
arg2[2] = Gamecode << 1;
|
||||
Init2(arg2);
|
||||
Init2(arg2);
|
||||
}
|
||||
|
||||
// ARM9 decryption check values
|
||||
private const uint MAGIC30 = 0x72636E65;
|
||||
private const uint MAGIC34 = 0x6A624F79;
|
||||
|
||||
/// <summary>
|
||||
/// Decrypt the secure ARM9 area of the file, if possible
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
private void DecryptARM9(BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
// Seek to the beginning of the secure area
|
||||
reader.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
|
||||
uint p0 = reader.ReadUInt32();
|
||||
uint p1 = reader.ReadUInt32();
|
||||
|
||||
Init1();
|
||||
Decrypt(ref p1, ref p0);
|
||||
arg2[1] <<= 1;
|
||||
arg2[2] >>= 1;
|
||||
Init2(arg2);
|
||||
Decrypt(ref p1, ref p0);
|
||||
|
||||
if (p0 == MAGIC30 && p1 == MAGIC34)
|
||||
{
|
||||
p0 = 0xE7FFDEFF;
|
||||
p1 = 0xE7FFDEFF;
|
||||
}
|
||||
|
||||
writer.Write(p0);
|
||||
writer.Write(p1);
|
||||
|
||||
uint size = 0x800 - 8;
|
||||
while (size > 0)
|
||||
{
|
||||
p0 = reader.ReadUInt32();
|
||||
p1 = reader.ReadUInt32();
|
||||
|
||||
Decrypt(ref p1, ref p0);
|
||||
|
||||
writer.Write(p0);
|
||||
writer.Write(p1);
|
||||
|
||||
size -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypt the secure ARM9 area of the file, if possible
|
||||
/// </summary>
|
||||
/// <param name="reader">BinaryReader representing the input stream</param>
|
||||
/// <param name="writer">BinaryWriter representing the output stream</param>
|
||||
private void EncryptARM9(BinaryReader reader, BinaryWriter writer)
|
||||
{
|
||||
// Seek to the beginning of the secure area (skip first 2 UInt32s)
|
||||
reader.BaseStream.Seek(0x4008, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek(0x4008, SeekOrigin.Begin);
|
||||
|
||||
Init1();
|
||||
|
||||
arg2[1] <<= 1;
|
||||
arg2[2] >>= 1;
|
||||
|
||||
Init2(arg2);
|
||||
|
||||
uint p0, p1;
|
||||
uint size = 0x800 - 8;
|
||||
while (size > 0)
|
||||
{
|
||||
p0 = reader.ReadUInt32();
|
||||
p1 = reader.ReadUInt32();
|
||||
|
||||
Encrypt(ref p1, ref p0);
|
||||
|
||||
writer.Write(p0);
|
||||
writer.Write(p1);
|
||||
|
||||
size -= 8;
|
||||
}
|
||||
|
||||
// place header
|
||||
reader.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
writer.BaseStream.Seek(0x4000, SeekOrigin.Begin);
|
||||
|
||||
p0 = reader.ReadUInt32();
|
||||
p1 = reader.ReadUInt32();
|
||||
|
||||
if (p0 == 0xE7FFDEFF && p1 == 0xE7FFDEFF)
|
||||
{
|
||||
p0 = MAGIC30;
|
||||
p1 = MAGIC34;
|
||||
}
|
||||
|
||||
Encrypt(ref p1, ref p0);
|
||||
Init1();
|
||||
Encrypt(ref p1, ref p0);
|
||||
|
||||
writer.Write(p0);
|
||||
writer.Write(p1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
36
NDecrypt/Tools/Enums.cs
Normal file
36
NDecrypt/Tools/Enums.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace NDecrypt.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,
|
||||
}
|
||||
}
|
||||
123
NDecrypt/Tools/Hasher.cs
Normal file
123
NDecrypt/Tools/Hasher.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace NDecrypt.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
148
NDecrypt/Tools/IniReader.cs
Normal file
148
NDecrypt/Tools/IniReader.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NDecrypt.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
153
NDecrypt/Tools/OptimizedCRC.cs
Normal file
153
NDecrypt/Tools/OptimizedCRC.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
NDecrypt/Tools/ThreadLoadBuffer.cs
Normal file
79
NDecrypt/Tools/ThreadLoadBuffer.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace Compress.ThreadReaders
|
||||
{
|
||||
public class ThreadLoadBuffer : IDisposable
|
||||
{
|
||||
private readonly AutoResetEvent _waitEvent;
|
||||
private readonly AutoResetEvent _outEvent;
|
||||
private readonly Thread _tWorker;
|
||||
|
||||
private byte[] _buffer;
|
||||
private int _size;
|
||||
private readonly Stream _ds;
|
||||
private bool _finished;
|
||||
public bool errorState;
|
||||
|
||||
public int SizeRead;
|
||||
|
||||
public ThreadLoadBuffer(Stream ds)
|
||||
{
|
||||
_waitEvent = new AutoResetEvent(false);
|
||||
_outEvent = new AutoResetEvent(false);
|
||||
_finished = false;
|
||||
_ds = ds;
|
||||
errorState = false;
|
||||
|
||||
_tWorker = new Thread(MainLoop);
|
||||
_tWorker.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_waitEvent.Close();
|
||||
_outEvent.Close();
|
||||
}
|
||||
|
||||
private void MainLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
_waitEvent.WaitOne();
|
||||
if (_finished)
|
||||
{
|
||||
break;
|
||||
}
|
||||
try
|
||||
{
|
||||
SizeRead = _ds.Read(_buffer, 0, _size);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
errorState = true;
|
||||
}
|
||||
_outEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Trigger(byte[] buffer, int size)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_size = size;
|
||||
_waitEvent.Set();
|
||||
}
|
||||
|
||||
public void Wait()
|
||||
{
|
||||
_outEvent.WaitOne();
|
||||
}
|
||||
|
||||
public void Finish()
|
||||
{
|
||||
_finished = true;
|
||||
_waitEvent.Set();
|
||||
_tWorker.Join();
|
||||
}
|
||||
}
|
||||
}
|
||||
77
README.md
Normal file
77
README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# NDecrypt
|
||||
|
||||
A simple tool for simple people.
|
||||
|
||||
## What is this?
|
||||
|
||||
This is a code port of 3 different programs:
|
||||
|
||||
- `3ds_encrypt.py`
|
||||
- `3ds_decrypt.py`
|
||||
- `woodsec` (part of [wooddumper](https://github.com/TuxSH/wooddumper))
|
||||
|
||||
## No really, what is this?
|
||||
|
||||
This tool allows you to encrypt and decrypt your personally dumped NDS and N3DS roms with minimal hassle. The only caveat right now is that you need a `keys.bin` file for your personally obtained encryption keys.
|
||||
|
||||
## So how do I use this?
|
||||
|
||||
NDecrypt.exe <operation> [flags] <path> ...
|
||||
|
||||
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 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 (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
|
||||
- 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. 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.
|
||||
Reference in New Issue
Block a user