├── 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 | [![Build and Test](https://github.com/SabreTools/NDecrypt/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/SabreTools/NDecrypt/actions/workflows/build_and_test.yml) 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 | --------------------------------------------------------------------------------