├── NDecrypt
├── Enumerations.cs
├── Features
│ ├── InfoFeature.cs
│ ├── DecryptFeature.cs
│ ├── EncryptFeature.cs
│ └── BaseFeature.cs
├── HashingHelper.cs
├── NDecrypt.csproj
└── Program.cs
├── config-default.json
├── .github
└── workflows
│ ├── check_pr.yml
│ └── build_and_test.yml
├── .vscode
├── tasks.json
└── launch.json
├── LICENSE
├── NDecrypt.Core
├── ITool.cs
├── NDecrypt.Core.csproj
├── Configuration.cs
├── PartitionKeys.cs
├── DSTool.cs
├── DecryptArgs.cs
└── ThreeDSTool.cs
├── NDecrypt.sln
├── .gitattributes
├── .gitignore
├── publish-win.ps1
├── publish-nix.sh
└── README.md
/NDecrypt/Enumerations.cs:
--------------------------------------------------------------------------------
1 | namespace NDecrypt
2 | {
3 | ///
4 | /// Type of the detected file
5 | ///
6 | internal enum FileType
7 | {
8 | NULL,
9 | NDS,
10 | NDSi,
11 | iQueDS,
12 | N3DS,
13 | iQue3DS,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/config-default.json:
--------------------------------------------------------------------------------
1 | {
2 | "NitroEncryptionData": null,
3 | "AESHardwareConstant": null,
4 | "KeyX0x18": null,
5 | "KeyX0x1B": null,
6 | "KeyX0x25": null,
7 | "KeyX0x2C": null,
8 | "DevKeyX0x18": null,
9 | "DevKeyX0x1B": null,
10 | "DevKeyX0x25": null,
11 | "DevKeyX0x2C": null
12 | }
--------------------------------------------------------------------------------
/.github/workflows/check_pr.yml:
--------------------------------------------------------------------------------
1 | name: Build PR
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v5
10 |
11 | - name: Setup .NET
12 | uses: actions/setup-dotnet@v5
13 | with:
14 | dotnet-version: |
15 | 8.0.x
16 | 9.0.x
17 | 10.0.x
18 |
19 | - name: Build
20 | run: dotnet build
21 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "build",
8 | "command": "dotnet",
9 | "type": "shell",
10 | "args": [
11 | "build",
12 | // Ask dotnet build to generate full paths for file names.
13 | "/property:GenerateFullPaths=true",
14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel
15 | "/consoleloggerparameters:NoSummary"
16 | ],
17 | "group": "build",
18 | "presentation": {
19 | "reveal": "silent"
20 | },
21 | "problemMatcher": "$msCompile"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018-2025 Matt Nadareski
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to find out which attributes exist for C# debugging
3 | // Use hover for the description of the existing attributes
4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (console)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | // If you have changed target frameworks, make sure to update the program path.
13 | "program": "${workspaceFolder}/NDecrypt/bin/Debug/net10.0/NDecrypt.dll",
14 | "args": [],
15 | "cwd": "${workspaceFolder}/NDecrypt",
16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
17 | "console": "internalConsole",
18 | "stopAtEntry": false
19 | },
20 | {
21 | "name": ".NET Core Attach",
22 | "type": "coreclr",
23 | "request": "attach",
24 | "processId": "${command:pickProcess}"
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/NDecrypt/Features/InfoFeature.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace NDecrypt.Features
4 | {
5 | internal sealed class InfoFeature : BaseFeature
6 | {
7 | #region Feature Definition
8 |
9 | public const string DisplayName = "info";
10 |
11 | private static readonly string[] _flags = ["i", "info"];
12 |
13 | private const string _description = "Output file information";
14 |
15 | #endregion
16 |
17 | public InfoFeature()
18 | : base(DisplayName, _flags, _description)
19 | {
20 | RequiresInputs = true;
21 |
22 | Add(HashFlag);
23 | }
24 |
25 | ///
26 | protected override void ProcessFile(string input)
27 | {
28 | // Attempt to derive the tool for the path
29 | var tool = DeriveTool(input);
30 | if (tool == null)
31 | return;
32 |
33 | Console.WriteLine($"Processing {input}");
34 |
35 | string? infoString = tool.GetInformation(input);
36 | infoString ??= "There was a problem getting file information!";
37 |
38 | Console.WriteLine(infoString);
39 |
40 | // Output the file hashes, if expected
41 | if (GetBoolean(HashName))
42 | WriteHashes(input);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/NDecrypt/HashingHelper.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using SabreTools.Hashing;
3 |
4 | namespace NDecrypt
5 | {
6 | internal static class HashingHelper
7 | {
8 | ///
9 | /// Retrieve file information for a single file
10 | ///
11 | /// Filename to get information from
12 | /// Formatted string representing the hashes, null on error
13 | public static string? GetInfo(string input)
14 | {
15 | // If the file doesn't exist, return null
16 | if (!File.Exists(input))
17 | return null;
18 |
19 | // Get the file information, if possible
20 | HashType[] hashTypes = [HashType.CRC32, HashType.MD5, HashType.SHA1, HashType.SHA256];
21 | var hashDict = HashTool.GetFileHashesAndSize(input, hashTypes, out long size);
22 | if (hashDict == null)
23 | return null;
24 |
25 | // Get the results
26 | return $"Size: {size}\n"
27 | + $"CRC-32: {(hashDict.TryGetValue(HashType.CRC32, out string? value) ? value : string.Empty)}\n"
28 | + $"MD5: {(hashDict.TryGetValue(HashType.MD5, out string? value1) ? value1 : string.Empty)}\n"
29 | + $"SHA-1: {(hashDict.TryGetValue(HashType.SHA1, out string? value2) ? value2 : string.Empty)}\n"
30 | + $"SHA-256: {(hashDict.TryGetValue(HashType.SHA256, out string? value3) ? value3 : string.Empty)}\n";
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/build_and_test.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v5
13 | with:
14 | fetch-depth: 0
15 |
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v5
18 | with:
19 | dotnet-version: |
20 | 8.0.x
21 | 9.0.x
22 | 10.0.x
23 |
24 | - name: Run tests
25 | run: dotnet test
26 |
27 | - name: Run publish script
28 | run: ./publish-nix.sh -d
29 |
30 | - name: Update rolling tag
31 | run: |
32 | git config user.name "github-actions[bot]"
33 | git config user.email "github-actions[bot]@users.noreply.github.com"
34 | git tag -f rolling
35 | git push origin :refs/tags/rolling || true
36 | git push origin rolling --force
37 |
38 | - name: Upload to rolling
39 | uses: ncipollo/release-action@v1.20.0
40 | with:
41 | allowUpdates: True
42 | artifacts: "*.nupkg,*.snupkg,*.zip"
43 | body: "Last built commit: ${{ github.sha }}"
44 | name: "Rolling Release"
45 | prerelease: True
46 | replacesArtifacts: True
47 | tag: "rolling"
48 | updateOnlyUnreleased: True
49 |
--------------------------------------------------------------------------------
/NDecrypt.Core/ITool.cs:
--------------------------------------------------------------------------------
1 | namespace NDecrypt.Core
2 | {
3 | public interface ITool
4 | {
5 | ///
6 | /// Attempts to encrypt an input file
7 | ///
8 | /// Name of the file to encrypt
9 | /// Optional name of the file to write to
10 | /// Indicates if the operation should be forced
11 | /// True if the file could be encrypted, false otherwise
12 | /// If an output filename is not provided, the input file will be overwritten
13 | bool EncryptFile(string input, string? output, bool force);
14 |
15 | ///
16 | /// Attempts to decrypt an input file
17 | ///
18 | /// Name of the file to decrypt
19 | /// Optional name of the file to write to
20 | /// Indicates if the operation should be forced
21 | /// True if the file could be decrypted, false otherwise
22 | /// If an output filename is not provided, the input file will be overwritten
23 | bool DecryptFile(string input, string? output, bool force);
24 |
25 | ///
26 | /// Attempts to get information on an input file
27 | ///
28 | /// Name of the file get information on
29 | /// String representing the info, null on error
30 | string? GetInformation(string filename);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/NDecrypt.Core/NDecrypt.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1
6 | false
7 | false
8 | true
9 | latest
10 | enable
11 | true
12 | snupkg
13 | true
14 | 0.5.0
15 |
16 |
17 | Matt Nadareski
18 | Common code for all NDecrypt processors
19 | Copyright (c) Matt Nadareski 2019-2025
20 | MIT
21 | https://github.com/SabreTools/
22 | https://github.com/SabreTools/NDecrypt
23 | git
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/NDecrypt/Features/DecryptFeature.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace NDecrypt.Features
4 | {
5 | internal sealed class DecryptFeature : BaseFeature
6 | {
7 | #region Feature Definition
8 |
9 | public const string DisplayName = "decrypt";
10 |
11 | private static readonly string[] _flags = ["d", "decrypt"];
12 |
13 | private const string _description = "Decrypt the input files";
14 |
15 | #endregion
16 |
17 | public DecryptFeature()
18 | : base(DisplayName, _flags, _description)
19 | {
20 | RequiresInputs = true;
21 |
22 | Add(ConfigString);
23 | Add(DevelopmentFlag);
24 | Add(ForceFlag);
25 | Add(HashFlag);
26 |
27 | // TODO: Include this when enabled
28 | // Add(OverwriteFlag);
29 | }
30 |
31 | ///
32 | protected override void ProcessFile(string input)
33 | {
34 | // Attempt to derive the tool for the path
35 | var tool = DeriveTool(input);
36 | if (tool == null)
37 | return;
38 |
39 | // Derive the output filename, if required
40 | string? output = null;
41 | if (!GetBoolean(OverwriteName))
42 | output = GetOutputFile(input, ".dec");
43 |
44 | Console.WriteLine($"Processing {input}");
45 |
46 | if (!tool.DecryptFile(input, output, GetBoolean(ForceName)))
47 | {
48 | Console.WriteLine("Decryption failed!");
49 | return;
50 | }
51 |
52 | // Output the file hashes, if expected
53 | if (GetBoolean(HashName))
54 | WriteHashes(input);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/NDecrypt/Features/EncryptFeature.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace NDecrypt.Features
4 | {
5 | internal sealed class EncryptFeature : BaseFeature
6 | {
7 | #region Feature Definition
8 |
9 | public const string DisplayName = "encrypt";
10 |
11 | private static readonly string[] _flags = ["e", "encrypt"];
12 |
13 | private const string _description = "Encrypt the input files";
14 |
15 | #endregion
16 |
17 | public EncryptFeature()
18 | : base(DisplayName, _flags, _description)
19 | {
20 | RequiresInputs = true;
21 |
22 | Add(ConfigString);
23 | Add(DevelopmentFlag);
24 | Add(ForceFlag);
25 | Add(HashFlag);
26 |
27 | // TODO: Include this when enabled
28 | // Add(OverwriteFlag);
29 | }
30 |
31 | ///
32 | protected override void ProcessFile(string input)
33 | {
34 | // Attempt to derive the tool for the path
35 | var tool = DeriveTool(input);
36 | if (tool == null)
37 | return;
38 |
39 | // Derive the output filename, if required
40 | string? output = null;
41 | if (!GetBoolean(OverwriteName))
42 | output = GetOutputFile(input, ".enc");
43 |
44 | Console.WriteLine($"Processing {input}");
45 |
46 | if (!tool.EncryptFile(input, output, GetBoolean(ForceName)))
47 | {
48 | Console.WriteLine("Encryption failed!");
49 | return;
50 | }
51 |
52 | // Output the file hashes, if expected
53 | if (GetBoolean(HashName))
54 | WriteHashes(input);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/NDecrypt.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32922.545
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{31CE0445-F693-4C9A-B6CD-499C38CFF7FE}"
7 | ProjectSection(SolutionItems) = preProject
8 | README.md = README.md
9 | EndProjectSection
10 | EndProject
11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDecrypt", "NDecrypt\NDecrypt.csproj", "{05566793-831F-4AE1-A6D2-F9214F36618E}"
12 | EndProject
13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDecrypt.Core", "NDecrypt.Core\NDecrypt.Core.csproj", "{91C54370-5741-4742-B2E9-EC498551AD1C}"
14 | EndProject
15 | Global
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug|Any CPU = Debug|Any CPU
18 | Release|Any CPU = Release|Any CPU
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {05566793-831F-4AE1-A6D2-F9214F36618E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {05566793-831F-4AE1-A6D2-F9214F36618E}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {05566793-831F-4AE1-A6D2-F9214F36618E}.Release|Any CPU.ActiveCfg = Release|Any CPU
24 | {05566793-831F-4AE1-A6D2-F9214F36618E}.Release|Any CPU.Build.0 = Release|Any CPU
25 | {91C54370-5741-4742-B2E9-EC498551AD1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {91C54370-5741-4742-B2E9-EC498551AD1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {91C54370-5741-4742-B2E9-EC498551AD1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {91C54370-5741-4742-B2E9-EC498551AD1C}.Release|Any CPU.Build.0 = Release|Any CPU
29 | EndGlobalSection
30 | GlobalSection(SolutionProperties) = preSolution
31 | HideSolutionNode = FALSE
32 | EndGlobalSection
33 | GlobalSection(ExtensibilityGlobals) = postSolution
34 | SolutionGuid = {5AB50D9B-BA18-4F96-804B-52E7E0845B37}
35 | EndGlobalSection
36 | EndGlobal
37 |
--------------------------------------------------------------------------------
/NDecrypt.Core/Configuration.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Newtonsoft.Json;
3 |
4 | namespace NDecrypt.Core
5 | {
6 | internal class Configuration
7 | {
8 | #region DS-Specific Fields
9 |
10 | ///
11 | /// Encryption data taken from woodsec
12 | ///
13 | public string? NitroEncryptionData { get; set; }
14 |
15 | #endregion
16 |
17 | #region 3DS-Specific Fields
18 |
19 | ///
20 | /// AES Hardware Constant
21 | ///
22 | /// generator
23 | public string? AESHardwareConstant { get; set; }
24 |
25 | ///
26 | /// KeyX 0x18 (New 3DS 9.3)
27 | ///
28 | /// slot0x18KeyX
29 | public string? KeyX0x18 { get; set; }
30 |
31 | ///
32 | /// Dev KeyX 0x18 (New 3DS 9.3)
33 | ///
34 | public string? DevKeyX0x18 { get; set; }
35 |
36 | ///
37 | /// KeyX 0x1B (New 3DS 9.6)
38 | ///
39 | /// slot0x1BKeyX
40 | public string? KeyX0x1B { get; set; }
41 |
42 | ///
43 | /// Dev KeyX 0x1B New 3DS 9.6)
44 | ///
45 | public string? DevKeyX0x1B { get; set; }
46 |
47 | ///
48 | /// KeyX 0x25 (> 7.x)
49 | ///
50 | /// slot0x25KeyX
51 | public string? KeyX0x25 { get; set; }
52 |
53 | ///
54 | /// Dev KeyX 0x25 (> 7.x)
55 | ///
56 | public string? DevKeyX0x25 { get; set; }
57 |
58 | ///
59 | /// KeyX 0x2C (< 6.x)
60 | ///
61 | /// slot0x2CKeyX
62 | public string? KeyX0x2C { get; set; }
63 |
64 | ///
65 | /// Dev KeyX 0x2C (< 6.x)
66 | ///
67 | public string? DevKeyX0x2C { get; set; }
68 |
69 | #endregion
70 |
71 | public static Configuration? Create(string path)
72 | {
73 | // Ensure the file exists
74 | if (!File.Exists(path))
75 | return null;
76 |
77 | // Parse the configuration directly
78 | try
79 | {
80 | string contents = File.ReadAllText(path);
81 | return JsonConvert.DeserializeObject(contents);
82 | }
83 | catch
84 | {
85 | return null;
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/NDecrypt/NDecrypt.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0
6 | Exe
7 | false
8 | true
9 | false
10 | latest
11 | enable
12 | true
13 | true
14 | 0.5.0
15 |
16 |
17 | NDecrypt
18 | Matt Nadareski
19 | DS/3DS Encryption Tool
20 | Copyright (c) Matt Nadareski 2018-2025
21 | MIT
22 | https://github.com/SabreTools/
23 | https://github.com/SabreTools/NDecrypt
24 | git
25 |
26 |
27 |
28 |
29 | win-x86;win-x64
30 |
31 |
32 | win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64
33 |
34 |
35 | win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64
36 |
37 |
38 | net6.0;net7.0;net8.0;net9.0;net10.0
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/NDecrypt/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using NDecrypt.Features;
4 | using SabreTools.CommandLine;
5 | using SabreTools.CommandLine.Features;
6 |
7 | namespace NDecrypt
8 | {
9 | class Program
10 | {
11 | public static void Main(string[] args)
12 | {
13 | // Create the command set
14 | var commandSet = CreateCommands();
15 |
16 | // If we have no args, show the help and quit
17 | if (args == null || args.Length == 0)
18 | {
19 | commandSet.OutputAllHelp();
20 | return;
21 | }
22 |
23 | // Get the first argument as a feature flag
24 | string featureName = args[0];
25 |
26 | // Get the associated feature
27 | var topLevel = commandSet.GetTopLevel(featureName);
28 | if (topLevel == null || topLevel is not Feature feature)
29 | {
30 | Console.WriteLine($"'{featureName}' is not valid feature flag");
31 | commandSet.OutputFeatureHelp(featureName);
32 | return;
33 | }
34 |
35 | // Handle default help functionality
36 | if (topLevel is Help helpFeature)
37 | {
38 | helpFeature.ProcessArgs(args, 0, commandSet);
39 | return;
40 | }
41 |
42 | // Now verify that all other flags are valid
43 | if (!feature.ProcessArgs(args, 1))
44 | return;
45 |
46 | // If inputs are required
47 | if (feature.RequiresInputs && !feature.VerifyInputs())
48 | {
49 | commandSet.OutputFeatureHelp(topLevel.Name);
50 | Environment.Exit(0);
51 | }
52 |
53 | // Now execute the current feature
54 | if (!feature.Execute())
55 | {
56 | Console.Error.WriteLine("An error occurred during processing!");
57 | commandSet.OutputFeatureHelp(topLevel.Name);
58 | }
59 | }
60 |
61 | ///
62 | /// Create the command set for the program
63 | ///
64 | private static CommandSet CreateCommands()
65 | {
66 | List header = [
67 | "Cart Image Encrypt/Decrypt Tool",
68 | string.Empty,
69 | "NDecrypt [options] ...",
70 | string.Empty,
71 | ];
72 |
73 | List footer = [
74 | string.Empty,
75 | " can be any file or folder that contains uncompressed items.",
76 | "More than one path can be specified at a time.",
77 | ];
78 |
79 | var commandSet = new CommandSet(header, footer);
80 |
81 | commandSet.Add(new Help());
82 | commandSet.Add(new EncryptFeature());
83 | commandSet.Add(new DecryptFeature());
84 | commandSet.Add(new InfoFeature());
85 |
86 | return commandSet;
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/NDecrypt.Core/PartitionKeys.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using SabreTools.Data.Models.N3DS;
3 | using SabreTools.IO.Extensions;
4 |
5 | namespace NDecrypt.Core
6 | {
7 | ///
8 | /// Set of all keys associated with a partition
9 | ///
10 | public class PartitionKeys
11 | {
12 | public byte[] KeyX { get; private set; }
13 |
14 | public byte[] KeyX2C { get; }
15 |
16 | public byte[] KeyY { get; }
17 |
18 | public byte[] NormalKey { get; private set; }
19 |
20 | public byte[] NormalKey2C { get; }
21 |
22 | ///
23 | /// Decryption args to use while processing
24 | ///
25 | private readonly DecryptArgs _decryptArgs;
26 |
27 | ///
28 | /// Indicates if development images are expected
29 | ///
30 | private readonly bool _development;
31 |
32 | ///
33 | /// Create a new set of keys for a given partition
34 | ///
35 | /// Decryption args representing available keys
36 | /// RSA-2048 signature from the partition
37 | /// BitMasks from the partition or backup header
38 | /// CryptoMethod from the partition or backup header
39 | /// Determine if development keys are used
40 | public PartitionKeys(DecryptArgs args, byte[]? signature, BitMasks masks, CryptoMethod method, bool development)
41 | {
42 | // Validate inputs
43 | if (args.IsReady != true)
44 | throw new InvalidOperationException($"{nameof(args)} must be initialized before use");
45 | if (signature != null && signature.Length < 16)
46 | throw new ArgumentOutOfRangeException(nameof(signature), $"{nameof(signature)} must be at least 16 bytes");
47 |
48 | // Set fields for future use
49 | _decryptArgs = args;
50 | _development = development;
51 |
52 | // Set the standard KeyX values
53 | KeyX = new byte[16];
54 | KeyX2C = development ? args.DevKeyX0x2C : args.KeyX0x2C;
55 |
56 | // Backup headers can't have a KeyY value set
57 | KeyY = new byte[16];
58 | if (signature != null)
59 | Array.Copy(signature, KeyY, 16);
60 |
61 | // Set the standard normal key values
62 | NormalKey = new byte[16];
63 |
64 | NormalKey2C = KeyX2C.RotateLeft(2);
65 | NormalKey2C = NormalKey2C.Xor(KeyY);
66 | NormalKey2C = NormalKey2C.Add(add: args.AESHardwareConstant);
67 | NormalKey2C = NormalKey2C.RotateLeft(87);
68 |
69 | // Special case for zero-key
70 | #if NET20 || NET35
71 | if ((masks & BitMasks.FixedCryptoKey) > 0)
72 | #else
73 | if (masks.HasFlag(BitMasks.FixedCryptoKey))
74 | #endif
75 | {
76 | Console.WriteLine("Encryption Method: Zero Key");
77 | NormalKey = new byte[16];
78 | NormalKey2C = new byte[16];
79 | return;
80 | }
81 |
82 | // Set KeyX values based on crypto method
83 | switch (method)
84 | {
85 | case CryptoMethod.Original:
86 | Console.WriteLine("Encryption Method: Key 0x2C");
87 | KeyX = development ? args.DevKeyX0x2C : args.KeyX0x2C;
88 | break;
89 |
90 | case CryptoMethod.Seven:
91 | Console.WriteLine("Encryption Method: Key 0x25");
92 | KeyX = development ? args.DevKeyX0x25 : args.KeyX0x25;
93 | break;
94 |
95 | case CryptoMethod.NineThree:
96 | Console.WriteLine("Encryption Method: Key 0x18");
97 | KeyX = development ? args.DevKeyX0x18 : args.KeyX0x18;
98 | break;
99 |
100 | case CryptoMethod.NineSix:
101 | Console.WriteLine("Encryption Method: Key 0x1B");
102 | KeyX = development ? args.DevKeyX0x1B : args.KeyX0x1B;
103 | break;
104 | }
105 |
106 | // Set the normal key based on the new KeyX value
107 | NormalKey = KeyX.RotateLeft(2);
108 | NormalKey = NormalKey.Xor(KeyY);
109 | NormalKey = NormalKey.Add(args.AESHardwareConstant);
110 | NormalKey = NormalKey.RotateLeft(87);
111 | }
112 |
113 | ///
114 | /// Set RomFS values based on the bit masks
115 | ///
116 | public void SetRomFSValues(BitMasks masks)
117 | {
118 | // NormalKey has a constant value for zero-key
119 | #if NET20 || NET35
120 | if ((masks & BitMasks.FixedCryptoKey) > 0)
121 | #else
122 | if (masks.HasFlag(BitMasks.FixedCryptoKey))
123 | #endif
124 | {
125 | NormalKey = new byte[16];
126 | return;
127 | }
128 |
129 | // Encrypting RomFS for partitions 1 and up always use Key0x2C
130 | KeyX = _development ? _decryptArgs.DevKeyX0x2C : _decryptArgs.KeyX0x2C;
131 |
132 | NormalKey = KeyX.RotateLeft(2);
133 | NormalKey = NormalKey.Xor(KeyY);
134 | NormalKey = NormalKey.Add(_decryptArgs.AESHardwareConstant);
135 | NormalKey = NormalKey.RotateLeft(87);
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | project.fragment.lock.json
46 | artifacts/
47 |
48 | *_i.c
49 | *_p.c
50 | *_i.h
51 | *.ilk
52 | *.meta
53 | *.obj
54 | *.pch
55 | *.pdb
56 | *.pgc
57 | *.pgd
58 | *.rsp
59 | *.sbr
60 | *.tlb
61 | *.tli
62 | *.tlh
63 | *.tmp
64 | *.tmp_proj
65 | *.log
66 | *.vspscc
67 | *.vssscc
68 | .builds
69 | *.pidb
70 | *.svclog
71 | *.scc
72 |
73 | # Chutzpah Test files
74 | _Chutzpah*
75 |
76 | # Visual C++ cache files
77 | ipch/
78 | *.aps
79 | *.ncb
80 | *.opendb
81 | *.opensdf
82 | *.sdf
83 | *.cachefile
84 | *.VC.db
85 | *.VC.VC.opendb
86 |
87 | # Visual Studio profiler
88 | *.psess
89 | *.vsp
90 | *.vspx
91 | *.sap
92 |
93 | # TFS 2012 Local Workspace
94 | $tf/
95 |
96 | # Guidance Automation Toolkit
97 | *.gpState
98 |
99 | # ReSharper is a .NET coding add-in
100 | _ReSharper*/
101 | *.[Rr]e[Ss]harper
102 | *.DotSettings.user
103 |
104 | # JustCode is a .NET coding add-in
105 | .JustCode
106 |
107 | # TeamCity is a build add-in
108 | _TeamCity*
109 |
110 | # DotCover is a Code Coverage Tool
111 | *.dotCover
112 |
113 | # NCrunch
114 | _NCrunch_*
115 | .*crunch*.local.xml
116 | nCrunchTemp_*
117 |
118 | # MightyMoose
119 | *.mm.*
120 | AutoTest.Net/
121 |
122 | # Web workbench (sass)
123 | .sass-cache/
124 |
125 | # Installshield output folder
126 | [Ee]xpress/
127 |
128 | # DocProject is a documentation generator add-in
129 | DocProject/buildhelp/
130 | DocProject/Help/*.HxT
131 | DocProject/Help/*.HxC
132 | DocProject/Help/*.hhc
133 | DocProject/Help/*.hhk
134 | DocProject/Help/*.hhp
135 | DocProject/Help/Html2
136 | DocProject/Help/html
137 |
138 | # Click-Once directory
139 | publish/
140 |
141 | # Publish Web Output
142 | *.[Pp]ublish.xml
143 | *.azurePubxml
144 | # TODO: Comment the next line if you want to checkin your web deploy settings
145 | # but database connection strings (with potential passwords) will be unencrypted
146 | #*.pubxml
147 | *.publishproj
148 |
149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
150 | # checkin your Azure Web App publish settings, but sensitive information contained
151 | # in these scripts will be unencrypted
152 | PublishScripts/
153 |
154 | # NuGet Packages
155 | *.nupkg
156 | # The packages folder can be ignored because of Package Restore
157 | **/packages/*
158 | # except build/, which is used as an MSBuild target.
159 | !**/packages/build/
160 | # Uncomment if necessary however generally it will be regenerated when needed
161 | #!**/packages/repositories.config
162 | # NuGet v3's project.json files produces more ignoreable files
163 | *.nuget.props
164 | *.nuget.targets
165 |
166 | # Microsoft Azure Build Output
167 | csx/
168 | *.build.csdef
169 |
170 | # Microsoft Azure Emulator
171 | ecf/
172 | rcf/
173 |
174 | # Windows Store app package directories and files
175 | AppPackages/
176 | BundleArtifacts/
177 | Package.StoreAssociation.xml
178 | _pkginfo.txt
179 |
180 | # Visual Studio cache files
181 | # files ending in .cache can be ignored
182 | *.[Cc]ache
183 | # but keep track of directories ending in .cache
184 | !*.[Cc]ache/
185 |
186 | # Others
187 | ClientBin/
188 | ~$*
189 | *~
190 | *.dbmdl
191 | *.dbproj.schemaview
192 | *.jfm
193 | *.pfx
194 | *.publishsettings
195 | node_modules/
196 | orleans.codegen.cs
197 |
198 | # Since there are multiple workflows, uncomment next line to ignore bower_components
199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
200 | #bower_components/
201 |
202 | # RIA/Silverlight projects
203 | Generated_Code/
204 |
205 | # Backup & report files from converting an old project file
206 | # to a newer Visual Studio version. Backup files are not needed,
207 | # because we have git ;-)
208 | _UpgradeReport_Files/
209 | Backup*/
210 | UpgradeLog*.XML
211 | UpgradeLog*.htm
212 |
213 | # SQL Server files
214 | *.mdf
215 | *.ldf
216 |
217 | # Business Intelligence projects
218 | *.rdl.data
219 | *.bim.layout
220 | *.bim_*.settings
221 |
222 | # Microsoft Fakes
223 | FakesAssemblies/
224 |
225 | # GhostDoc plugin setting file
226 | *.GhostDoc.xml
227 |
228 | # Node.js Tools for Visual Studio
229 | .ntvs_analysis.dat
230 |
231 | # Visual Studio 6 build log
232 | *.plg
233 |
234 | # Visual Studio 6 workspace options file
235 | *.opt
236 |
237 | # Visual Studio LightSwitch build output
238 | **/*.HTMLClient/GeneratedArtifacts
239 | **/*.DesktopClient/GeneratedArtifacts
240 | **/*.DesktopClient/ModelManifest.xml
241 | **/*.Server/GeneratedArtifacts
242 | **/*.Server/ModelManifest.xml
243 | _Pvt_Extensions
244 |
245 | # Paket dependency manager
246 | .paket/paket.exe
247 | paket-files/
248 |
249 | # FAKE - F# Make
250 | .fake/
251 |
252 | # JetBrains Rider
253 | .idea/
254 | *.sln.iml
255 |
256 | # CodeRush
257 | .cr/
258 |
259 | # Python Tools for Visual Studio (PTVS)
260 | __pycache__/
261 | *.pyc
262 |
263 | # VSCode
264 | /NDecrypt/Properties/launchSettings.json
265 |
--------------------------------------------------------------------------------
/publish-win.ps1:
--------------------------------------------------------------------------------
1 | # This batch file assumes the following:
2 | # - .NET 9.0 (or newer) SDK is installed and in PATH
3 | # - 7-zip commandline (7z.exe) is installed and in PATH
4 | # - Git for Windows is installed and in PATH
5 | #
6 | # If any of these are not satisfied, the operation may fail
7 | # in an unpredictable way and result in an incomplete output.
8 |
9 | # Optional parameters
10 | param(
11 | [Parameter(Mandatory = $false)]
12 | [Alias("UseAll")]
13 | [switch]$USE_ALL,
14 |
15 | [Parameter(Mandatory = $false)]
16 | [Alias("IncludeDebug")]
17 | [switch]$INCLUDE_DEBUG,
18 |
19 | [Parameter(Mandatory = $false)]
20 | [Alias("NoBuild")]
21 | [switch]$NO_BUILD,
22 |
23 | [Parameter(Mandatory = $false)]
24 | [Alias("NoArchive")]
25 | [switch]$NO_ARCHIVE
26 | )
27 |
28 | # Set the current directory as a variable
29 | $BUILD_FOLDER = $PSScriptRoot
30 |
31 | # Set the current commit hash
32 | $COMMIT = git log --pretty=format:"%H" -1
33 |
34 | # Output the selected options
35 | Write-Host "Selected Options:"
36 | Write-Host " Use all frameworks (-UseAll) $USE_ALL"
37 | Write-Host " Include debug builds (-IncludeDebug) $INCLUDE_DEBUG"
38 | Write-Host " No build (-NoBuild) $NO_BUILD"
39 | Write-Host " No archive (-NoArchive) $NO_ARCHIVE"
40 | Write-Host " "
41 |
42 | # Create the build matrix arrays
43 | $FRAMEWORKS = @('net10.0')
44 | $RUNTIMES = @('win-x86', 'win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')
45 |
46 | # Use expanded lists, if requested
47 | if ($USE_ALL.IsPresent) {
48 | $FRAMEWORKS = @('net20', 'net35', 'net40', 'net452', 'net462', 'net472', 'net48', 'netcoreapp3.1', 'net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0')
49 | }
50 |
51 | # Create the filter arrays
52 | $SINGLE_FILE_CAPABLE = @('net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0')
53 | $VALID_APPLE_FRAMEWORKS = @('net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0')
54 | $VALID_CROSS_PLATFORM_FRAMEWORKS = @('netcoreapp3.1', 'net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0')
55 | $VALID_CROSS_PLATFORM_RUNTIMES = @('win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')
56 |
57 | # Only build if requested
58 | if (!$NO_BUILD.IsPresent) {
59 | # Restore Nuget packages for all builds
60 | Write-Host "Restoring Nuget packages"
61 | dotnet restore
62 |
63 | # Create Nuget Package
64 | dotnet pack NDecrypt.Core\NDecrypt.Core.csproj --output $BUILD_FOLDER
65 |
66 | # Build Program
67 | foreach ($FRAMEWORK in $FRAMEWORKS) {
68 | foreach ($RUNTIME in $RUNTIMES) {
69 | # Output the current build
70 | Write-Host "===== Build Program - $FRAMEWORK, $RUNTIME ====="
71 |
72 | # If we have an invalid combination of framework and runtime
73 | if ($VALID_CROSS_PLATFORM_FRAMEWORKS -notcontains $FRAMEWORK -and $VALID_CROSS_PLATFORM_RUNTIMES -contains $RUNTIME) {
74 | Write-Host "Skipped due to invalid combination"
75 | continue
76 | }
77 |
78 | # If we have Apple silicon but an unsupported framework
79 | if ($VALID_APPLE_FRAMEWORKS -notcontains $FRAMEWORK -and $RUNTIME -eq 'osx-arm64') {
80 | Write-Host "Skipped due to no Apple Silicon support"
81 | continue
82 | }
83 |
84 | # Only .NET 5 and above can publish to a single file
85 | if ($SINGLE_FILE_CAPABLE -contains $FRAMEWORK) {
86 | # Only include Debug if set
87 | if ($INCLUDE_DEBUG.IsPresent) {
88 | dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true
89 | }
90 | dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false
91 | }
92 | else {
93 | # Only include Debug if set
94 | if ($INCLUDE_DEBUG.IsPresent) {
95 | dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT
96 | }
97 | dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:DebugType=None -p:DebugSymbols=false
98 | }
99 | }
100 | }
101 | }
102 |
103 | # Only create archives if requested
104 | if (!$NO_ARCHIVE.IsPresent) {
105 | # Create Program archives
106 | foreach ($FRAMEWORK in $FRAMEWORKS) {
107 | foreach ($RUNTIME in $RUNTIMES) {
108 | # Output the current build
109 | Write-Host "===== Archive Program - $FRAMEWORK, $RUNTIME ====="
110 |
111 | # If we have an invalid combination of framework and runtime
112 | if ($VALID_CROSS_PLATFORM_FRAMEWORKS -notcontains $FRAMEWORK -and $VALID_CROSS_PLATFORM_RUNTIMES -contains $RUNTIME) {
113 | Write-Host "Skipped due to invalid combination"
114 | continue
115 | }
116 |
117 | # If we have Apple silicon but an unsupported framework
118 | if ($VALID_APPLE_FRAMEWORKS -notcontains $FRAMEWORK -and $RUNTIME -eq 'osx-arm64') {
119 | Write-Host "Skipped due to no Apple Silicon support"
120 | continue
121 | }
122 |
123 | # Only include Debug if set
124 | if ($INCLUDE_DEBUG.IsPresent) {
125 | Set-Location -Path $BUILD_FOLDER\NDecrypt\bin\Debug\${FRAMEWORK}\${RUNTIME}\publish\
126 | 7z a -tzip $BUILD_FOLDER\NDecrypt_${FRAMEWORK}_${RUNTIME}_debug.zip *
127 | }
128 |
129 | Set-Location -Path $BUILD_FOLDER\NDecrypt\bin\Release\${FRAMEWORK}\${RUNTIME}\publish\
130 | 7z a -tzip $BUILD_FOLDER\NDecrypt_${FRAMEWORK}_${RUNTIME}_release.zip *
131 | }
132 | }
133 |
134 | # Reset the directory
135 | Set-Location -Path $PSScriptRoot
136 | }
137 |
--------------------------------------------------------------------------------
/NDecrypt.Core/DSTool.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | #if NETFRAMEWORK || NETSTANDARD2_0_OR_GREATER
5 | using SabreTools.IO.Extensions;
6 | #endif
7 | using SabreTools.Serialization.Wrappers;
8 |
9 | namespace NDecrypt.Core
10 | {
11 | public class DSTool : ITool
12 | {
13 | ///
14 | /// Decryption args to use while processing
15 | ///
16 | private readonly DecryptArgs _decryptArgs;
17 |
18 | public DSTool(DecryptArgs decryptArgs)
19 | {
20 | _decryptArgs = decryptArgs;
21 | }
22 |
23 | #region Encrypt
24 |
25 | ///
26 | public bool EncryptFile(string input, string? output, bool force)
27 | {
28 | try
29 | {
30 | // If the output is provided, copy the input file
31 | if (output != null)
32 | File.Copy(input, output, overwrite: true);
33 | else
34 | output = input;
35 |
36 | // Open the output file for processing
37 | using var reader = File.Open(output, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
38 |
39 | // Deserialize the cart information
40 | var nitro = Nitro.Create(reader);
41 | if (nitro == null)
42 | {
43 | Console.WriteLine("Error: Not a DS or DSi Rom!");
44 | return false;
45 | }
46 |
47 | // Ensure the secure area was read
48 | if (nitro.SecureArea == null)
49 | {
50 | Console.WriteLine("Error: Invalid secure area!");
51 | return false;
52 | }
53 |
54 | // Encrypt the secure area
55 | nitro.EncryptSecureArea(_decryptArgs.NitroEncryptionData, force);
56 |
57 | // Write the encrypted secure area
58 | using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
59 | writer.Seek(0x4000, SeekOrigin.Begin);
60 | writer.Write(nitro.SecureArea);
61 | writer.Flush();
62 |
63 | return true;
64 | }
65 | catch
66 | {
67 | Console.WriteLine($"An error has occurred. {output} may be corrupted if it was partially processed.");
68 | Console.WriteLine("Please check that the file was a valid DS or DSi file and try again.");
69 | return false;
70 | }
71 | }
72 |
73 | #endregion
74 |
75 | #region Decrypt
76 |
77 | ///
78 | public bool DecryptFile(string input, string? output, bool force)
79 | {
80 | try
81 | {
82 | // If the output is provided, copy the input file
83 | if (output != null)
84 | File.Copy(input, output, overwrite: true);
85 | else
86 | output = input;
87 |
88 | // Open the read and write on the same file for inplace processing
89 | using var reader = File.Open(output, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
90 |
91 | // Deserialize the cart information
92 | var nitro = Nitro.Create(reader);
93 | if (nitro == null)
94 | {
95 | Console.WriteLine("Error: Not a DS or DSi Rom!");
96 | return false;
97 | }
98 |
99 | // Ensure the secure area was read
100 | if (nitro.SecureArea == null)
101 | {
102 | Console.WriteLine("Error: Invalid secure area!");
103 | return false;
104 | }
105 |
106 | // Decrypt the secure area
107 | nitro.DecryptSecureArea(_decryptArgs.NitroEncryptionData, force);
108 |
109 | // Write the decrypted secure area
110 | using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
111 | writer.Seek(0x4000, SeekOrigin.Begin);
112 | writer.Write(nitro.SecureArea);
113 | writer.Flush();
114 |
115 | return true;
116 | }
117 | catch
118 | {
119 | Console.WriteLine($"An error has occurred. {output} may be corrupted if it was partially processed.");
120 | Console.WriteLine("Please check that the file was a valid DS or DSi file and try again.");
121 | return false;
122 | }
123 | }
124 |
125 | #endregion
126 |
127 | #region Info
128 |
129 | ///
130 | public string? GetInformation(string filename)
131 | {
132 | try
133 | {
134 | // Open the file for reading
135 | using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
136 |
137 | // Deserialize the cart information
138 | var cart = Nitro.Create(input);
139 | if (cart?.Model == null)
140 | return "Error: Not a DS/DSi cart image!";
141 |
142 | // Get a string builder for the status
143 | var sb = new StringBuilder();
144 | sb.Append("\tSecure Area: ");
145 |
146 | // Get the encryption status
147 | bool? decrypted = cart.CheckIfDecrypted(out _);
148 | if (decrypted == null)
149 | sb.Append("Empty");
150 | else if (decrypted == true)
151 | sb.Append("Decrypted");
152 | else
153 | sb.Append("Encrypted");
154 |
155 | // Return the status for the secure area
156 | sb.Append(Environment.NewLine);
157 | return sb.ToString();
158 | }
159 | catch (Exception ex)
160 | {
161 | Console.WriteLine(ex);
162 | return null;
163 | }
164 | }
165 |
166 | #endregion
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/publish-nix.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This batch file assumes the following:
4 | # - .NET 9.0 (or newer) SDK is installed and in PATH
5 | # - zip is installed and in PATH
6 | # - Git is installed and in PATH
7 | #
8 | # If any of these are not satisfied, the operation may fail
9 | # in an unpredictable way and result in an incomplete output.
10 |
11 | # Optional parameters
12 | USE_ALL=false
13 | INCLUDE_DEBUG=false
14 | NO_BUILD=false
15 | NO_ARCHIVE=false
16 | while getopts "udba" OPTION; do
17 | case $OPTION in
18 | u)
19 | USE_ALL=true
20 | ;;
21 | d)
22 | INCLUDE_DEBUG=true
23 | ;;
24 | b)
25 | NO_BUILD=true
26 | ;;
27 | a)
28 | NO_ARCHIVE=true
29 | ;;
30 | *)
31 | echo "Invalid option provided"
32 | exit 1
33 | ;;
34 | esac
35 | done
36 |
37 | # Set the current directory as a variable
38 | BUILD_FOLDER=$PWD
39 |
40 | # Set the current commit hash
41 | COMMIT=$(git log --pretty=%H -1)
42 |
43 | # Output the selected options
44 | echo "Selected Options:"
45 | echo " Use all frameworks (-u) $USE_ALL"
46 | echo " Include debug builds (-d) $INCLUDE_DEBUG"
47 | echo " No build (-b) $NO_BUILD"
48 | echo " No archive (-a) $NO_ARCHIVE"
49 | echo " "
50 |
51 | # Create the build matrix arrays
52 | FRAMEWORKS=("net10.0")
53 | RUNTIMES=("win-x86" "win-x64" "win-arm64" "linux-x64" "linux-arm64" "osx-x64" "osx-arm64")
54 |
55 | # Use expanded lists, if requested
56 | if [ $USE_ALL = true ]; then
57 | FRAMEWORKS=("net20" "net35" "net40" "net452" "net462" "net472" "net48" "netcoreapp3.1" "net5.0" "net6.0" "net7.0" "net8.0" "net9.0" "net10.0")
58 | fi
59 |
60 | # Create the filter arrays
61 | SINGLE_FILE_CAPABLE=("net5.0" "net6.0" "net7.0" "net8.0" "net9.0" "net10.0")
62 | VALID_APPLE_FRAMEWORKS=("net6.0" "net7.0" "net8.0" "net9.0" "net10.0")
63 | VALID_CROSS_PLATFORM_FRAMEWORKS=("netcoreapp3.1" "net5.0" "net6.0" "net7.0" "net8.0" "net9.0" "net10.0")
64 | VALID_CROSS_PLATFORM_RUNTIMES=("win-arm64" "linux-x64" "linux-arm64" "osx-x64" "osx-arm64")
65 |
66 | # Only build if requested
67 | if [ $NO_BUILD = false ]; then
68 | # Restore Nuget packages for all builds
69 | echo "Restoring Nuget packages"
70 | dotnet restore
71 |
72 | # Create Nuget Packages
73 | dotnet pack NDecrypt.Core/NDecrypt.Core.csproj --output $BUILD_FOLDER
74 |
75 | # Build Program
76 | for FRAMEWORK in "${FRAMEWORKS[@]}"; do
77 | for RUNTIME in "${RUNTIMES[@]}"; do
78 | # Output the current build
79 | echo "===== Build Program - $FRAMEWORK, $RUNTIME ====="
80 |
81 | # If we have an invalid combination of framework and runtime
82 | if [[ ! $(echo ${VALID_CROSS_PLATFORM_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then
83 | if [[ $(echo ${VALID_CROSS_PLATFORM_RUNTIMES[@]} | fgrep -w $RUNTIME) ]]; then
84 | echo "Skipped due to invalid combination"
85 | continue
86 | fi
87 | fi
88 |
89 | # If we have Apple silicon but an unsupported framework
90 | if [[ ! $(echo ${VALID_APPLE_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then
91 | if [ $RUNTIME = "osx-arm64" ]; then
92 | echo "Skipped due to no Apple Silicon support"
93 | continue
94 | fi
95 | fi
96 |
97 | # Only .NET 5 and above can publish to a single file
98 | if [[ $(echo ${SINGLE_FILE_CAPABLE[@]} | fgrep -w $FRAMEWORK) ]]; then
99 | # Only include Debug if set
100 | if [ $INCLUDE_DEBUG = true ]; then
101 | dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true
102 | fi
103 | dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false
104 | else
105 | # Only include Debug if set
106 | if [ $INCLUDE_DEBUG = true ]; then
107 | dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT
108 | fi
109 | dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:DebugType=None -p:DebugSymbols=false
110 | fi
111 | done
112 | done
113 | fi
114 |
115 | # Only create archives if requested
116 | if [ $NO_ARCHIVE = false ]; then
117 | # Create Test archives
118 | for FRAMEWORK in "${FRAMEWORKS[@]}"; do
119 | for RUNTIME in "${RUNTIMES[@]}"; do
120 | # Output the current build
121 | echo "===== Archive Program - $FRAMEWORK, $RUNTIME ====="
122 |
123 | # If we have an invalid combination of framework and runtime
124 | if [[ ! $(echo ${VALID_CROSS_PLATFORM_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then
125 | if [[ $(echo ${VALID_CROSS_PLATFORM_RUNTIMES[@]} | fgrep -w $RUNTIME) ]]; then
126 | echo "Skipped due to invalid combination"
127 | continue
128 | fi
129 | fi
130 |
131 | # If we have Apple silicon but an unsupported framework
132 | if [[ ! $(echo ${VALID_APPLE_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then
133 | if [ $RUNTIME = "osx-arm64" ]; then
134 | echo "Skipped due to no Apple Silicon support"
135 | continue
136 | fi
137 | fi
138 |
139 | # Only include Debug if set
140 | if [ $INCLUDE_DEBUG = true ]; then
141 | cd $BUILD_FOLDER/NDecrypt/bin/Debug/${FRAMEWORK}/${RUNTIME}/publish/
142 | zip -r $BUILD_FOLDER/NDecrypt_${FRAMEWORK}_${RUNTIME}_debug.zip .
143 | fi
144 | cd $BUILD_FOLDER/NDecrypt/bin/Release/${FRAMEWORK}/${RUNTIME}/publish/
145 | zip -r $BUILD_FOLDER/NDecrypt_${FRAMEWORK}_${RUNTIME}_release.zip .
146 | done
147 | done
148 |
149 | # Reset the directory
150 | cd $BUILD_FOLDER
151 | fi
152 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NDecrypt
2 |
3 | [](https://github.com/SabreTools/NDecrypt/actions/workflows/build_and_test.yml)
4 |
5 | A simple tool for simple people.
6 |
7 | ## What is this?
8 |
9 | This is a code port of 3 different programs:
10 |
11 | - `3ds_encrypt.py`
12 | - `3ds_decrypt.py`
13 | - `woodsec` (part of [wooddumper](https://github.com/TuxSH/wooddumper))
14 |
15 | ## No really, what is this?
16 |
17 | This tool allows you to encrypt and decrypt your personally dumped Nintendo DS, 3DS, and New 3DS cart images with minimal hassle.
18 |
19 | ## Where do I find it?
20 |
21 | For the most recent stable build, download the latest release here: [Releases Page](https://github.com/SabreTools/NDecrypt/releases)
22 |
23 | For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/NDecrypt/releases/tag/rolling)
24 |
25 | ## So how do I use this?
26 |
27 | Usage: NDecrypt [flags] ...
28 |
29 | Possible values for :
30 | e, encrypt - Encrypt the input files
31 | d, decrypt - Decrypt the input files
32 | i, info - Output file information
33 |
34 | Possible values for [flags] (one or more can be used):
35 | -?, -h, --help Display this help text and quit
36 | -c, --config Path to config.json
37 | -d, --development Enable using development keys, if available
38 | -f, --force Force operation by avoiding sanity checks
39 | --hash Output size and hashes to a companion file
40 |
41 | can be any file or folder that contains uncompressed items.
42 | More than one path can be specified at a time.
43 |
44 | ### Additional Notes
45 |
46 | - Input files are overwritten, even if they are only partially processed. You should make backups of the files you're working on if you're worried about this.
47 | - 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.
48 | - Required files will automatically be searched for in the application runtime directory as well as `%HOME%/.config/ndecrypt`, also known as `%USERPROFILE%\.config\ndecrypt` on Windows.
49 |
50 | ## I feel like something is missing
51 |
52 | There is a major file that you can use to give NDecrypt that extra _oomph_ of functionality that it really needs. That is, you can't do any encryption or decryption without it present. I can't give you the files and I can't generate them for you on the fly with the correct values. Keys are a thorny thing and I just do not want to deal with them. Values are validated, at least, but you'll only get yelled at on run if one of them is wrong. Don't worry, they're just disabled, not removed.
53 |
54 | This convenient table gives an overview of mappings between the current `config.json` type along with the 2 formerly-supported types and a completely unsupported but common type.
55 |
56 | | `config.json` | `keys.bin` order | `aes_keys.txt` | rom-properties `keys.conf` |
57 | | --- | --- | --- | --- |
58 | | `NitroEncryptionData` | **N/A** | **UNMAPPED** | **UNMAPPED** |
59 | | `AESHardwareConstant` | 1 | `generator` | `ctr-scrambler` |
60 | | `KeyX0x18` | 2 | `slot0x18KeyX` | `ctr-Slot0x18KeyX` |
61 | | `KeyX0x1B` | 3 | `slot0x1BKeyX` | `ctr-Slot0x1BKeyX` |
62 | | `KeyX0x25` | 4 | `slot0x25KeyX` | `ctr-Slot0x25KeyX` |
63 | | `KeyX0x2C` | 5 | `slot0x2CKeyX` | `ctr-Slot0x2CKeyX` |
64 | | `DevKeyX0x18` | 6 | **UNMAPPED** | `ctr-dev-Slot0x18KeyX` |
65 | | `DevKeyX0x1B` | 7 | **UNMAPPED** | `ctr-dev-Slot0x1BKeyX` |
66 | | `DevKeyX0x25` | 8 | **UNMAPPED** | `ctr-dev-Slot0x25KeyX` |
67 | | `DevKeyX0x2C` | 9 | **UNMAPPED** | `ctr-dev-Slot0x2CKeyX` |
68 |
69 | **Note:** `Dev*` keys are not required for the vast majority of normal operations. They're only used if the `-d` option is included. Working with your own retail carts will pretty much never require these, so don't drive yourself silly dealing with them.
70 |
71 | **Note:** The `NitroEncryptionData` field is also known as the "Blowfish table" for Nintendo DS carts. It's stored in the same hex string format as the other keys. There's some complicated stuff about how it's used and where it's stored, but all you need to know is that it is required.
72 |
73 | **Community Note:** If you have used previous versions of NDecrypt and already have either `keys.bin` or `aes_keys.txt`, consider using [this helpful community-made script](https://gist.github.com/Dimensional/82f212a0b35bcf9caaa2bc9a70b3a92a) to make your life a bit easier. It will convert them into the new `config.json` format that will be supported from here on out.
74 |
75 | ### `config.json`
76 |
77 | The up-and-coming, shiny, new, exciting, JSON-based format for storing the encryption keys that you need for Nintendo DS, 3DS, and New 3DS. This JSON file is not generated by anything, but maps pretty much one-to-one with the code inside of NDecrypt, making it super convenient to use. Keys provided need to be hex strings (e.g. `"AABBCCDD"`). Any keys that are left with `null` or `""` as the value will be ignored. See [the sample config](https://github.com/SabreTools/NDecrypt/blob/master/config-default.json) that I've nicely generated for you. You're welcome.
78 |
79 | In the future, this file will be automatically generated on first run along with some cutesy little message telling you to fill it out when you get a chance. It's not doing it right now because I don't want to confuse users. Including those reading this. How meta.
80 |
81 | ### `keys.bin` (Deprecated)
82 |
83 | This is the OG of NDecrypt key file formats. It's a weird, binary blob of a format that is composed of little-endian values (most common extraction methods produce big endian, so keep that in mind). It's only compatible wtih Nintendo 3DS and New 3DS keys and is incredibly inflexible in its layout. The little-endianness of it is a relic of how keys were handled in-code previously and I really can't fix it now. If you don't have a key, it needs to be filled with `0x00` bytes so it doesn't mess up the read. Yeah.
84 |
85 | Oddly, this gets confused with some similar format that GodMode9 works with, but it has nothing to do with it. If you try to use one of those files in place of this one, something will probably break. It wasn't intentional, I just didn't look ahead of time. See the table in the main part of this section for the order the keys need to be stored in.
86 |
87 | ### `aes_keys.txt` (Deprecated)
88 |
89 | This is an INI-based format that was super popular among 3DS emulators and probably still is. Weird thing, I know, but just roll with it please.
90 |
91 | ## But does it work?
92 |
93 | 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):
94 |
95 | - **Nintendo DS** - >99% compatible (Both encryption and decryption)
96 | - **Nintendo DSi** - 100% compatible (Both encryption and decryption)
97 | - **Nintendo 3DS** - 100% compatible (Both encryption and decryption)
98 | - **Nintendo New 3DS** - 100% compatible (Both encryption and decryption)
99 |
100 | 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.
101 |
102 | ## Anything else?
103 |
104 | 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.
105 |
106 | ## Disclaimer
107 |
108 | 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.
109 |
--------------------------------------------------------------------------------
/NDecrypt/Features/BaseFeature.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using NDecrypt.Core;
5 | using SabreTools.CommandLine;
6 | using SabreTools.CommandLine.Inputs;
7 |
8 | namespace NDecrypt.Features
9 | {
10 | internal abstract class BaseFeature : Feature
11 | {
12 | #region Common Inputs
13 |
14 | protected const string ConfigName = "config";
15 | protected readonly StringInput ConfigString = new(ConfigName, ["-c", "--config"], "Path to config.json");
16 |
17 | protected const string DevelopmentName = "development";
18 | protected readonly FlagInput DevelopmentFlag = new(DevelopmentName, ["-d", "--development"], "Enable using development keys, if available");
19 |
20 | protected const string ForceName = "force";
21 | protected readonly FlagInput ForceFlag = new(ForceName, ["-f", "--force"], "Force operation by avoiding sanity checks");
22 |
23 | protected const string HashName = "hash";
24 | protected readonly FlagInput HashFlag = new(HashName, "--hash", "Output size and hashes to a companion file");
25 |
26 | protected const string OverwriteName = "overwrite";
27 | protected readonly FlagInput OverwriteFlag = new(OverwriteName, ["-o", "--overwrite"], "Overwrite input files instead of creating new ones");
28 |
29 | #endregion
30 |
31 | ///
32 | /// Mapping of reusable tools
33 | ///
34 | private readonly Dictionary _tools = [];
35 |
36 | protected BaseFeature(string name, string[] flags, string description, string? detailed = null)
37 | : base(name, flags, description, detailed)
38 | {
39 | }
40 |
41 | ///
42 | public override bool Execute()
43 | {
44 | // Initialize required pieces
45 | InitializeTools();
46 |
47 | for (int i = 0; i < Inputs.Count; i++)
48 | {
49 | if (File.Exists(Inputs[i]))
50 | {
51 | ProcessFile(Inputs[i]);
52 | }
53 | else if (Directory.Exists(Inputs[i]))
54 | {
55 | foreach (string file in Directory.GetFiles(Inputs[i], "*", SearchOption.AllDirectories))
56 | {
57 | ProcessFile(file);
58 | }
59 | }
60 | else
61 | {
62 | Console.WriteLine($"{Inputs[i]} is not a file or folder. Please check your spelling and formatting and try again.");
63 | }
64 | }
65 |
66 | return true;
67 | }
68 |
69 | ///
70 | public override bool VerifyInputs() => Inputs.Count > 0;
71 |
72 | ///
73 | /// Process a single file path
74 | ///
75 | /// File path to process
76 | protected abstract void ProcessFile(string input);
77 |
78 | ///
79 | /// Initialize the tools to be used by the feature
80 | ///
81 | private void InitializeTools()
82 | {
83 |
84 | var decryptArgs = new DecryptArgs(GetString(ConfigName, "config.json"));
85 | _tools[FileType.NDS] = new DSTool(decryptArgs);
86 | _tools[FileType.N3DS] = new ThreeDSTool(GetBoolean(DevelopmentName), decryptArgs);
87 | }
88 |
89 | ///
90 | /// Derive the encryption tool to be used for the given file
91 | ///
92 | /// Filename to derive the tool from
93 | protected ITool? DeriveTool(string filename)
94 | {
95 | if (!File.Exists(filename))
96 | {
97 | Console.WriteLine($"{filename} does not exist! Skipping...");
98 | return null;
99 | }
100 |
101 | FileType type = DetermineFileType(filename);
102 | return type switch
103 | {
104 | FileType.NDS => _tools[FileType.NDS],
105 | FileType.NDSi => _tools[FileType.NDS],
106 | FileType.iQueDS => _tools[FileType.NDS],
107 | FileType.N3DS => _tools[FileType.N3DS],
108 | _ => null,
109 | };
110 | }
111 |
112 | ///
113 | /// Derive an output filename from the input, if possible
114 | ///
115 | /// Name of the input file to derive from
116 | /// Preferred extension set by the feature implementation
117 | /// Output filename based on the input
118 | protected static string GetOutputFile(string filename, string extension)
119 | {
120 | // Empty filenames are passed back
121 | if (filename.Length == 0)
122 | return filename;
123 |
124 | // TODO: Replace the suffix instead of just appending
125 | // TODO: Ensure that the input and output aren't the same
126 |
127 | // If the extension does not include a leading period
128 | #if NETCOREAPP || NETSTANDARD2_0_OR_GREATER
129 | if (!extension.StartsWith('.'))
130 | #else
131 | if (!extension.StartsWith("."))
132 | #endif
133 | extension = $".{extension}";
134 |
135 | // Append the extension and return
136 | return $"{filename}{extension}";
137 | }
138 |
139 | ///
140 | /// Write out the hashes of a file to a named file
141 | ///
142 | /// Filename to get hashes for/param>
143 | protected static void WriteHashes(string filename)
144 | {
145 | // If the file doesn't exist, don't try anything
146 | if (!File.Exists(filename))
147 | return;
148 |
149 | // Get the hash string from the file
150 | string? hashString = HashingHelper.GetInfo(filename);
151 | if (hashString == null)
152 | return;
153 |
154 | // Open the output file and write the hashes
155 | using var fs = File.Open(Path.GetFullPath(filename) + ".hash", FileMode.Create, FileAccess.Write, FileShare.None);
156 | using var sw = new StreamWriter(fs);
157 | sw.Write(hashString);
158 | }
159 |
160 | ///
161 | /// Determine the file type from the filename extension
162 | ///
163 | /// Filename to derive the type from
164 | /// FileType value, if possible
165 | private static FileType DetermineFileType(string filename)
166 | {
167 | if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase) // Standard carts
168 | || filename.EndsWith(".nds.dec", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area decrypted
169 | || filename.EndsWith(".nds.enc", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area encrypted
170 | || filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase)) // Development carts/images
171 | {
172 | Console.WriteLine("File recognized as Nintendo DS");
173 | return FileType.NDS;
174 | }
175 | else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
176 | {
177 | Console.WriteLine("File recognized as Nintendo DSi");
178 | return FileType.NDSi;
179 | }
180 | else if (filename.EndsWith(".ids", StringComparison.OrdinalIgnoreCase))
181 | {
182 | Console.WriteLine("File recognized as iQue DS");
183 | return FileType.iQueDS;
184 | }
185 | else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase) // Standard carts
186 | || filename.EndsWith(".3ds.dec", StringComparison.OrdinalIgnoreCase) // Decrypted carts/images
187 | || filename.EndsWith(".3ds.enc", StringComparison.OrdinalIgnoreCase) // Encrypted carts/images
188 | || filename.EndsWith(".cci", StringComparison.OrdinalIgnoreCase)) // Development carts/images
189 | {
190 | Console.WriteLine("File recognized as Nintendo 3DS");
191 | return FileType.N3DS;
192 | }
193 |
194 | Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds, *.cci");
195 | return FileType.NULL;
196 | }
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/NDecrypt.Core/DecryptArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using SabreTools.Hashing;
4 | using SabreTools.IO.Encryption;
5 | using SabreTools.IO.Extensions;
6 |
7 | namespace NDecrypt.Core
8 | {
9 | public class DecryptArgs
10 | {
11 | #region Common Fields
12 |
13 | ///
14 | /// Represents if all of the keys have been initialized properly
15 | ///
16 | public bool? IsReady { get; private set; }
17 |
18 | #endregion
19 |
20 | #region DS-Specific Fields
21 |
22 | ///
23 | /// Blowfish Table
24 | ///
25 | public byte[] NitroEncryptionData { get; private set; } = [];
26 |
27 | #endregion
28 |
29 | #region 3DS-Specific Fields
30 |
31 | ///
32 | /// AES Hardware Constant
33 | ///
34 | public byte[] AESHardwareConstant { get; private set; } = [];
35 |
36 | ///
37 | /// KeyX 0x18 (New 3DS 9.3)
38 | ///
39 | public byte[] KeyX0x18 { get; private set; } = [];
40 |
41 | ///
42 | /// Dev KeyX 0x18 (New 3DS 9.3)
43 | ///
44 | public byte[] DevKeyX0x18 { get; private set; } = [];
45 |
46 | ///
47 | /// KeyX 0x1B (New 3DS 9.6)
48 | ///
49 | public byte[] KeyX0x1B { get; private set; } = [];
50 |
51 | ///
52 | /// Dev KeyX 0x1B New 3DS 9.6)
53 | ///
54 | public byte[] DevKeyX0x1B { get; private set; } = [];
55 |
56 | ///
57 | /// KeyX 0x25 (> 7.x)
58 | ///
59 | public byte[] KeyX0x25 { get; private set; } = [];
60 |
61 | ///
62 | /// Dev KeyX 0x25 (> 7.x)
63 | ///
64 | public byte[] DevKeyX0x25 { get; private set; } = [];
65 |
66 | ///
67 | /// KeyX 0x2C (< 6.x)
68 | ///
69 | public byte[] KeyX0x2C { get; private set; } = [];
70 |
71 | ///
72 | /// Dev KeyX 0x2C (< 6.x)
73 | ///
74 | public byte[] DevKeyX0x2C { get; private set; } = [];
75 |
76 | #endregion
77 |
78 | #region Internal Test Values
79 |
80 | ///
81 | /// Expected hash for NitroEncryptionData
82 | ///
83 | private static readonly byte[] ExpectedNitroSha512Hash =
84 | [
85 | 0x1A, 0xD6, 0x40, 0x21, 0xFC, 0x3D, 0x1A, 0x9A,
86 | 0x9B, 0xC0, 0x88, 0x8E, 0x2E, 0x68, 0xDE, 0x4E,
87 | 0x8A, 0x60, 0x6B, 0x86, 0x63, 0x22, 0xD2, 0xC7,
88 | 0xC6, 0xD7, 0xD6, 0xCE, 0x65, 0xF5, 0xBA, 0xA7,
89 | 0xEA, 0x69, 0x63, 0x7E, 0xC9, 0xE4, 0x57, 0x7B,
90 | 0x01, 0xFD, 0xCE, 0xC2, 0x26, 0x3B, 0xD9, 0x0D,
91 | 0x84, 0x57, 0xC2, 0x00, 0xB8, 0x56, 0x9F, 0xE5,
92 | 0x56, 0xDA, 0x8D, 0xDE, 0x84, 0xB8, 0x8E, 0xE4,
93 | ];
94 |
95 | ///
96 | /// Initial value for key validation tests
97 | ///
98 | private static readonly byte[] TestIV =
99 | [
100 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
101 | 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
102 | ];
103 |
104 | ///
105 | /// Pattern to use for key validation tests
106 | ///
107 | private static readonly byte[] TestPattern =
108 | [
109 | 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
110 | 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
111 | 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
112 | 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
113 | 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
114 | 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
115 | 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
116 | 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
117 | ];
118 |
119 | ///
120 | /// Expected output value for KeyX0x18
121 | ///
122 | private static readonly byte[] ExpectedKeyX0x18 =
123 | [
124 | 0x06, 0xF1, 0xB2, 0x3B, 0x12, 0xAD, 0x80, 0xC1,
125 | 0x13, 0xC6, 0x18, 0x3D, 0x27, 0xB8, 0xB9, 0x95,
126 | 0x49, 0x73, 0x59, 0x82, 0xEF, 0xFE, 0x16, 0x48,
127 | 0x91, 0x2A, 0x89, 0x55, 0x9A, 0xDC, 0x3C, 0xA0,
128 | 0x84, 0x46, 0x14, 0xE0, 0x16, 0x59, 0x8E, 0x4F,
129 | 0xC2, 0x6C, 0x52, 0xA4, 0x7D, 0xAD, 0x4F, 0x23,
130 | 0xF1, 0xC6, 0x99, 0x44, 0x39, 0xB7, 0x42, 0xF0,
131 | 0x1F, 0xBB, 0x02, 0xF6, 0x0A, 0x8A, 0xC2, 0x9A,
132 | ];
133 |
134 | ///
135 | /// Expected output value for DevKeyX0x18
136 | ///
137 | private static readonly byte[] ExpectedDevKeyX0x18 =
138 | [
139 | 0x99, 0x6E, 0x3C, 0x54, 0x97, 0x3C, 0xEA, 0xE8,
140 | 0xBA, 0xAE, 0x18, 0x5C, 0x93, 0x27, 0x65, 0x50,
141 | 0xF6, 0x6D, 0x67, 0xD7, 0xEF, 0xBD, 0x7C, 0xCB,
142 | 0x8A, 0xC1, 0x1A, 0x54, 0xFC, 0x3B, 0x8B, 0x3A,
143 | 0x0E, 0xE5, 0xEF, 0x27, 0x4A, 0x73, 0x7E, 0x0A,
144 | 0x2E, 0x2E, 0x9D, 0xAF, 0x6C, 0x03, 0xF2, 0x91,
145 | 0xC4, 0xFA, 0x73, 0xFD, 0x6B, 0xA0, 0x07, 0xD4,
146 | 0x75, 0x5B, 0x6F, 0x2E, 0x8B, 0x68, 0x4C, 0xD1,
147 | ];
148 |
149 | ///
150 | /// Expected output value for KeyX0x1B
151 | ///
152 | private static readonly byte[] ExpectedKeyX0x1B =
153 | [
154 | 0x0A, 0xE4, 0x79, 0x02, 0x1B, 0xFA, 0x25, 0x4B,
155 | 0x2D, 0x92, 0x4F, 0xA8, 0x41, 0x59, 0xCE, 0x10,
156 | 0x09, 0xE6, 0x08, 0x61, 0x23, 0xC7, 0xD2, 0x30,
157 | 0x84, 0x37, 0xD5, 0x49, 0x42, 0x94, 0xB2, 0x70,
158 | 0x6A, 0xF3, 0x75, 0xB0, 0x1F, 0x4F, 0xA1, 0xCE,
159 | 0x03, 0xA2, 0x6A, 0x19, 0x5D, 0x32, 0x0D, 0xB5,
160 | 0x79, 0xCD, 0xFD, 0xF0, 0xDE, 0x49, 0x26, 0x2D,
161 | 0x29, 0x36, 0x30, 0x69, 0x8B, 0x45, 0xE1, 0xFC,
162 | ];
163 |
164 | ///
165 | /// Expected output value for DevKeyX0x1B
166 | ///
167 | private static readonly byte[] ExpectedDevKeyX0x1B =
168 | [
169 | 0x16, 0x4F, 0xD9, 0x58, 0xC9, 0x20, 0xB3, 0xED,
170 | 0xC4, 0xEB, 0x57, 0x39, 0x10, 0xEF, 0xA8, 0xCC,
171 | 0xE5, 0x49, 0xBF, 0x52, 0x10, 0xA9, 0xCC, 0xE1,
172 | 0x65, 0x3B, 0x2D, 0x51, 0x45, 0xFB, 0x60, 0x52,
173 | 0x3E, 0x29, 0xEB, 0xEB, 0x3F, 0xF2, 0x76, 0x08,
174 | 0x00, 0x05, 0x7F, 0x64, 0x29, 0x4A, 0x17, 0x22,
175 | 0x56, 0x7F, 0x49, 0x94, 0x1A, 0x8C, 0x56, 0x35,
176 | 0x38, 0xBE, 0xA4, 0x2E, 0x58, 0xD3, 0x81, 0x8C,
177 | ];
178 |
179 | ///
180 | /// Expected output value for KeyX0x25
181 | ///
182 | private static readonly byte[] ExpectedKeyX0x25 =
183 | [
184 | 0x37, 0xBC, 0x73, 0xD6, 0xEE, 0x73, 0xE0, 0x94,
185 | 0x42, 0x84, 0x74, 0xE5, 0xD8, 0xFB, 0x5F, 0x65,
186 | 0xF4, 0xCF, 0x2E, 0xC1, 0x43, 0x48, 0x6C, 0xAA,
187 | 0xC8, 0xF9, 0x96, 0xE6, 0x33, 0xDD, 0xE7, 0xBF,
188 | 0xD2, 0x21, 0x89, 0x39, 0x13, 0xD1, 0xEC, 0xCA,
189 | 0x1D, 0x5D, 0x1F, 0x77, 0x95, 0xD2, 0x8B, 0x27,
190 | 0x92, 0x79, 0xC5, 0x1D, 0x72, 0xA7, 0x28, 0x57,
191 | 0x41, 0x0E, 0x46, 0xB8, 0x80, 0x7B, 0x7C, 0x0D,
192 | ];
193 |
194 | ///
195 | /// Expected output value for DevKeyX0x25
196 | ///
197 | private static readonly byte[] ExpectedDevKeyX0x25 =
198 | [
199 | 0x71, 0x65, 0x30, 0xF2, 0x68, 0xEC, 0x65, 0x0A,
200 | 0x8C, 0x9E, 0xC5, 0x5A, 0xFA, 0x37, 0x8E, 0xDA,
201 | 0x7B, 0x58, 0x3B, 0x66, 0x7C, 0x9D, 0x16, 0xD9,
202 | 0x2D, 0x8F, 0xCF, 0x04, 0x66, 0x7F, 0x27, 0x41,
203 | 0xBF, 0x5F, 0x1E, 0x11, 0x4C, 0xD6, 0xB9, 0x0A,
204 | 0xC5, 0x42, 0xCF, 0x2B, 0x87, 0x6B, 0xD4, 0x72,
205 | 0x4D, 0x9C, 0x29, 0x2E, 0xF8, 0xB0, 0x6F, 0x22,
206 | 0x35, 0x5B, 0x96, 0x83, 0xD1, 0xE4, 0x5E, 0xDB,
207 | ];
208 |
209 | ///
210 | /// Expected output value for KeyX0x2C
211 | ///
212 | private static readonly byte[] ExpectedKeyX0x2C =
213 | [
214 | 0xAE, 0x44, 0x20, 0xDB, 0xA5, 0x96, 0xDC, 0xF3,
215 | 0xD8, 0x23, 0x9E, 0x3C, 0x44, 0x73, 0x3D, 0xCD,
216 | 0x07, 0xD5, 0xF8, 0xD0, 0xC6, 0xB3, 0x5A, 0x80,
217 | 0xB5, 0x5A, 0x55, 0x30, 0x5D, 0x4A, 0xBE, 0x61,
218 | 0xBF, 0xEF, 0x64, 0x17, 0x28, 0xD6, 0x26, 0x52,
219 | 0x42, 0x4D, 0x8F, 0x1C, 0xBC, 0x63, 0xD3, 0x91,
220 | 0x7D, 0xA6, 0x4F, 0xAF, 0x26, 0x38, 0x60, 0xEE,
221 | 0x79, 0x92, 0x2F, 0xD8, 0xCA, 0x4E, 0xE7, 0xEC,
222 | ];
223 |
224 | ///
225 | /// Expected output value for DevKeyX0x2C
226 | ///
227 | private static readonly byte[] ExpectedDevKeyX0x2C =
228 | [
229 | 0x5F, 0x73, 0xD5, 0x9A, 0x67, 0xFF, 0x8C, 0x12,
230 | 0x31, 0x58, 0x0B, 0x58, 0x46, 0xFE, 0x05, 0x16,
231 | 0x92, 0xE4, 0x84, 0x06, 0x18, 0x9B, 0x58, 0x91,
232 | 0xE7, 0xF8, 0xCD, 0xA9, 0x95, 0xAC, 0x07, 0xCD,
233 | 0x43, 0x20, 0x7A, 0x8C, 0xCC, 0xAB, 0x48, 0x50,
234 | 0x29, 0x2F, 0x96, 0x73, 0xB0, 0xD9, 0xE5, 0xCB,
235 | 0xE6, 0x9A, 0x0D, 0xF7, 0xD0, 0x1E, 0xC2, 0xEC,
236 | 0xC1, 0xE2, 0x8E, 0xEE, 0x89, 0xB9, 0xB1, 0x97,
237 | ];
238 |
239 | #endregion
240 |
241 | ///
242 | /// Setup all of the necessary constants
243 | ///
244 | /// Path to the keyfile
245 | public DecryptArgs(string? config)
246 | {
247 | if (config == null || !File.Exists(config))
248 | {
249 | IsReady = false;
250 | return;
251 | }
252 |
253 | // Try to read the configuration file
254 | var configObj = Configuration.Create(config);
255 | if (configObj == null)
256 | {
257 | IsReady = false;
258 | return;
259 | }
260 |
261 | // Set the fields from the configuration
262 | NitroEncryptionData = configObj.NitroEncryptionData.FromHexString() ?? [];
263 | AESHardwareConstant = configObj.AESHardwareConstant.FromHexString() ?? [];
264 | KeyX0x18 = configObj.KeyX0x18.FromHexString() ?? [];
265 | KeyX0x1B = configObj.KeyX0x1B.FromHexString() ?? [];
266 | KeyX0x25 = configObj.KeyX0x25.FromHexString() ?? [];
267 | KeyX0x2C = configObj.KeyX0x2C.FromHexString() ?? [];
268 | DevKeyX0x18 = configObj.DevKeyX0x18.FromHexString() ?? [];
269 | DevKeyX0x1B = configObj.DevKeyX0x1B.FromHexString() ?? [];
270 | DevKeyX0x25 = configObj.DevKeyX0x25.FromHexString() ?? [];
271 | DevKeyX0x2C = configObj.DevKeyX0x2C.FromHexString() ?? [];
272 |
273 | IsReady = true;
274 | ValidateKeys();
275 | }
276 |
277 | ///
278 | /// Validate that all keys provided are going to be valid
279 | ///
280 | /// Does not know what the keys are, just the result
281 | private void ValidateKeys()
282 | {
283 | // NitroEncryptionData
284 | if (NitroEncryptionData.Length > 0)
285 | {
286 | byte[]? actual = HashTool.GetByteArrayHashArray(NitroEncryptionData, HashType.SHA512);
287 | if (actual == null || !actual.EqualsExactly(ExpectedNitroSha512Hash))
288 | {
289 | Console.WriteLine($"NitroEncryptionData invalid value, disabling...");
290 | NitroEncryptionData = [];
291 | }
292 | }
293 |
294 | // KeyX0x18
295 | if (KeyX0x18.Length > 0)
296 | {
297 | var cipher = AESCTR.CreateEncryptionCipher(KeyX0x18, TestIV);
298 | byte[] actual = cipher.ProcessBytes(TestPattern);
299 | if (!actual.EqualsExactly(ExpectedKeyX0x18))
300 | {
301 | Console.WriteLine($"KeyX0x18 invalid value, disabling...");
302 | KeyX0x18 = [];
303 | }
304 | }
305 |
306 | // DevKeyX0x18
307 | if (DevKeyX0x18.Length > 0)
308 | {
309 | var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x18, TestIV);
310 | byte[] actual = cipher.ProcessBytes(TestPattern);
311 | if (!actual.EqualsExactly(ExpectedDevKeyX0x18))
312 | {
313 | Console.WriteLine($"DevKeyX0x18 invalid value, disabling...");
314 | DevKeyX0x18 = [];
315 | }
316 | }
317 |
318 | // KeyX0x1B
319 | if (KeyX0x1B.Length > 0)
320 | {
321 | var cipher = AESCTR.CreateEncryptionCipher(KeyX0x1B, TestIV);
322 | byte[] actual = cipher.ProcessBytes(TestPattern);
323 | if (!actual.EqualsExactly(ExpectedKeyX0x1B))
324 | {
325 | Console.WriteLine($"KeyX0x1B invalid value, disabling...");
326 | KeyX0x1B = [];
327 | }
328 | }
329 |
330 | // DevKeyX0x1B
331 | if (DevKeyX0x1B.Length > 0)
332 | {
333 | var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x1B, TestIV);
334 | byte[] actual = cipher.ProcessBytes(TestPattern);
335 | if (!actual.EqualsExactly(ExpectedDevKeyX0x1B))
336 | {
337 | Console.WriteLine($"DevKeyX0x1B invalid value, disabling...");
338 | DevKeyX0x1B = [];
339 | }
340 | }
341 |
342 | // KeyX0x25
343 | if (KeyX0x25.Length > 0)
344 | {
345 | var cipher = AESCTR.CreateEncryptionCipher(KeyX0x25, TestIV);
346 | byte[] actual = cipher.ProcessBytes(TestPattern);
347 | if (!actual.EqualsExactly(ExpectedKeyX0x25))
348 | {
349 | Console.WriteLine($"KeyX0x25 invalid value, disabling...");
350 | KeyX0x25 = [];
351 | }
352 | }
353 |
354 | // DevKeyX0x25
355 | if (DevKeyX0x25.Length > 0)
356 | {
357 | var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x25, TestIV);
358 | byte[] actual = cipher.ProcessBytes(TestPattern);
359 | if (!actual.EqualsExactly(ExpectedDevKeyX0x25))
360 | {
361 | Console.WriteLine($"DevKeyX0x25 invalid value, disabling...");
362 | DevKeyX0x25 = [];
363 | }
364 | }
365 |
366 | // KeyX0x2C
367 | if (KeyX0x2C.Length > 0)
368 | {
369 | var cipher = AESCTR.CreateEncryptionCipher(KeyX0x2C, TestIV);
370 | byte[] actual = cipher.ProcessBytes(TestPattern);
371 | if (!actual.EqualsExactly(ExpectedKeyX0x2C))
372 | {
373 | Console.WriteLine($"KeyX0x2C invalid value, disabling...");
374 | KeyX0x2C = [];
375 | }
376 | }
377 |
378 | // DevKeyX0x2C
379 | if (DevKeyX0x2C.Length > 0)
380 | {
381 | var cipher = AESCTR.CreateEncryptionCipher(DevKeyX0x2C, TestIV);
382 | byte[] actual = cipher.ProcessBytes(TestPattern);
383 | if (!actual.EqualsExactly(ExpectedDevKeyX0x2C))
384 | {
385 | Console.WriteLine($"DevKeyX0x2C invalid value, disabling...");
386 | DevKeyX0x2C = [];
387 | }
388 | }
389 | }
390 | }
391 | }
392 |
--------------------------------------------------------------------------------
/NDecrypt.Core/ThreeDSTool.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using SabreTools.Data.Models.N3DS;
5 | using SabreTools.IO.Encryption;
6 | using SabreTools.IO.Extensions;
7 | using SabreTools.Serialization.Wrappers;
8 | using static SabreTools.Data.Models.N3DS.Constants;
9 |
10 | namespace NDecrypt.Core
11 | {
12 | public class ThreeDSTool : ITool
13 | {
14 | ///
15 | /// Decryption args to use while processing
16 | ///
17 | private readonly DecryptArgs _decryptArgs;
18 |
19 | ///
20 | /// Indicates if development images are expected
21 | ///
22 | private readonly bool _development;
23 |
24 | ///
25 | /// Set of all partition keys
26 | ///
27 | private readonly PartitionKeys[] _keysMap = new PartitionKeys[8];
28 |
29 | public ThreeDSTool(bool development, DecryptArgs decryptArgs)
30 | {
31 | _development = development;
32 | _decryptArgs = decryptArgs;
33 | }
34 |
35 | #region Decrypt
36 |
37 | ///
38 | public bool DecryptFile(string input, string? output, bool force)
39 | {
40 | // Ensure the constants are all set
41 | if (_decryptArgs.IsReady != true)
42 | {
43 | Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
44 | return false;
45 | }
46 |
47 | try
48 | {
49 | // If the output is provided, copy the input file
50 | if (output != null)
51 | File.Copy(input, output, overwrite: true);
52 | else
53 | output = input;
54 |
55 | // Open the output file for processing
56 | using var reader = File.Open(output, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
57 | using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
58 |
59 | // Deserialize the cart information
60 | var cart = N3DS.Create(reader);
61 | if (cart?.Model == null)
62 | {
63 | Console.WriteLine("Error: Not a 3DS cart image!");
64 | return false;
65 | }
66 |
67 | // Decrypt all 8 NCCH partitions
68 | DecryptAllPartitions(cart, force, reader, writer);
69 | return true;
70 | }
71 | catch
72 | {
73 | Console.WriteLine($"An error has occurred. {output} may be corrupted if it was partially processed.");
74 | Console.WriteLine("Please check that the file was a valid 3DS or New 3DS cart image and try again.");
75 | return false;
76 | }
77 | }
78 |
79 | ///
80 | /// Decrypt all partitions in the partition table of an NCSD header
81 | ///
82 | /// Cart representing the 3DS file
83 | /// Indicates if the operation should be forced
84 | /// Stream representing the input
85 | /// Stream representing the output
86 | private void DecryptAllPartitions(N3DS cart, bool force, Stream reader, Stream writer)
87 | {
88 | // Check the partitions table
89 | if (cart.PartitionsTable == null || cart.Partitions == null)
90 | {
91 | Console.WriteLine("Invalid partitions table!");
92 | return;
93 | }
94 |
95 | // Iterate over all 8 NCCH partitions
96 | for (int p = 0; p < 8; p++)
97 | {
98 | var partition = cart.Partitions[p];
99 | if (partition == null || partition.MagicID != NCCHMagicNumber)
100 | {
101 | Console.WriteLine($"Partition {p} Not found... Skipping...");
102 | continue;
103 | }
104 |
105 | // Check the partition has data
106 | var partitionEntry = cart.PartitionsTable[p];
107 | if (partitionEntry == null || partitionEntry.Length == 0)
108 | {
109 | Console.WriteLine($"Partition {p} No data... Skipping...");
110 | continue;
111 | }
112 |
113 | // Decrypt the partition, if possible
114 | if (ShouldDecryptPartition(cart, p, force))
115 | DecryptPartition(cart, p, reader, writer);
116 | }
117 | }
118 |
119 | ///
120 | /// Determine if the current partition should be decrypted
121 | /// s
122 | private static bool ShouldDecryptPartition(N3DS cart, int index, bool force)
123 | {
124 | // If we're forcing the operation, tell the user
125 | if (force)
126 | {
127 | Console.WriteLine($"Partition {index} is not verified due to force flag being set.");
128 | return true;
129 | }
130 | // If we're not forcing the operation, check if the 'NoCrypto' bit is set
131 | else if (cart.PossiblyDecrypted(index))
132 | {
133 | Console.WriteLine($"Partition {index}: Already Decrypted?...");
134 | return false;
135 | }
136 |
137 | // By default, it passes
138 | return true;
139 | }
140 |
141 | ///
142 | /// Decrypt a single partition
143 | ///
144 | /// Cart representing the 3DS file
145 | /// Index of the partition
146 | /// Stream representing the input
147 | /// Stream representing the output
148 | private void DecryptPartition(N3DS cart, int index, Stream reader, Stream writer)
149 | {
150 | // Determine the keys needed for this partition
151 | SetDecryptionKeys(cart, index);
152 |
153 | // Decrypt the parts of the partition
154 | DecryptExtendedHeader(cart, index, reader, writer);
155 | DecryptExeFS(cart, index, reader, writer);
156 | DecryptRomFS(cart, index, reader, writer);
157 |
158 | // Update the flags
159 | UpdateDecryptCryptoAndMasks(cart, index, writer);
160 | }
161 |
162 | ///
163 | /// Determine the set of keys to be used for decryption
164 | ///
165 | /// Cart representing the 3DS file
166 | /// Index of the partition
167 | private void SetDecryptionKeys(N3DS cart, int index)
168 | {
169 | // Get the partition
170 | var partition = cart.Partitions?[index];
171 | if (partition?.Flags == null)
172 | return;
173 |
174 | // Get partition-specific values
175 | byte[]? rsaSignature = partition.RSA2048Signature;
176 | BitMasks masks = cart.GetBitMasks(index);
177 | CryptoMethod method = cart.GetCryptoMethod(index);
178 |
179 | // Get the partition keys
180 | _keysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
181 | }
182 |
183 | ///
184 | /// Decrypt the extended header, if it exists
185 | ///
186 | /// Cart representing the 3DS file
187 | /// Index of the partition
188 | /// Stream representing the input
189 | /// Stream representing the output
190 | private bool DecryptExtendedHeader(N3DS cart, int index, Stream reader, Stream writer)
191 | {
192 | // Get required offsets
193 | uint partitionOffset = cart.GetPartitionOffset(index);
194 | if (partitionOffset == 0 || partitionOffset > reader.Length)
195 | {
196 | Console.WriteLine($"Partition {index} No Data... Skipping...");
197 | return false;
198 | }
199 |
200 | uint extHeaderSize = cart.GetExtendedHeaderSize(index);
201 | if (extHeaderSize == 0)
202 | {
203 | Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
204 | return false;
205 | }
206 |
207 | // Seek to the extended header
208 | reader.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
209 | writer.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
210 |
211 | Console.WriteLine($"Partition {index}: Decrypting - ExHeader");
212 |
213 | // Create the Plain AES cipher for this partition
214 | var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey2C, cart.PlainIV(index));
215 |
216 | // Process the extended header
217 | AESCTR.PerformOperation(Constants.CXTExtendedDataHeaderLength, cipher, reader, writer, null);
218 |
219 | #if NET6_0_OR_GREATER
220 | // In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
221 | reader.Seek(0, SeekOrigin.Begin);
222 | #endif
223 | writer.Flush();
224 | return true;
225 | }
226 |
227 | ///
228 | /// Decrypt the ExeFS, if it exists
229 | ///
230 | /// Cart representing the 3DS file
231 | /// Index of the partition
232 | /// Stream representing the input
233 | /// Stream representing the output
234 | private bool DecryptExeFS(N3DS cart, int index, Stream reader, Stream writer)
235 | {
236 | // Validate the ExeFS
237 | uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
238 | if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > reader.Length)
239 | {
240 | Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
241 | return false;
242 | }
243 |
244 | uint exeFsSize = cart.GetExeFSSize(index);
245 | if (exeFsSize == 0)
246 | {
247 | Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
248 | return false;
249 | }
250 |
251 | // Decrypt the filename table
252 | DecryptExeFSFilenameTable(cart, index, reader, writer);
253 |
254 | // For all but the original crypto method, process each of the files in the table
255 | if (cart.GetCryptoMethod(index) != CryptoMethod.Original)
256 | DecryptExeFSFileEntries(cart, index, reader, writer);
257 |
258 | // Get the ExeFS files offset
259 | uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
260 |
261 | // Seek to the ExeFS
262 | reader.Seek(exeFsFilesOffset, SeekOrigin.Begin);
263 | writer.Seek(exeFsFilesOffset, SeekOrigin.Begin);
264 |
265 | // Create the ExeFS AES cipher for this partition
266 | uint ctroffsetE = cart.MediaUnitSize / 0x10;
267 | byte[] exefsIVWithOffset = cart.ExeFSIV(index).Add(ctroffsetE);
268 | var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey2C, exefsIVWithOffset);
269 |
270 | // Setup and perform the decryption
271 | exeFsSize -= cart.MediaUnitSize;
272 | AESCTR.PerformOperation(exeFsSize,
273 | cipher,
274 | reader,
275 | writer,
276 | s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
277 |
278 | return true;
279 | }
280 |
281 | ///
282 | /// Decrypt the ExeFS Filename Table
283 | ///
284 | /// Cart representing the 3DS file
285 | /// Index of the partition
286 | /// Stream representing the input
287 | /// Stream representing the output
288 | private void DecryptExeFSFilenameTable(N3DS cart, int index, Stream reader, Stream writer)
289 | {
290 | // Get ExeFS offset
291 | uint exeFsOffset = cart.GetExeFSOffset(index);
292 | if (exeFsOffset == 0 || exeFsOffset > reader.Length)
293 | {
294 | Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
295 | return;
296 | }
297 |
298 | // Seek to the ExeFS header
299 | reader.Seek(exeFsOffset, SeekOrigin.Begin);
300 | writer.Seek(exeFsOffset, SeekOrigin.Begin);
301 |
302 | Console.WriteLine($"Partition {index} ExeFS: Decrypting - ExeFS Filename Table");
303 |
304 | // Create the ExeFS AES cipher for this partition
305 | var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey2C, cart.ExeFSIV(index));
306 |
307 | // Process the filename table
308 | byte[] readBytes = reader.ReadBytes((int)cart.MediaUnitSize);
309 | byte[] processedBytes = cipher.ProcessBytes(readBytes);
310 | writer.Write(processedBytes);
311 |
312 | #if NET6_0_OR_GREATER
313 | // In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
314 | reader.Seek(0, SeekOrigin.Begin);
315 | #endif
316 | writer.Flush();
317 | }
318 |
319 | ///
320 | /// Decrypt the ExeFS file entries
321 | ///
322 | /// Cart representing the 3DS file
323 | /// Index of the partition
324 | /// Stream representing the input
325 | /// Stream representing the output
326 | private void DecryptExeFSFileEntries(N3DS cart, int index, Stream reader, Stream writer)
327 | {
328 | if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
329 | {
330 | Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
331 | return;
332 | }
333 |
334 | // Reread the decrypted ExeFS header
335 | uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
336 | reader.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
337 | cart.ExeFSHeaders[index] = SabreTools.Serialization.Readers.N3DS.ParseExeFSHeader(reader);
338 |
339 | // Get the ExeFS header
340 | var exeFsHeader = cart.ExeFSHeaders[index];
341 | if (exeFsHeader?.FileHeaders == null)
342 | {
343 | Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
344 | return;
345 | }
346 |
347 | // Get the ExeFS files offset
348 | uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
349 |
350 | // Loop through and process all headers
351 | for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
352 | {
353 | // Only attempt to process code binary files
354 | if (!cart.IsCodeBinary(index, i))
355 | continue;
356 |
357 | // Get the file header
358 | var fileHeader = exeFsHeader.FileHeaders[i];
359 | if (fileHeader == null)
360 | continue;
361 |
362 | // Create the ExeFS AES ciphers for this partition
363 | uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize) / 0x10;
364 | byte[] exefsIVWithOffsetForHeader = cart.ExeFSIV(index).Add(ctroffset);
365 | var firstCipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey, exefsIVWithOffsetForHeader);
366 | var secondCipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
367 |
368 | // Seek to the file entry
369 | reader.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
370 | writer.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
371 |
372 | // Setup and perform the encryption
373 | AESCTR.PerformOperation(fileHeader.FileSize,
374 | firstCipher,
375 | secondCipher,
376 | reader,
377 | writer,
378 | s => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
379 | }
380 | }
381 |
382 | ///
383 | /// Decrypt the RomFS, if it exists
384 | ///
385 | /// Cart representing the 3DS file
386 | /// Index of the partition
387 | /// Stream representing the input
388 | /// Stream representing the output
389 | private bool DecryptRomFS(N3DS cart, int index, Stream reader, Stream writer)
390 | {
391 | // Validate the RomFS
392 | uint romFsOffset = cart.GetRomFSOffset(index);
393 | if (romFsOffset == 0 || romFsOffset > reader.Length)
394 | {
395 | Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
396 | return false;
397 | }
398 |
399 | uint romFsSize = cart.GetRomFSSize(index);
400 | if (romFsSize == 0)
401 | {
402 | Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
403 | return false;
404 | }
405 |
406 | // Seek to the RomFS
407 | reader.Seek(romFsOffset, SeekOrigin.Begin);
408 | writer.Seek(romFsOffset, SeekOrigin.Begin);
409 |
410 | // Create the RomFS AES cipher for this partition
411 | var cipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey, cart.RomFSIV(index));
412 |
413 | // Setup and perform the decryption
414 | AESCTR.PerformOperation(romFsSize,
415 | cipher,
416 | reader,
417 | writer,
418 | s => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
419 |
420 | return true;
421 | }
422 |
423 | ///
424 | /// Update the CryptoMethod and BitMasks for the decrypted partition
425 | ///
426 | /// Cart representing the 3DS file
427 | /// Index of the partition
428 | /// Stream representing the output
429 | private static void UpdateDecryptCryptoAndMasks(N3DS cart, int index, Stream writer)
430 | {
431 | // Get required offsets
432 | uint partitionOffset = cart.GetPartitionOffset(index);
433 |
434 | // Seek to the CryptoMethod location
435 | writer.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
436 |
437 | // Write the new CryptoMethod
438 | writer.Write((byte)CryptoMethod.Original);
439 | writer.Flush();
440 |
441 | // Seek to the BitMasks location
442 | writer.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
443 |
444 | // Write the new BitMasks flag
445 | BitMasks flag = cart.GetBitMasks(index);
446 | flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
447 | flag |= BitMasks.NoCrypto;
448 | writer.Write((byte)flag);
449 | writer.Flush();
450 | }
451 |
452 | #endregion
453 |
454 | #region Encrypt
455 |
456 | ///
457 | public bool EncryptFile(string input, string? output, bool force)
458 | {
459 | // Ensure the constants are all set
460 | if (_decryptArgs.IsReady != true)
461 | {
462 | Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
463 | return false;
464 | }
465 |
466 | try
467 | {
468 | // If the output is provided, copy the input file
469 | if (output != null)
470 | File.Copy(input, output, overwrite: true);
471 | else
472 | output = input;
473 |
474 | // Open the output file for processing
475 | using var reader = File.Open(output, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
476 | using var writer = File.Open(output, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
477 |
478 | // Deserialize the cart information
479 | var cart = N3DS.Create(reader);
480 | if (cart?.Model == null)
481 | {
482 | Console.WriteLine("Error: Not a 3DS cart image!");
483 | return false;
484 | }
485 |
486 | // Encrypt all 8 NCCH partitions
487 | EncryptAllPartitions(cart, force, reader, writer);
488 | return true;
489 | }
490 | catch
491 | {
492 | Console.WriteLine($"An error has occurred. {output} may be corrupted if it was partially processed.");
493 | Console.WriteLine("Please check that the file was a valid 3DS or New 3DS cart image and try again.");
494 | return false;
495 | }
496 | }
497 |
498 | ///
499 | /// Encrypt all partitions in the partition table of an NCSD header
500 | ///
501 | /// Cart representing the 3DS file
502 | /// Indicates if the operation should be forced
503 | /// Stream representing the input
504 | /// Stream representing the output
505 | private void EncryptAllPartitions(N3DS cart, bool force, Stream reader, Stream writer)
506 | {
507 | // Check the partitions table
508 | if (cart.PartitionsTable == null || cart.Partitions == null)
509 | {
510 | Console.WriteLine("Invalid partitions table!");
511 | return;
512 | }
513 |
514 | // Iterate over all 8 NCCH partitions
515 | for (int p = 0; p < 8; p++)
516 | {
517 | // Check the partition exists
518 | var partition = cart.Partitions[p];
519 | if (partition == null || partition.MagicID != NCCHMagicNumber)
520 | {
521 | Console.WriteLine($"Partition {p} Not found... Skipping...");
522 | continue;
523 | }
524 |
525 | // Check the partition has data
526 | var partitionEntry = cart.PartitionsTable[p];
527 | if (partitionEntry == null || partitionEntry.Length == 0)
528 | {
529 | Console.WriteLine($"Partition {p} No data... Skipping...");
530 | continue;
531 | }
532 |
533 | // Encrypt the partition, if possible
534 | if (ShouldEncryptPartition(cart, p, force))
535 | EncryptPartition(cart, p, reader, writer);
536 | }
537 | }
538 |
539 | ///
540 | /// Determine if the current partition should be encrypted
541 | ///
542 | private static bool ShouldEncryptPartition(N3DS cart, int index, bool force)
543 | {
544 | // If we're forcing the operation, tell the user
545 | if (force)
546 | {
547 | Console.WriteLine($"Partition {index} is not verified due to force flag being set.");
548 | return true;
549 | }
550 | // If we're not forcing the operation, check if the 'NoCrypto' bit is set
551 | else if (!cart.PossiblyDecrypted(index))
552 | {
553 | Console.WriteLine($"Partition {index}: Already Encrypted?...");
554 | return false;
555 | }
556 |
557 | // By default, it passes
558 | return true;
559 | }
560 |
561 | ///
562 | /// Encrypt a single partition
563 | ///
564 | /// Cart representing the 3DS file
565 | /// Index of the partition
566 | /// Stream representing the input
567 | /// Stream representing the output
568 | private void EncryptPartition(N3DS cart, int index, Stream reader, Stream writer)
569 | {
570 | // Determine the keys needed for this partition
571 | SetEncryptionKeys(cart, index);
572 |
573 | // Encrypt the parts of the partition
574 | EncryptExtendedHeader(cart, index, reader, writer);
575 | EncryptExeFS(cart, index, reader, writer);
576 | EncryptRomFS(cart, index, reader, writer);
577 |
578 | // Update the flags
579 | UpdateEncryptCryptoAndMasks(cart, index, writer);
580 | }
581 |
582 | ///
583 | /// Determine the set of keys to be used for encryption
584 | ///
585 | /// Cart representing the 3DS file
586 | /// Index of the partition
587 | private void SetEncryptionKeys(N3DS cart, int index)
588 | {
589 | // Get the partition
590 | var partition = cart.Partitions?[index];
591 | if (partition == null)
592 | return;
593 |
594 | // Get the backup header
595 | var backupHeader = cart.BackupHeader;
596 | if (backupHeader?.Flags == null)
597 | return;
598 |
599 | // Get partition-specific values
600 | byte[]? rsaSignature = partition.RSA2048Signature;
601 | BitMasks masks = backupHeader.Flags.BitMasks;
602 | CryptoMethod method = backupHeader.Flags.CryptoMethod;
603 |
604 | // Get the partition keys
605 | _keysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
606 | }
607 |
608 | ///
609 | /// Encrypt the extended header, if it exists
610 | ///
611 | /// Cart representing the 3DS file
612 | /// Index of the partition
613 | /// Stream representing the input
614 | /// Stream representing the output
615 | private bool EncryptExtendedHeader(N3DS cart, int index, Stream reader, Stream writer)
616 | {
617 | // Get required offsets
618 | uint partitionOffset = cart.GetPartitionOffset(index);
619 | if (partitionOffset == 0 || partitionOffset > reader.Length)
620 | {
621 | Console.WriteLine($"Partition {index} No Data... Skipping...");
622 | return false;
623 | }
624 |
625 | uint extHeaderSize = cart.GetExtendedHeaderSize(index);
626 | if (extHeaderSize == 0)
627 | {
628 | Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
629 | return false;
630 | }
631 |
632 | // Seek to the extended header
633 | reader.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
634 | writer.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
635 |
636 | Console.WriteLine($"Partition {index}: Encrypting - ExHeader");
637 |
638 | // Create the Plain AES cipher for this partition
639 | var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey2C, cart.PlainIV(index));
640 |
641 | // Process the extended header
642 | AESCTR.PerformOperation(Constants.CXTExtendedDataHeaderLength, cipher, reader, writer, null);
643 |
644 | #if NET6_0_OR_GREATER
645 | // In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
646 | reader.Seek(0, SeekOrigin.Begin);
647 | #endif
648 | writer.Flush();
649 | return true;
650 | }
651 |
652 | ///
653 | /// Encrypt the ExeFS, if it exists
654 | ///
655 | /// Cart representing the 3DS file
656 | /// Index of the partition
657 | /// Stream representing the input
658 | /// Stream representing the output
659 | private bool EncryptExeFS(N3DS cart, int index, Stream reader, Stream writer)
660 | {
661 | if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
662 | {
663 | Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
664 | return false;
665 | }
666 |
667 | // Get the ExeFS header
668 | var exefsHeader = cart.ExeFSHeaders[index];
669 | if (exefsHeader == null)
670 | {
671 | Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
672 | return false;
673 | }
674 |
675 | // For all but the original crypto method, process each of the files in the table
676 | var backupHeader = cart.BackupHeader;
677 | if (backupHeader!.Flags!.CryptoMethod != CryptoMethod.Original)
678 | EncryptExeFSFileEntries(cart, index, reader, writer);
679 |
680 | // Encrypt the filename table
681 | EncryptExeFSFilenameTable(cart, index, reader, writer);
682 |
683 | // Get the ExeFS files offset
684 | uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
685 | uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
686 |
687 | // Seek to the ExeFS
688 | reader.Seek(exeFsFilesOffset, SeekOrigin.Begin);
689 | writer.Seek(exeFsFilesOffset, SeekOrigin.Begin);
690 |
691 | // Create the ExeFS AES cipher for this partition
692 | uint ctroffsetE = cart.MediaUnitSize / 0x10;
693 | byte[] exefsIVWithOffset = cart.ExeFSIV(index).Add(ctroffsetE);
694 | var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey2C, exefsIVWithOffset);
695 |
696 | // Setup and perform the encryption
697 | uint exeFsSize = cart.GetExeFSSize(index) - cart.MediaUnitSize;
698 | AESCTR.PerformOperation(exeFsSize,
699 | cipher,
700 | reader,
701 | writer,
702 | s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
703 |
704 | return true;
705 | }
706 |
707 | ///
708 | /// Encrypt the ExeFS Filename Table
709 | ///
710 | /// Cart representing the 3DS file
711 | /// Index of the partition
712 | /// Stream representing the input
713 | /// Stream representing the output
714 | private void EncryptExeFSFilenameTable(N3DS cart, int index, Stream reader, Stream writer)
715 | {
716 | // Get ExeFS offset
717 | uint exeFsOffset = cart.GetExeFSOffset(index);
718 | if (exeFsOffset == 0 || exeFsOffset > reader.Length)
719 | {
720 | Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
721 | return;
722 | }
723 |
724 | // Seek to the ExeFS header
725 | reader.Seek(exeFsOffset, SeekOrigin.Begin);
726 | writer.Seek(exeFsOffset, SeekOrigin.Begin);
727 |
728 | Console.WriteLine($"Partition {index} ExeFS: Encrypting - ExeFS Filename Table");
729 |
730 | // Create the ExeFS AES cipher for this partition
731 | var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey2C, cart.ExeFSIV(index));
732 |
733 | // Process the filename table
734 | byte[] readBytes = reader.ReadBytes((int)cart.MediaUnitSize);
735 | byte[] processedBytes = cipher.ProcessBytes(readBytes);
736 | writer.Write(processedBytes);
737 |
738 | #if NET6_0_OR_GREATER
739 | // In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
740 | reader.Seek(0, SeekOrigin.Begin);
741 | #endif
742 | writer.Flush();
743 | }
744 |
745 | ///
746 | /// Encrypt the ExeFS file entries
747 | ///
748 | /// Cart representing the 3DS file
749 | /// Index of the partition
750 | /// Stream representing the input
751 | /// Stream representing the output
752 | private void EncryptExeFSFileEntries(N3DS cart, int index, Stream reader, Stream writer)
753 | {
754 | // Get ExeFS offset
755 | uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
756 | if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > reader.Length)
757 | {
758 | Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
759 | return;
760 | }
761 |
762 | // Get to the start of the files
763 | uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
764 |
765 | // If the header failed to read, log and return
766 | var exeFsHeader = cart.ExeFSHeaders?[index];
767 | if (exeFsHeader?.FileHeaders == null)
768 | {
769 | Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
770 | return;
771 | }
772 |
773 | // Loop through and process all headers
774 | for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
775 | {
776 | // Only attempt to process code binary files
777 | if (!cart.IsCodeBinary(index, i))
778 | continue;
779 |
780 | // Get the file header
781 | var fileHeader = exeFsHeader.FileHeaders[i];
782 | if (fileHeader == null)
783 | continue;
784 |
785 | // Create the ExeFS AES ciphers for this partition
786 | uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize) / 0x10;
787 | byte[] exefsIVWithOffsetForHeader = cart.ExeFSIV(index).Add(ctroffset);
788 | var firstCipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey, exefsIVWithOffsetForHeader);
789 | var secondCipher = AESCTR.CreateDecryptionCipher(_keysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
790 |
791 | // Seek to the file entry
792 | reader.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
793 | writer.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
794 |
795 | // Setup and perform the encryption
796 | AESCTR.PerformOperation(fileHeader.FileSize,
797 | firstCipher,
798 | secondCipher,
799 | reader,
800 | writer,
801 | s => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
802 | }
803 | }
804 |
805 | ///
806 | /// Encrypt the RomFS, if it exists
807 | ///
808 | /// Cart representing the 3DS file
809 | /// Index of the partition
810 | /// Stream representing the input
811 | /// Stream representing the output
812 | private bool EncryptRomFS(N3DS cart, int index, Stream reader, Stream writer)
813 | {
814 | // Validate the RomFS
815 | uint romFsOffset = cart.GetRomFSOffset(index);
816 | if (romFsOffset == 0 || romFsOffset > reader.Length)
817 | {
818 | Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
819 | return false;
820 | }
821 |
822 | uint romFsSize = cart.GetRomFSSize(index);
823 | if (romFsSize == 0)
824 | {
825 | Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
826 | return false;
827 | }
828 |
829 | // Seek to the RomFS
830 | reader.Seek(romFsOffset, SeekOrigin.Begin);
831 | writer.Seek(romFsOffset, SeekOrigin.Begin);
832 |
833 | // Force setting encryption keys for partitions 1 and above
834 | if (index > 0)
835 | {
836 | var backupHeader = cart.BackupHeader;
837 | _keysMap[index].SetRomFSValues(backupHeader!.Flags!.BitMasks);
838 | }
839 |
840 | // Create the RomFS AES cipher for this partition
841 | var cipher = AESCTR.CreateEncryptionCipher(_keysMap[index].NormalKey, cart.RomFSIV(index));
842 |
843 | // Setup and perform the decryption
844 | AESCTR.PerformOperation(romFsSize,
845 | cipher,
846 | reader,
847 | writer,
848 | s => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
849 |
850 | return true;
851 | }
852 |
853 | ///
854 | /// Update the CryptoMethod and BitMasks for the encrypted partition
855 | ///
856 | /// Cart representing the 3DS file
857 | /// Index of the partition
858 | /// Stream representing the output
859 | private static void UpdateEncryptCryptoAndMasks(N3DS cart, int index, Stream writer)
860 | {
861 | // Get required offsets
862 | uint partitionOffset = cart.GetPartitionOffset(index);
863 |
864 | // Get the backup header
865 | var backupHeader = cart.BackupHeader;
866 | if (backupHeader?.Flags == null)
867 | return;
868 |
869 | // Seek to the CryptoMethod location
870 | writer.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
871 |
872 | // Write the new CryptoMethod
873 | // - For partitions 1 and up, set crypto-method to 0x00
874 | // - If partition 0, restore crypto-method from backup flags
875 | byte cryptoMethod = index > 0 ? (byte)CryptoMethod.Original : (byte)backupHeader.Flags.CryptoMethod;
876 | writer.Write(cryptoMethod);
877 | writer.Flush();
878 |
879 | // Seek to the BitMasks location
880 | writer.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
881 |
882 | // Write the new BitMasks flag
883 | BitMasks flag = cart.GetBitMasks(index);
884 | flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF;
885 | flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupHeader.Flags.BitMasks;
886 | writer.Write((byte)flag);
887 | writer.Flush();
888 | }
889 |
890 | #endregion
891 |
892 | #region Info
893 |
894 | ///
895 | public string? GetInformation(string filename)
896 | {
897 | try
898 | {
899 | // Open the file for reading
900 | using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
901 |
902 | // Deserialize the cart information
903 | var cart = N3DS.Create(input);
904 | if (cart?.Model == null)
905 | return "Error: Not a 3DS cart image!";
906 |
907 | // Get a string builder for the status
908 | var sb = new StringBuilder();
909 |
910 | // Iterate over all 8 NCCH partitions
911 | for (int p = 0; p < 8; p++)
912 | {
913 | bool decrypted = cart.PossiblyDecrypted(p);
914 | sb.AppendLine($"\tPartition {p}: {(decrypted ? "Decrypted" : "Encrypted")}");
915 | }
916 |
917 | // Return the status for all partitions
918 | return sb.ToString();
919 | }
920 | catch (Exception ex)
921 | {
922 | Console.WriteLine(ex);
923 | return null;
924 | }
925 | }
926 |
927 | #endregion
928 | }
929 | }
930 |
--------------------------------------------------------------------------------