250 Commits
0.1.2 ... 0.3.2

Author SHA1 Message Date
Matt Nadareski
31b40e6f10 Bump version 2025-04-04 12:43:54 -04:00
Matt Nadareski
69a07cde85 Change table columns to match sections 2025-04-04 12:04:02 -04:00
Matt Nadareski
b9b4d6876c Include native libraries in executable for publish 2025-04-03 12:16:17 -04:00
Matt Nadareski
bed965c6dd Add script link to README, thanks Dimensional 2025-04-02 17:01:01 -04:00
Matt Nadareski
f01c04d796 Be consistent with language for once 2025-04-02 13:35:42 -04:00
Matt Nadareski
5de970965e README overhaul and minor flag update 2025-04-02 13:34:19 -04:00
Matt Nadareski
f3e4aa0b7e Slight cleanup of notes in README 2025-04-02 12:55:29 -04:00
Matt Nadareski
e2110e80c0 Add keys.bin order for now 2025-04-02 11:52:18 -04:00
Matt Nadareski
b0f2c6658a Add keys.conf mappings to README 2025-04-02 11:50:24 -04:00
Matt Nadareski
baf0b9045e Remove old note, add new note 2025-04-02 11:15:37 -04:00
Matt Nadareski
b82eb54aab Do something with the results 2025-04-02 11:10:48 -04:00
Matt Nadareski
7d541b6c4b Document unlicensed DS values 2025-04-02 10:50:38 -04:00
Matt Nadareski
9d72a25cc2 Relax validation on NDS 2025-04-01 20:25:07 -04:00
Matt Nadareski
79e8c1b6bf Add debug prefix for all key validation 2025-04-01 20:12:29 -04:00
Matt Nadareski
8f6df04e2c Remove weirdly-phrased time limitation 2025-04-01 16:54:24 -04:00
Matt Nadareski
466144f8c9 Move default config to root level 2025-04-01 16:50:52 -04:00
Matt Nadareski
629f2cc11e Fix old .NET build 2025-04-01 16:47:03 -04:00
Matt Nadareski
1ffd1a3bff Look for files in places with things (fixes #19) 2025-04-01 16:43:21 -04:00
Matt Nadareski
d4bcd62941 Add more config stuff to README 2025-04-01 14:12:25 -04:00
Matt Nadareski
702c31413b Add minimal documentation of new input 2025-04-01 14:06:31 -04:00
Matt Nadareski
d4a6e902cf Add NED validation 2025-04-01 13:58:55 -04:00
Matt Nadareski
c00e72506e Prefer config file, if exists (nodoc) 2025-04-01 13:46:34 -04:00
Matt Nadareski
7ccc160e90 Prepare configuration for future use (unused) 2025-04-01 13:30:18 -04:00
Matt Nadareski
068b83da8e Add default configuration file (unused) 2025-04-01 13:11:26 -04:00
Matt Nadareski
d429ea3e64 Remove now-obsolete TODO 2025-04-01 13:05:05 -04:00
Matt Nadareski
5bf1d85e05 Add key validation (unused) 2025-04-01 13:04:52 -04:00
Matt Nadareski
32b655b1f9 Reread decrypted ExeFS headers (fixes #20) 2025-04-01 12:33:05 -04:00
Matt Nadareski
e3a64ece96 Fix stray reference to old AES keys param 2025-04-01 11:59:19 -04:00
Matt Nadareski
74986afc38 Start wiring through configuration file 2025-04-01 11:58:23 -04:00
Matt Nadareski
3cc2bcf022 Remove support for old "citra" flag name 2025-04-01 11:45:13 -04:00
Matt Nadareski
ebd13e3728 Update README 2025-04-01 11:44:29 -04:00
Matt Nadareski
2d7a54ddb7 Create unused Configuration class 2025-04-01 11:42:25 -04:00
Matt Nadareski
60d989d899 Move more encryption vars to common location 2025-04-01 11:23:27 -04:00
Matt Nadareski
4de3cfe6fa Fix how conditions are used for references 2025-02-25 21:26:08 -05:00
Matt Nadareski
50034975c3 Add newline for DSTool info output 2025-02-19 21:15:58 -05:00
Matt Nadareski
ab4f076846 Use tabs before info lines for readability in multi-file scenarios 2025-02-19 21:10:52 -05:00
Matt Nadareski
08840055a4 The British are coming! 2025-02-19 20:55:55 -05:00
Matt Nadareski
02eafc688a Let info writing also write hashes 2025-02-19 15:51:46 -05:00
Matt Nadareski
db7c7ee273 Add simple info check (fixes #18) 2025-02-19 15:47:05 -05:00
Matt Nadareski
383e6bdfce Add info to interface; add start of implementations 2025-02-19 15:36:36 -05:00
Matt Nadareski
169bd48762 Add info skeleton 2025-02-19 15:32:28 -05:00
Matt Nadareski
a2ab3c25c9 Convert encrypt/decrypt bool to an enum 2025-02-19 15:30:59 -05:00
Matt Nadareski
abf0843d22 Update copyright 2024-12-30 21:32:22 -05:00
Matt Nadareski
ca64a2575e Remove unnecessary action step 2024-12-30 21:32:01 -05:00
Matt Nadareski
035760b5f5 Ensure .NET versions are installed for testing 2024-12-30 21:31:43 -05:00
Matt Nadareski
66e6a8cd4a Allow symbols to be packed 2024-12-30 21:31:01 -05:00
Matt Nadareski
d85e7656c4 Update packages 2024-12-30 21:29:34 -05:00
Matt Nadareski
942151b6d7 Add .cci as a recognized extension for 3DS 2024-12-13 09:51:39 -05:00
Matt Nadareski
d7a51c7798 Ensure publish script is executable 2024-12-06 12:26:44 -05:00
Matt Nadareski
bd86b95494 Use publish script and update README 2024-12-06 12:24:31 -05:00
Matt Nadareski
5562b403a4 Use wrappers to be more safe 2024-11-28 22:06:52 -05:00
Matt Nadareski
ead70df624 Support back to .NET Framework 2.0 2024-11-18 16:20:09 -05:00
Matt Nadareski
33e62e046b Make Core an unpublished package 2024-11-14 21:59:04 -05:00
Matt Nadareski
bb0b5a3d05 Remove all CIA references
The implementation was woefully incomplete to begin with, and if working with 3DS cart images has taught anything, making sure that it can deserialize properly needs to be done first. Once CIA support can be started again, it should be built from the ground up.
2024-11-14 21:53:31 -05:00
Matt Nadareski
6a094b2dd8 Bump version 2024-11-14 21:40:01 -05:00
Matt Nadareski
0e3d22020b Fix remaining offset and size issues 2024-11-14 21:39:20 -05:00
Matt Nadareski
695f0f44b6 Ensure ExeFS table doesn't finalize 2024-11-14 21:25:38 -05:00
Matt Nadareski
dc4bd15e04 Fix incorrect size being used 2024-11-14 21:17:35 -05:00
Matt Nadareski
ce54eb24e2 Fix more issues 2024-11-14 13:43:07 -05:00
Matt Nadareski
15961e7047 Wording change 2024-11-14 11:56:18 -05:00
Matt Nadareski
edd8ebc048 Update Serialization to 1.7.3 2024-11-14 11:44:41 -05:00
Matt Nadareski
23e9edbf69 Remove null assurances 2024-11-14 02:16:50 -05:00
Matt Nadareski
00ac5e1ca2 Split some steps for readability 2024-11-14 01:56:06 -05:00
Matt Nadareski
c79781a6d7 Keep fixing byte array math 2024-11-14 01:48:14 -05:00
Matt Nadareski
5aa50ee252 Fix IV and addition 2024-11-14 00:26:58 -05:00
Matt Nadareski
3fbaedd7d5 Reduce unnecessary methods 2024-11-13 21:15:44 -05:00
Matt Nadareski
bc31cb0f6a Start fixing issues, remove BigInteger 2024-11-13 21:11:26 -05:00
Matt Nadareski
3ef32748e9 Remove temp code 2024-11-13 14:51:16 -05:00
Matt Nadareski
41293ab7c5 Add byte array helpers 2024-11-13 14:41:32 -05:00
Matt Nadareski
7a5593dd72 Bump version 2024-11-13 12:27:54 -05:00
Matt Nadareski
326b747204 Add .NET 9 to target frameworks 2024-11-13 12:24:36 -05:00
Matt Nadareski
ac9fc92e06 Should be debug only 2024-11-05 14:09:00 -05:00
Matt Nadareski
8a2d91aace Attempt to reduce nesting in GHA builds 2024-11-05 13:57:50 -05:00
Matt Nadareski
add3d28488 Remove unnecessary code region 2024-10-14 00:24:39 -04:00
Matt Nadareski
bd078e4585 Reduce unnecessary namespace nesting 2024-10-14 00:22:44 -04:00
Matt Nadareski
9f5ffaf035 Move various extensions to Serialization 2024-10-14 00:15:26 -04:00
Matt Nadareski
3785426789 Not sure how those slipped through 2024-10-14 00:06:52 -04:00
Matt Nadareski
ee48155c77 Make usings make more sense 2024-10-14 00:05:41 -04:00
Matt Nadareski
c67277caa6 Fix formatting issue in DSTool 2024-10-14 00:03:33 -04:00
Matt Nadareski
cced3b5b3d Even further consolidation 2024-10-14 00:02:59 -04:00
Matt Nadareski
4c56377d7e Reduce unnessary code found after move 2024-10-13 23:59:44 -04:00
Matt Nadareski
201098e32c Re-consolidate internal libraries
These were originally split out from each other when the models for each of the different cart and digital types were included in this project. After it got split out, and after a lot of recent changes, it was apparent that this split was no longer necessary.
2024-10-13 23:50:22 -04:00
Matt Nadareski
8ec0307e1c Move DecryptArgs to N3DS namespace 2024-10-13 23:45:53 -04:00
Matt Nadareski
864cb57d30 Slight cleanup of PartitionKeys 2024-10-13 23:42:00 -04:00
Matt Nadareski
7576608111 Slight formatting tweak 2024-10-13 23:31:47 -04:00
Matt Nadareski
e3f740e115 Ensure better accessors 2024-10-13 23:31:13 -04:00
Matt Nadareski
4eaa56212f Missed one comment 2024-10-13 23:09:43 -04:00
Matt Nadareski
6ef543ee1a That was tiring 2024-10-13 23:08:48 -04:00
Matt Nadareski
5a4e747329 Almost there... 2024-10-13 23:05:34 -04:00
Matt Nadareski
4a35d490d5 Stay on target... 2024-10-13 23:01:36 -04:00
Matt Nadareski
2983e21330 One more step 2024-10-13 22:56:32 -04:00
Matt Nadareski
ead4f52f3c Bring CIA up to par with 3DS 2024-10-13 22:49:31 -04:00
Matt Nadareski
72afaeb010 Migrate more to split code 2024-10-13 22:10:59 -04:00
Matt Nadareski
5148ba5d83 Create and use CommonOperations for 3DS 2024-10-13 22:02:14 -04:00
Matt Nadareski
ccf746ca7c Better split encrypt/decrypt code 2024-10-13 20:52:04 -04:00
Matt Nadareski
656b8158e9 Slight formatting change 2024-10-13 20:41:02 -04:00
Matt Nadareski
263ca9b6bf Replace partitionIndex with index 2024-10-13 20:37:54 -04:00
Matt Nadareski
158fcfff36 Make more helper functions 2024-10-13 20:36:42 -04:00
Matt Nadareski
6e13e9e4c4 Make CIATool consistent with ThreeDSTool 2024-10-13 11:56:03 -04:00
Matt Nadareski
6d5bc3dbcf Be more consistent with naming 2024-10-13 11:50:52 -04:00
Matt Nadareski
c73e971704 Use new PartitionKeys class 2024-10-13 11:48:30 -04:00
Matt Nadareski
6c575d0157 Create new PartitionKeys class 2024-10-13 11:40:29 -04:00
Matt Nadareski
5be07157e8 Move FileType to separate file 2024-10-13 11:26:16 -04:00
Matt Nadareski
7d069d6bf7 Slight tweak for better readability 2024-10-13 11:24:13 -04:00
Matt Nadareski
46b60559a2 Rename private fields 2024-10-13 11:21:24 -04:00
Matt Nadareski
849315076c Rename private fields 2024-10-13 11:20:58 -04:00
Matt Nadareski
f6f3cd062e Make decrypt args slightly nicer 2024-10-13 11:19:56 -04:00
Matt Nadareski
3dac146451 It's okay, it's complex again 2024-10-12 02:44:44 -04:00
Matt Nadareski
af226e347c Support .dec extension 2024-10-12 02:34:31 -04:00
Matt Nadareski
37f27e41cf Slight redo of comments 2024-10-12 02:30:03 -04:00
Matt Nadareski
3742a5c29e Simplify decrypt args creation 2024-10-12 02:27:45 -04:00
Matt Nadareski
2e7bc64e26 Yeah, sure 2024-10-12 02:25:32 -04:00
Matt Nadareski
af14f829b0 Development is a flag, not food 2024-10-12 02:20:17 -04:00
Matt Nadareski
93d34c3b9c Change warning text 2024-10-12 02:09:44 -04:00
Matt Nadareski
51b690bb6d Rename flag but keep old one in code for now 2024-10-12 02:08:23 -04:00
Matt Nadareski
bd1d6935d8 Reduce passed around values 2024-10-12 02:06:51 -04:00
Matt Nadareski
08a1dc5e42 Derive key file in a safer way 2024-10-12 01:58:07 -04:00
Matt Nadareski
77407f54fb Only initialize for 3DS 2024-10-12 01:46:45 -04:00
Matt Nadareski
2eb969b6ac Slightly rename _decryptArgs 2024-10-12 01:43:04 -04:00
Matt Nadareski
747625c2b8 Path is part of the args now 2024-10-12 01:42:02 -04:00
Matt Nadareski
993ce492b7 Remove now-unused field from DSTool 2024-10-12 01:33:16 -04:00
Matt Nadareski
3b33e3d370 Force is not a decryption argument 2024-10-12 01:32:32 -04:00
Matt Nadareski
36ae14a997 Reduce reliance in decrypt args in 3DSTool 2024-10-12 01:26:45 -04:00
Matt Nadareski
4e0a582269 Reduce reliance in decrypt args in DSTool 2024-10-12 01:24:30 -04:00
Matt Nadareski
9f018ca73b Split encrypt and decrypt in DS 2024-10-12 01:07:00 -04:00
Matt Nadareski
3f07271e7f Move encrypt flag out of args 2024-10-12 00:59:07 -04:00
Matt Nadareski
84b62e2c52 Remove unused compatiblity packages 2024-10-12 00:32:24 -04:00
Matt Nadareski
041d5b633e Remove reliance on BinaryReader/Writer 2024-10-12 00:30:02 -04:00
Matt Nadareski
54080b3ef1 Simplify ExeFS reading 2024-10-12 00:18:00 -04:00
Matt Nadareski
32b430fafa Update libraries 2024-10-12 00:15:00 -04:00
Matt Nadareski
450f6fb267 Fix issue in Windows build script 2024-08-08 19:55:48 -04:00
Matt Nadareski
96f9715dff Missed a couple 2024-08-08 16:15:19 -04:00
Matt Nadareski
96ed279db5 Standardize comments in 3DS 2024-08-08 16:06:40 -04:00
Matt Nadareski
344d69ca52 Make 3DS code more readable 2024-08-08 15:49:36 -04:00
Matt Nadareski
0aa946378a Update SabreTools.Models 2024-08-08 14:55:07 -04:00
Matt Nadareski
6ec5c69e27 Use var because I can 2024-08-08 14:06:52 -04:00
Matt Nadareski
45de58d2f1 Simplify using statements 2024-08-08 14:01:42 -04:00
Matt Nadareski
10f1532bb1 Split NDS serialization methods 2024-08-08 13:40:45 -04:00
Matt Nadareski
45f5e1ffff Clean up serialization methods 2024-08-08 13:32:30 -04:00
Matt Nadareski
9fad439ab8 Use proper model name 2024-08-08 13:17:50 -04:00
Matt Nadareski
179b53f462 Migrate 3DS to using standard models 2024-08-08 11:53:47 -04:00
Matt Nadareski
c52d3de426 Reorganize NDS 2024-08-08 09:38:02 -04:00
Matt Nadareski
36393f7950 Migrate NDS to using standard models 2024-08-08 09:15:50 -04:00
Matt Nadareski
af8a8eb72d Bump to net8.0 for launch 2024-07-20 22:23:13 -04:00
Matt Nadareski
5040de04e5 Make aes_keys keys more obvious 2024-07-20 22:21:23 -04:00
Matt Nadareski
78b168e4ce Fix that funky formatting 2024-07-20 22:15:08 -04:00
Matt Nadareski
2df30d095b Modernize NDecrypt 2024-07-20 22:00:54 -04:00
spiritfader
e2c2c804a5 add support for .nds.enc extension (#16)
* add support for.nds.enc extension

* Update Program.cs
2024-03-31 16:36:39 -07:00
Matt Nadareski
ce13c34b0a Support and skip #-prefixed comments (fixes #13) 2024-02-18 23:03:46 -05:00
soxhi8
71199ee94a Fix opeation typo (#10) 2023-02-21 09:37:33 -08:00
Matt Nadareski
1159b677bc Bump version to 0.2.5 2023-01-06 10:14:31 -08:00
Matt Nadareski
6527d85f29 Commit hash only 2023-01-06 10:01:23 -08:00
Matt Nadareski
ff815299fb Try using a different variable 2023-01-06 09:56:43 -08:00
Matt Nadareski
c04de5700a Use different formatting for version string 2023-01-06 09:52:38 -08:00
Matt Nadareski
ab9d32f56a Attempt to use commit hash in naming 2023-01-06 09:51:40 -08:00
Matt Nadareski
8151f306ec Experiment adding version numbers 2023-01-06 09:46:37 -08:00
Matt Nadareski
fd9b94d428 Fix artifact naming 2023-01-06 09:41:43 -08:00
Matt Nadareski
9cc2b6befe Replace assembly location 2023-01-06 09:40:52 -08:00
Matt Nadareski
8c2d4d701a Move AppVeyor script, oops 2023-01-06 09:36:31 -08:00
Matt Nadareski
593e8b2b4d Add build status to README 2023-01-06 09:33:45 -08:00
Matt Nadareski
71f3295a2b Add AppVeyor config 2023-01-06 09:32:55 -08:00
Matt Nadareski
a29071e697 Modernize NDecrypt 2023-01-06 09:31:42 -08:00
Matt Nadareski
96c3c3732c Set license properly to MIT
MIT most closely matches the statement that was included previously in the README about an extremely permissive license.
2023-01-06 09:27:41 -08:00
Matt Nadareski
c8f8a7d99c Bump version to 0.2.4 2022-10-31 10:30:52 -07:00
Matt Nadareski
3ee8c2e79b Update csproj files with full info 2022-10-31 10:27:56 -07:00
Matt Nadareski
7d263886da Force reader buffer refresh to omit stale data 2022-10-27 16:41:22 -07:00
Matt Nadareski
6d1b23ffec Be less clever in code to make debugging easier 2022-10-27 15:28:34 -07:00
Matt Nadareski
92d1c3c5e5 Split NDecrypt.Library; add build targets 2022-10-27 11:27:13 -07:00
Matt Nadareski
855a353e90 Reverse library and exe naming 2022-10-27 11:06:06 -07:00
Matt Nadareski
6dac45f973 Update BouncyCastle to 1.9.0 2022-09-27 13:35:07 -07:00
Matt Nadareski
a6337e0864 Drop .NET 5 support 2022-09-27 12:27:16 -07:00
Matt Nadareski
778d5d4ffc Fix hashing because of .NET 6 2022-09-27 12:19:53 -07:00
Matt Nadareski
16a84f0dc6 .NET 6.0, .NET Standard 2.0, Cleanup 2022-04-17 13:10:55 -07:00
Matt Nadareski
7fbe044ab8 Read ContentIndex data, just in case 2021-09-28 22:23:04 -07:00
Matt Nadareski
7c9a035d7c Cleanup and add note 2021-09-28 22:19:23 -07:00
Matt Nadareski
964e6f243c Fill in all methods... with a lot of TODOs 2021-09-28 22:14:40 -07:00
Matt Nadareski
b454e42f8e Start filling in more methods for CIA 2021-09-28 21:54:31 -07:00
Matt Nadareski
9f70eae8be Fix reading NCCH data in CIA header 2021-09-28 21:48:58 -07:00
Matt Nadareski
20987573f4 Fix build 2021-09-28 21:37:12 -07:00
Matt Nadareski
2fb05b577e Fix reading CIA header info 2021-09-28 21:32:30 -07:00
Matt Nadareski
f72b8c3506 Rename NDS -> Nitro 2021-09-27 10:56:49 -07:00
Matt Nadareski
8b32b21475 Better reading of Ticket limits 2021-09-27 10:51:44 -07:00
Matt Nadareski
2779f5ef72 Better certificate chain reading 2021-09-27 10:42:54 -07:00
Matt Nadareski
bbbf71a603 Fill in more CIA-specific objects 2021-09-27 10:26:22 -07:00
Matt Nadareski
bea979dca5 Add CIA notes 2021-09-26 23:31:39 -07:00
Matt Nadareski
5a0169310b Better wording 2021-09-26 23:20:06 -07:00
Matt Nadareski
67c83bd126 Make note a little more apparent 2021-09-26 22:52:12 -07:00
Matt Nadareski
6fdcc2effe Add Citra names to README 2021-09-26 22:38:14 -07:00
Matt Nadareski
9ff0205e15 Add keyfile path input 2021-09-26 22:35:10 -07:00
Matt Nadareski
c91d7f2708 Separate out CMD portion
This also introduces a new helper class for holding all of the decryption args. This makes it so that the method signatures don't have to change so much.
2021-09-26 22:22:17 -07:00
Matt Nadareski
d9b333e3ba Start adding Citra keyfile support 2021-09-26 21:36:55 -07:00
Matt Nadareski
ef5a01edf7 Add CIA header (nw) 2021-07-22 22:36:05 -07:00
Matt Nadareski
012787a5b1 Hook up hashing, fix string output 2021-02-08 22:13:30 -08:00
Matt Nadareski
df5664fa1f Add hashing code (not hooked up) 2021-02-08 21:54:23 -08:00
Matt Nadareski
bef252f57f Merge pull request #4 from Icyelut/fix-README
Fix typos in README that suggests the wrong keyslots
2021-01-26 21:04:01 -08:00
Icyelut
b78db22464 Fix typo in README that suggests the wrong dev keyslot
Signed-off-by: Icyelut <PanetGhoul@protonmail.com>
2021-01-11 20:46:29 +09:00
Icyelut
6d980c94fd Fix typo in README that suggests a keyslot that doesn't exist
Signed-off-by: Icyelut <PanetGhoul@protonmail.com>
2021-01-11 19:27:38 +09:00
Matt Nadareski
8452cb4fc0 Update compatibilty numers (100%!!!) 2020-12-19 21:43:56 -08:00
Matt Nadareski
0deaa0aed3 Tools to proper namespaces 2020-12-17 22:06:30 -08:00
Matt Nadareski
9b5f90a750 A bit of code cleanup, better communication 2020-12-17 22:02:04 -08:00
Matt Nadareski
7415a21d5c Try/catch blocks around file processing 2020-12-17 11:30:34 -08:00
Matt Nadareski
f349389994 keys.bin is only needed for 3DS, better docs 2020-12-17 11:08:53 -08:00
Matt Nadareski
a046bd4152 Remove forced pause 2020-12-17 10:39:30 -08:00
Matt Nadareski
51a8f0c9df Report new compatibility information 2020-12-17 10:37:34 -08:00
Matt Nadareski
0449c16a01 Yet another empty one 2020-12-16 19:39:33 -08:00
Matt Nadareski
7638d7dbf8 One more decrypted empty secure area 2020-12-16 16:27:39 -08:00
Matt Nadareski
387bf46e5a Fix incorrect matching criteria 2020-12-16 16:07:45 -08:00
Matt Nadareski
6d257dc1e3 Add iQue extension for NDS 2020-12-16 13:15:49 -08:00
Matt Nadareski
91d816e359 Handle PoP cases, add compatibility to README 2020-12-16 11:57:55 -08:00
Matt Nadareski
e90f7a76af One more comment 2020-12-15 14:33:30 -08:00
Matt Nadareski
9750ae5a1e Add DQ5 DS secure area values (fixes #1) 2020-12-15 14:27:14 -08:00
Matt Nadareski
fa518ed5a5 Make method private 2020-12-15 13:51:34 -08:00
Matt Nadareski
350acd7be4 Each system in own namespace 2020-12-15 13:45:19 -08:00
Matt Nadareski
dde0c96e6c Re-merge projects 2020-12-15 13:30:28 -08:00
Matt Nadareski
36e6e803cc Fix build, add modern builds 2020-12-15 12:01:21 -08:00
Matt Nadareski
e669936e08 Add new versions with only Read 2020-12-14 23:29:08 -08:00
Matt Nadareski
dc55511a84 Add a couple proto checks, better logs for weird cases 2020-10-11 23:31:37 -07:00
Matt Nadareski
b9b8c76e84 Add force flag for both NDS and N3DS 2020-10-11 23:13:23 -07:00
Matt Nadareski
10fcd51e10 This annoyed me 2019-09-16 22:48:02 -07:00
Matt Nadareski
a0878d0bf4 Better handle 2GiB+ games
Thanks to Aringon for reporting the issue
2019-09-16 22:36:20 -07:00
Matt Nadareski
ed55f76d1e Better define keys.bin 2019-04-11 23:50:00 -07:00
Matt Nadareski
a726fc26d9 Code rearrange 2019-04-11 22:06:07 -07:00
Matt Nadareski
414df7808e Fix readme 2019-04-11 21:49:17 -07:00
Matt Nadareski
d61e03687b Add readme 2019-04-11 21:25:15 -07:00
Matt Nadareski
cccb4e6261 Remove constants from code 2019-04-11 16:58:02 -07:00
Matt Nadareski
df7ff2b95a Make main program a little better 2019-04-11 15:42:52 -07:00
Matt Nadareski
52a928638b Add other known blank values 2019-04-11 15:28:10 -07:00
Matt Nadareski
5962fc6072 Better comments 2019-04-11 15:02:01 -07:00
Matt Nadareski
5807d4ddc2 Handle empty secure area, better 2019-04-11 14:59:05 -07:00
Matt Nadareski
6fa8867f93 Empty secure area 2019-04-11 14:43:39 -07:00
Matt Nadareski
ed45d2d14e Add edge case decrypt values 2019-04-11 14:15:11 -07:00
Matt Nadareski
69ecb0d379 Fix name 2019-04-11 12:55:41 -07:00
Matt Nadareski
5974e2371e Rename stuff, add SRL extension 2019-04-11 11:17:21 -07:00
Matt Nadareski
1e5e3badbc Start of cleanup 2019-04-11 10:02:38 -07:00
Matt Nadareski
ef02b1c33a Rename to NDecrypt, Add NDS encrypt/decrypt 2019-04-11 03:01:13 -07:00
Matt Nadareski
32e8bab2a6 Math is hard 2019-04-08 21:08:36 -07:00
Matt Nadareski
bd1c6f8b51 Processing in right files 2019-04-08 20:28:14 -07:00
Matt Nadareski
30da008cec Processing shunting 2019-04-08 02:09:33 -07:00
Matt Nadareski
aa7566c312 Separate out helper methods 2019-04-08 01:13:51 -07:00
Matt Nadareski
56a5f4951b Set proper keys 2019-04-08 01:07:08 -07:00
Matt Nadareski
4b71cd621f Code shared too much 2019-04-08 00:55:57 -07:00
Matt Nadareski
4042b1c216 Rename IO, move common case 2019-04-08 00:17:37 -07:00
Matt Nadareski
26c7a34e98 Read and use full headers 2019-04-08 00:09:18 -07:00
Matt Nadareski
dc68aa5046 Summary overhaul 2019-04-07 22:45:27 -07:00
Matt Nadareski
ff486d8613 More modularization of shared code 2019-04-07 21:21:34 -07:00
Matt Nadareski
941f7d5191 Modularization, cleanup 2019-04-07 13:54:25 -07:00
Matt Nadareski
b83293e8c9 Fix encrypt 2019-04-07 03:04:05 -07:00
Matt Nadareski
0190f3d316 Encrypt, comments 2019-04-07 02:10:16 -07:00
Matt Nadareski
5c6b303d41 DoFinal 2019-04-07 02:03:44 -07:00
Matt Nadareski
0800d93d22 Notes and minor 2019-04-07 01:32:04 -07:00
46 changed files with 3943 additions and 1616 deletions

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

@@ -0,0 +1,40 @@
name: Build and Test
on:
push:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Run tests
run: dotnet test
- name: Run publish script
run: ./publish-nix.sh -d
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: "*.nupkg,*.snupkg,*.zip"
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True

20
.github/workflows/check_pr.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Build PR
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Build
run: dotnet build

5
.gitignore vendored
View File

@@ -258,4 +258,7 @@ paket-files/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
*.pyc
# VSCode
/NDecrypt/Properties/launchSettings.json

27
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,27 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/NDecrypt/bin/Debug/net9.0/NDecrypt.dll",
"args": [],
"cwd": "${workspaceFolder}/NDecrypt",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

24
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "shell",
"args": [
"build",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:NoSummary"
],
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -1,25 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2006
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "3DSDecrypt", "3DSDecrypt\3DSDecrypt.csproj", "{2E30006A-3C60-4576-A262-937B21C83C06}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2E30006A-3C60-4576-A262-937B21C83C06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E30006A-3C60-4576-A262-937B21C83C06}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E30006A-3C60-4576-A262-937B21C83C06}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E30006A-3C60-4576-A262-937B21C83C06}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5AB50D9B-BA18-4F96-804B-52E7E0845B37}
EndGlobalSection
EndGlobal

View File

@@ -1,75 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2E30006A-3C60-4576-A262-937B21C83C06}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>_3DSDecrypt</RootNamespace>
<AssemblyName>3DSDecrypt</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Reference Include="BouncyCastle.Crypto, Version=1.8.4.0, Culture=neutral, PublicKeyToken=0e99375e54769942">
<HintPath>..\packages\BouncyCastle.1.8.4\lib\BouncyCastle.Crypto.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Data\Constants.cs" />
<Compile Include="Headers\NCCHHeaderFlags.cs" />
<Compile Include="ThreeDSTool.cs" />
<Compile Include="Data\Enums.cs" />
<Compile Include="Headers\AccessControlInfo.cs" />
<Compile Include="Headers\ARM11KernelCapabilities.cs" />
<Compile Include="Headers\ARM11LocalSystemCapabilities.cs" />
<Compile Include="Headers\ARM9AccessControl.cs" />
<Compile Include="Headers\CodeSetInfo.cs" />
<Compile Include="Headers\CXIExtendedHeader.cs" />
<Compile Include="Headers\ExeFSFileHeader.cs" />
<Compile Include="Headers\ExeFSHeader.cs" />
<Compile Include="Headers\NCCHHeader.cs" />
<Compile Include="Headers\NCSDHeader.cs" />
<Compile Include="Headers\PartitionTableEntry.cs" />
<Compile Include="Headers\StorageInfo.cs" />
<Compile Include="Headers\SystemControlInfo.cs" />
<Compile Include="Headers\SystemInfo.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -1,66 +0,0 @@
using System.Numerics;
namespace ThreeDS.Data
{
public static class Constants
{
// Setup Keys and IVs
public static byte[] PlainCounter = new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static byte[] ExefsCounter = new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static byte[] RomfsCounter = new byte[] { 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// Big Endian - 0x1F, 0xF9, 0xE9, 0xAA, 0xC5, 0xFE, 0x04, 0x08, 0x02, 0x45, 0x91, 0xDC, 0x5D, 0x52, 0x76, 0x8A
// Little Endian - 0x8A, 0x76, 0x52, 0x5D, 0xDC, 0x91, 0x45, 0x02, 0x08, 0x04, 0xFE, 0xC5, 0xAA, 0xE9, 0xF9, 0x1F
public static BigInteger AESHardwareConstant = new BigInteger(new byte[] { 0x8A, 0x76, 0x52, 0x5D, 0xDC, 0x91, 0x45, 0x02, 0x08, 0x04, 0xFE, 0xC5, 0xAA, 0xE9, 0xF9, 0x1F });
#region Retail keys
// KeyX 0x18 (New 3DS 9.3)
// Big Endian - 0x82, 0xE9, 0xC9, 0xBE, 0xBF, 0xB8, 0xBD, 0xB8, 0x75, 0xEC, 0xC0, 0xA0, 0x7D, 0x47, 0x43, 0x74
// Little Endian - 0x74, 0x43, 0x47, 0x7D, 0xA0, 0xC0, 0xEC, 0x75, 0xB8, 0xBD, 0xB8, 0xBF, 0xBE, 0xC9, 0xE9, 0x82
public static BigInteger KeyX0x18 = new BigInteger(new byte[] { 0x74, 0x43, 0x47, 0x7D, 0xA0, 0xC0, 0xEC, 0x75, 0xB8, 0xBD, 0xB8, 0xBF, 0xBE, 0xC9, 0xE9, 0x82, 0x00 });
// KeyX 0x1B (New 3DS 9.6)
// Big Endian - 0x45, 0xAD, 0x04, 0x95, 0x39, 0x92, 0xC7, 0xC8, 0x93, 0x72, 0x4A, 0x9A, 0x7B, 0xCE, 0x61, 0x82
// Little Endian - 0x82, 0x61, 0xCE, 0x7B, 0x9A, 0x4A, 0x72, 0x93, 0xC8, 0xC7, 0x92, 0x39, 0x95, 0x04, 0xAD, 0x45
public static BigInteger KeyX0x1B = new BigInteger(new byte[] { 0x82, 0x61, 0xCE, 0x7B, 0x9A, 0x4A, 0x72, 0x93, 0xC8, 0xC7, 0x92, 0x39, 0x95, 0x04, 0xAD, 0x45, 0x00 });
// KeyX 0x25 (> 7.x)
// Big Endian - 0xCE, 0xE7, 0xD8, 0xAB, 0x30, 0xC0, 0x0D, 0xAE, 0x85, 0x0E, 0xF5, 0xE3, 0x82, 0xAC, 0x5A, 0xF3
// Little Endian - 0xF3, 0x5A, 0xAC, 0x82, 0xE3, 0xF5, 0x0E, 0x85, 0xAE, 0x0D, 0xC0, 0x30, 0xAB, 0xD8, 0xE7, 0xCE
public static BigInteger KeyX0x25 = new BigInteger(new byte[] { 0xF3, 0x5A, 0xAC, 0x82, 0xE3, 0xF5, 0x0E, 0x85, 0xAE, 0x0D, 0xC0, 0x30, 0xAB, 0xD8, 0xE7, 0xCE, 0x00 });
// Dev KeyX 0x2C (< 6.x)
// Big Endian - 0xB9, 0x8E, 0x95, 0xCE, 0xCA, 0x3E, 0x4D, 0x17, 0x1F, 0x76, 0xA9, 0x4D, 0xE9, 0x34, 0xC0, 0x53
// Little Endian - 0x53, 0xC0, 0x34, 0xE9, 0x4D, 0xA9, 0x76, 0x1F, 0x17, 0x4D, 0x3E, 0xCA, 0xCE, 0x95, 0x8E, 0xB9
public static BigInteger KeyX0x2C = new BigInteger(new byte[] { 0x53, 0xC0, 0x34, 0xE9, 0x4D, 0xA9, 0x76, 0x1F, 0x17, 0x4D, 0x3E, 0xCA, 0xCE, 0x95, 0x8E, 0xB9, 0x00 });
#endregion
#region Dev Keys (Must be converted to LE)
// Dev KeyX 0x18 (New 3DS 9.3)
// Big Endian - 0x30, 0x4B, 0xF1, 0x46, 0x83, 0x72, 0xEE, 0x64, 0x11, 0x5E, 0xBD, 0x40, 0x93, 0xD8, 0x42, 0x76
// Little Endian - 0x76, 0x42, 0xD8, 0x93, 0x40, 0xBD, 0x5E, 0x11, 0x64, 0xEE, 0x72, 0x83, 0x46, 0xF1, 0x4B, 0x30
public static BigInteger DevKeyX0x18 = new BigInteger(new byte[] { 0x76, 0x42, 0xD8, 0x93, 0x40, 0xBD, 0x5E, 0x11, 0x64, 0xEE, 0x72, 0x83, 0x46, 0xF1, 0x4B, 0x30, 0x00 });
// Dev KeyX 0x1B New 3DS 9.6)
// Big Endian - 0x6C, 0x8B, 0x29, 0x44, 0xA0, 0x72, 0x60, 0x35, 0xF9, 0x41, 0xDF, 0xC0, 0x18, 0x52, 0x4F, 0xB6
// Little Endian - 0xB6, 0x4F, 0x52, 0x18, 0xC0, 0xDF, 0x41, 0xF9, 0x35, 0x60, 0x72, 0xA0, 0x44, 0x29, 0x8B, 0x6C
public static BigInteger DevKeyX0x1B = new BigInteger(new byte[] { 0xB6, 0x4F, 0x52, 0x18, 0xC0, 0xDF, 0x41, 0xF9, 0x35, 0x60, 0x72, 0xA0, 0x44, 0x29, 0x8B, 0x6C, 0x00 });
// Dev KeyX 0x25 (> 7.x)
// Big Endian - 0x81, 0x90, 0x7A, 0x4B, 0x6F, 0x1B, 0x47, 0x32, 0x3A, 0x67, 0x79, 0x74, 0xCE, 0x4A, 0xD7, 0x1B
// Little Endian - 0x1B, 0xD7, 0x4A, 0xCE, 0x74, 0x79, 0x67, 0x3A, 0x32, 0x47, 0x1B, 0x6F, 0x4B, 0x7A, 0x90, 0x81
public static BigInteger DevKeyX0x25 = new BigInteger(new byte[] { 0x1B, 0xD7, 0x4A, 0xCE, 0x74, 0x79, 0x67, 0x3A, 0x32, 0x47, 0x1B, 0x6F, 0x4B, 0x7A, 0x90, 0x81, 0x00 });
// Dev KeyX 0x2C (< 6.x)
// Big Endian - 0x51, 0x02, 0x07, 0x51, 0x55, 0x07, 0xCB, 0xB1, 0x8E, 0x24, 0x3D, 0xCB, 0x85, 0xE2, 0x3A, 0x1D
// Little Endian - 0x1D, 0x3A, 0xE2, 0x85, 0xCB, 0x3D, 0x24, 0x8E, 0xB1, 0xCB, 0x07, 0x55, 0x51, 0x07, 0x02, 0x51
public static BigInteger DevKeyX0x2C = new BigInteger(new byte[] { 0x1D, 0x3A, 0xE2, 0x85, 0xCB, 0x3D, 0x24, 0x8E, 0xB1, 0xCB, 0x07, 0x55, 0x51, 0x07, 0x02, 0x51, 0x00 });
#endregion
public const int CXTExtendedDataHeaderLength = 0x800;
}
}

View File

@@ -1,85 +0,0 @@
using System;
namespace ThreeDS.Data
{
[Flags]
public enum BitMasks : byte
{
FixedCryptoKey = 0x01,
NoMountRomFs = 0x02,
NoCrypto = 0x04,
NewKeyYGenerator = 0x20,
}
public enum ContentPlatform : byte
{
CTR = 0x01,
Snake = 0x02, // New3DS
}
[Flags]
public enum ContentType : byte
{
Data = 0x01,
Executable = 0x02,
SystemUpdate = 0x04,
Manual = 0x08,
Child = 0x04 | 0x08,
Trial = 0x10,
}
public enum CryptoMethod : byte
{
Original = 0x00,
Seven = 0x01,
NineThree = 0x0A,
NineSix = 0x0B,
}
public enum FilesystemType : ulong
{
None = 0,
Normal = 1,
FIRM = 3,
AGB_FIRMSave = 4,
}
public enum MediaCardDeviceType : byte
{
NORFlash = 0x01,
None = 0x02,
BT = 0x03,
}
public enum MediaPlatformIndex : byte
{
CTR = 0x01,
}
public enum MediaTypeIndex : byte
{
InnerDevice = 0x00,
Card1 = 0x01,
Card2 = 0x02,
ExtendedDevice = 0x03,
}
public enum NCCHFlags
{
CryptoMethod = 0x03,
ContentPlatform = 0x04,
ContentTypeBitMask = 0x05,
ContentUnitSize = 0x06,
BitMasks = 0x07,
}
public enum NCSDFlags
{
BackupWriteWaitTime = 0x00,
MediaCardDevice3X = 0x03,
MediaPlatformIndex = 0x04,
MediaTypeIndex = 0x05,
MediaUnitSize = 0x06,
MediaCardDevice2X = 0x07,
}
}

View File

@@ -1,28 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class ARM11KernelCapabilities
{
public byte[][] Descriptors = new byte[28][];
public byte[] Reserved = new byte[0x10];
public static ARM11KernelCapabilities Read(BinaryReader reader)
{
ARM11KernelCapabilities kc = new ARM11KernelCapabilities();
try
{
for (int i = 0; i < 28; i++)
kc.Descriptors[i] = reader.ReadBytes(4);
kc.Reserved = reader.ReadBytes(0x10);
return kc;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,54 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class ARM11LocalSystemCapabilities
{
public byte[] ProgramID = new byte[8];
public uint CoreVersion;
public byte Flag1;
public byte Flag2;
public byte Flag0;
public byte Priority;
public byte[][] ResourceLimitDescriptors = new byte[16][];
public StorageInfo StorageInfo;
public byte[][] ServiceAccessControl = new byte[32][];
public byte[][] ExtendedServiceAccessControl = new byte[2][];
public byte[] Reserved = new byte[0xF];
public byte ResourceLimitCategory;
public static ARM11LocalSystemCapabilities Read(BinaryReader reader)
{
ARM11LocalSystemCapabilities lsc = new ARM11LocalSystemCapabilities();
try
{
lsc.ProgramID = reader.ReadBytes(8);
lsc.CoreVersion = reader.ReadUInt32();
lsc.Flag1 = reader.ReadByte();
lsc.Flag2 = reader.ReadByte();
lsc.Flag0 = reader.ReadByte();
lsc.Priority = reader.ReadByte();
for (int i = 0; i < 16; i++)
lsc.ResourceLimitDescriptors[i] = reader.ReadBytes(2);
lsc.StorageInfo = StorageInfo.Read(reader);
for (int i = 0; i < 32; i++)
lsc.ServiceAccessControl[i] = reader.ReadBytes(8);
for (int i = 0; i < 2; i++)
lsc.ExtendedServiceAccessControl[i] = reader.ReadBytes(8);
lsc.Reserved = reader.ReadBytes(0xF);
lsc.ResourceLimitCategory = reader.ReadByte();
return lsc;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,26 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class ARM9AccessControl
{
public byte[] Descriptors = new byte[15];
public byte DescriptorVersion;
public static ARM9AccessControl Read(BinaryReader reader)
{
ARM9AccessControl ac = new ARM9AccessControl();
try
{
ac.Descriptors = reader.ReadBytes(15);
ac.DescriptorVersion = reader.ReadByte();
return ac;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,28 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class AccessControlInfo
{
public ARM11LocalSystemCapabilities ARM11LocalSystemCapabilities;
public ARM11KernelCapabilities ARM11KernelCapabilities;
public ARM9AccessControl ARM9AccessControl;
public static AccessControlInfo Read(BinaryReader reader)
{
AccessControlInfo aci = new AccessControlInfo();
try
{
aci.ARM11LocalSystemCapabilities = ARM11LocalSystemCapabilities.Read(reader);
aci.ARM11KernelCapabilities = ARM11KernelCapabilities.Read(reader);
aci.ARM9AccessControl = ARM9AccessControl.Read(reader);
return aci;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,32 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class CXIExtendedHeader
{
public SystemControlInfo SCI;
public AccessControlInfo ACI;
public byte[] AccessDescSignature = new byte[0x100];
public byte[] NCCHHDRPublicKey = new byte[0x100];
public AccessControlInfo ACIForLimitations;
public static CXIExtendedHeader Read(BinaryReader reader)
{
CXIExtendedHeader header = new CXIExtendedHeader();
try
{
header.SCI = SystemControlInfo.Read(reader);
header.ACI = AccessControlInfo.Read(reader);
header.AccessDescSignature = reader.ReadBytes(0x100);
header.NCCHHDRPublicKey = reader.ReadBytes(0x100);
header.ACIForLimitations = AccessControlInfo.Read(reader);
return header;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,28 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class CodeSetInfo
{
public byte[] Address = new byte[0x04];
public uint PhysicalRegionSizeInPages;
public uint SizeInBytes;
public static CodeSetInfo Read(BinaryReader reader)
{
CodeSetInfo csi = new CodeSetInfo();
try
{
csi.Address = reader.ReadBytes(4);
csi.PhysicalRegionSizeInPages = reader.ReadUInt32();
csi.SizeInBytes = reader.ReadUInt32();
return csi;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,32 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class ExeFSFileHeader
{
private const string codeSegment = ".code\0\0\0";
public string FileName;
public bool IsCodeBinary { get { return FileName == codeSegment; } }
public uint FileOffset;
public uint FileSize;
public byte[] FileHash = new byte[0x20];
public static ExeFSFileHeader Read(BinaryReader reader)
{
ExeFSFileHeader header = new ExeFSFileHeader();
try
{
header.FileName = new string(reader.ReadChars(8));
header.FileOffset = reader.ReadUInt32();
header.FileSize = reader.ReadUInt32();
return header;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,35 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class ExeFSHeader
{
public ExeFSFileHeader[] FileHeaders = new ExeFSFileHeader[10];
public byte[] Reserved = new byte[0x20];
public static ExeFSHeader Read(BinaryReader reader)
{
ExeFSHeader header = new ExeFSHeader();
try
{
for (int i = 0; i < 10; i++)
header.FileHeaders[i] = ExeFSFileHeader.Read(reader);
header.Reserved = reader.ReadBytes(0x20);
for (int i = 0; i < 10; i++)
{
byte[] fileHash = reader.ReadBytes(0x20);
header.FileHeaders[9 - i].FileHash = fileHash;
}
return header;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,101 +0,0 @@
using System;
using System.IO;
using System.Linq;
using ThreeDS.Data;
namespace ThreeDS.Headers
{
public class NCCHHeader
{
private const string NCCHMagicNumber = "NCCH";
public byte[] RSA2048Signature = new byte[0x100];
public uint ContentSizeInMediaUnits;
public uint ContentSizeInBytes { get { return ContentSizeInMediaUnits * 0x200; } }
public byte[] PartitionId = new byte[8];
public byte[] MakerCode = new byte[2];
public byte[] Version = new byte[2];
public byte[] VerificationHash = new byte[4];
public byte[] ProgramId = new byte[4];
public byte[] Reserved1 = new byte[0x10];
public byte[] LogoRegionHash = new byte[0x20];
public byte[] ProductCode = new byte[0x10];
public byte[] ExtendedHeaderHash = new byte[0x20];
public uint ExtendedHeaderSizeInBytes;
public byte[] Reserved2 = new byte[4];
public NCCHHeaderFlags Flags;
public uint PlainRegionOffsetInMediaUnits;
public uint PlainRegionOffsetInBytes { get { return PlainRegionOffsetInMediaUnits * 0x200; } }
public uint PlainRegionSizeInMediaUnits;
public uint PlainRegionSizeInBytes { get { return PlainRegionSizeInMediaUnits * 0x200; } }
public uint LogoRegionOffsetInMediaUnits;
public uint LogoRegionOffsetInBytes { get { return LogoRegionOffsetInMediaUnits * 0x200; } }
public uint LogoRegionSizeInMediaUnits;
public uint LogoRegionSizeInBytes { get { return LogoRegionSizeInMediaUnits * 0x200; } }
public uint ExeFSOffsetInMediaUnits;
public uint ExeFSOffsetInBytes { get { return ExeFSOffsetInMediaUnits * 0x200; } }
public uint ExeFSSizeInMediaUnits;
public uint ExeFSSizeInBytes { get { return ExeFSSizeInMediaUnits * 0x200; } }
public uint ExeFSHashRegionOffsetInMediaUnits;
public uint ExeFSHashRegionOffsetInBytes { get { return ExeFSHashRegionOffsetInMediaUnits * 0x200; } }
public uint ExeFSHashRegionSizeInMediaUnits;
public uint ExeFSHashRegionSizeInBytes { get { return ExeFSHashRegionSizeInMediaUnits * 0x200; } }
public uint RomFSOffsetInMediaUnits;
public uint RomFSOffsetInBytes { get { return RomFSOffsetInMediaUnits * 0x200; } }
public uint RomFSSizeInMediaUnits;
public uint RomFSSizeInBytes { get { return RomFSSizeInMediaUnits * 0x200; } }
public uint RomFSHashRegionOffsetInMediaUnits;
public uint RomFSHashRegionOffsetInBytes { get { return RomFSHashRegionOffsetInMediaUnits * 0x200; } }
public uint RomFSHashRegionSizeInMediaUnits;
public uint RomFSHashRegionSizeInBytes { get { return RomFSHashRegionSizeInMediaUnits * 0x200; } }
public byte[] ExeFSSuperblockHash = new byte[0x20];
public byte[] RomFSSuperblockHash = new byte[0x20];
public static NCCHHeader Read(BinaryReader reader)
{
NCCHHeader header = new NCCHHeader();
try
{
header.RSA2048Signature = reader.ReadBytes(0x100);
if (new string(reader.ReadChars(4)) != NCCHMagicNumber)
return null;
header.ContentSizeInMediaUnits = reader.ReadUInt32();
header.PartitionId = reader.ReadBytes(8).Reverse().ToArray();
header.MakerCode = reader.ReadBytes(2);
header.Version = reader.ReadBytes(2);
header.VerificationHash = reader.ReadBytes(4);
header.ProgramId = reader.ReadBytes(8);
header.Reserved1 = reader.ReadBytes(0x10);
header.LogoRegionHash = reader.ReadBytes(0x20);
header.ProductCode = reader.ReadBytes(0x10);
header.ExtendedHeaderHash = reader.ReadBytes(0x20);
header.ExtendedHeaderSizeInBytes = reader.ReadUInt32();
header.Reserved2 = reader.ReadBytes(4);
header.Flags = NCCHHeaderFlags.Read(reader);
header.PlainRegionOffsetInMediaUnits = reader.ReadUInt32();
header.PlainRegionSizeInMediaUnits = reader.ReadUInt32();
header.LogoRegionOffsetInMediaUnits = reader.ReadUInt32();
header.LogoRegionSizeInMediaUnits = reader.ReadUInt32();
header.ExeFSOffsetInMediaUnits = reader.ReadUInt32();
header.ExeFSSizeInMediaUnits = reader.ReadUInt32();
header.ExeFSHashRegionOffsetInMediaUnits = reader.ReadUInt32();
header.ExeFSHashRegionSizeInMediaUnits = reader.ReadUInt32();
header.RomFSOffsetInMediaUnits = reader.ReadUInt32();
header.RomFSSizeInMediaUnits = reader.ReadUInt32();
header.RomFSHashRegionOffsetInMediaUnits = reader.ReadUInt32();
header.RomFSHashRegionSizeInMediaUnits = reader.ReadUInt32();
header.ExeFSSuperblockHash = reader.ReadBytes(0x20);
header.RomFSSuperblockHash = reader.ReadBytes(0x20);
return header;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,58 +0,0 @@
using System;
using System.IO;
using ThreeDS.Data;
namespace ThreeDS.Headers
{
public class NCCHHeaderFlags
{
public byte Reserved0;
public byte Reserved1;
public byte Reserved2;
public CryptoMethod CryptoMethod;
public ContentPlatform ContentPlatform;
public ContentType MediaPlatformIndex;
public byte ContentUnitSize;
public uint ContentUnitSizeInBytes { get { return (uint)(0x200 * Math.Pow(2, this.ContentUnitSize)); } }
public BitMasks BitMasks;
public static NCCHHeaderFlags Read(BinaryReader reader)
{
NCCHHeaderFlags flags = new NCCHHeaderFlags();
try
{
flags.Reserved0 = reader.ReadByte();
flags.Reserved1 = reader.ReadByte();
flags.Reserved2 = reader.ReadByte();
flags.CryptoMethod = (CryptoMethod)reader.ReadByte();
flags.ContentPlatform = (ContentPlatform)reader.ReadByte();
flags.MediaPlatformIndex = (ContentType)reader.ReadByte();
flags.ContentUnitSize = reader.ReadByte();
flags.BitMasks = (BitMasks)reader.ReadByte();
return flags;
}
catch
{
return null;
}
}
public void Write(BinaryWriter writer)
{
try
{
writer.Write(this.Reserved0);
writer.Write(this.Reserved1);
writer.Write(this.Reserved2);
writer.Write((byte)this.CryptoMethod);
writer.Write((byte)this.ContentPlatform);
writer.Write((byte)this.MediaPlatformIndex);
writer.Write((byte)this.ContentUnitSize);
writer.Write((byte)this.BitMasks);
}
catch { }
}
}
}

View File

@@ -1,90 +0,0 @@
using System;
using System.IO;
using ThreeDS.Data;
namespace ThreeDS.Headers
{
public class NCSDHeader
{
private const string NCSDMagicNumber = "NCSD";
// Common to all NCSD files
public byte[] RSA2048Signature = new byte[0x100];
public uint ImageSizeInMediaUnits;
public uint ImageSizeInBytes { get { return ImageSizeInMediaUnits * 0x200; } }
public byte[] MediaId = new byte[8];
public FilesystemType PartitionsFSType;
public byte[] PartitionsCryptType = new byte[8];
public PartitionTableEntry[] PartitionsTable = new PartitionTableEntry[8];
// For carts
public byte[] ExheaderHash = new byte[0x20];
public uint AdditionalHeaderSize;
public uint SectorZeroOffset;
private byte[] partitionFlags = new byte[8];
public byte BackupWriteWaitTime { get { return partitionFlags[(int)NCSDFlags.BackupWriteWaitTime]; } }
public MediaCardDeviceType MediaCardDevice3X { get { return (MediaCardDeviceType)partitionFlags[(int)NCSDFlags.MediaCardDevice3X]; } }
public MediaPlatformIndex MediaPlatformIndex { get { return (MediaPlatformIndex)partitionFlags[(int)NCSDFlags.MediaPlatformIndex]; } }
public MediaTypeIndex MediaTypeIndex { get { return (MediaTypeIndex)partitionFlags[(int)NCSDFlags.MediaTypeIndex]; } }
public uint SectorSize { get { return (uint)(0x200 * Math.Pow(2, partitionFlags[(int)NCSDFlags.MediaUnitSize])); } }
public MediaCardDeviceType MediaCardDevice2X { get { return (MediaCardDeviceType)partitionFlags[(int)NCSDFlags.MediaCardDevice2X]; } }
public byte[][] PartitionIdTable = new byte[8][];
public byte[] ReservedBlock1 = new byte[0x20];
public byte[] ReservedBlock2 = new byte[0xE];
public byte FirmUpdateByte1;
public byte FIrmUpdateByte2;
// For NAND
public byte[] Unknown = new byte[0x5E];
public byte[] EncryptedMBR = new byte[0x42];
public static NCSDHeader Read(BinaryReader reader)
{
NCSDHeader header = new NCSDHeader();
try
{
header.RSA2048Signature = reader.ReadBytes(0x100);
if (new string(reader.ReadChars(4)) != NCSDMagicNumber)
return null;
header.ImageSizeInMediaUnits = reader.ReadUInt32();
header.MediaId = reader.ReadBytes(8);
header.PartitionsFSType = (FilesystemType)reader.ReadUInt64();
header.PartitionsCryptType = reader.ReadBytes(8);
for (int i = 0; i < 8; i++)
header.PartitionsTable[i] = PartitionTableEntry.Read(reader);
if (header.PartitionsFSType == FilesystemType.Normal
|| header.PartitionsFSType == FilesystemType.None)
{
header.ExheaderHash = reader.ReadBytes(0x20);
header.AdditionalHeaderSize = reader.ReadUInt32();
header.SectorZeroOffset = reader.ReadUInt32();
header.partitionFlags = reader.ReadBytes(8);
for (int i = 0; i < 8; i++)
header.PartitionIdTable[i] = reader.ReadBytes(8);
header.ReservedBlock1 = reader.ReadBytes(0x20);
header.ReservedBlock2 = reader.ReadBytes(0xE);
header.FirmUpdateByte1 = reader.ReadByte();
header.FIrmUpdateByte2 = reader.ReadByte();
}
else if (header.PartitionsFSType == FilesystemType.FIRM)
{
header.Unknown = reader.ReadBytes(0x5E);
header.EncryptedMBR = reader.ReadBytes(0x42);
}
return header;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,31 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class PartitionTableEntry
{
public uint Offset { get; set; }
public uint Length { get; set; }
public static PartitionTableEntry Read(BinaryReader reader)
{
PartitionTableEntry entry = new PartitionTableEntry();
try
{
entry.Offset = reader.ReadUInt32();
entry.Length = reader.ReadUInt32();
return entry;
}
catch
{
return null;
}
}
public bool IsValid()
{
return Offset != 0 && Length != 0;
}
}
}

View File

@@ -1,32 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class StorageInfo
{
public byte[] ExtdataID = new byte[8];
public byte[] SystemSavedataIDs = new byte[8];
public byte[] StorageAccessibleUniqueIDs = new byte[8];
public byte[] FilesystemAccessInfo = new byte[7];
public byte OtherAttributes;
public static StorageInfo Read(BinaryReader reader)
{
StorageInfo si = new StorageInfo();
try
{
si.ExtdataID = reader.ReadBytes(8);
si.SystemSavedataIDs = reader.ReadBytes(8);
si.StorageAccessibleUniqueIDs = reader.ReadBytes(8);
si.FilesystemAccessInfo = reader.ReadBytes(7);
si.OtherAttributes = reader.ReadByte();
return si;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,49 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class SystemControlInfo
{
public char[] ApplicationTitle = new char[8];
public byte[] Reserved1 = new byte[5];
public byte Flag;
public byte[] RemasterVersion = new byte[2];
public CodeSetInfo TextCodesetInfo;
public uint StackSize;
public CodeSetInfo ReadOnlyCodeSetInfo;
public byte[] Reserved2 = new byte[4];
public CodeSetInfo DataCodeSetInfo;
public uint BSSSize;
public byte[][] DependencyModuleList = new byte[48][];
public SystemInfo SystemInfo;
public static SystemControlInfo Read(BinaryReader reader)
{
SystemControlInfo sci = new SystemControlInfo();
try
{
sci.ApplicationTitle = reader.ReadChars(8);
sci.Reserved1 = reader.ReadBytes(5);
sci.Flag = reader.ReadByte();
sci.RemasterVersion = reader.ReadBytes(2);
sci.TextCodesetInfo = CodeSetInfo.Read(reader);
sci.StackSize = reader.ReadUInt32();
sci.ReadOnlyCodeSetInfo = CodeSetInfo.Read(reader);
sci.Reserved2 = reader.ReadBytes(4);
sci.DataCodeSetInfo = CodeSetInfo.Read(reader);
sci.BSSSize = reader.ReadUInt32();
for (int i = 0; i < 48; i++)
sci.DependencyModuleList[i] = reader.ReadBytes(8);
sci.SystemInfo = SystemInfo.Read(reader);
return sci;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,28 +0,0 @@
using System.IO;
namespace ThreeDS.Headers
{
public class SystemInfo
{
public ulong SaveDataSize;
public byte[] JumpID = new byte[8];
public byte[] Reserved = new byte[0x30];
public static SystemInfo Read(BinaryReader reader)
{
SystemInfo si = new SystemInfo();
try
{
si.SaveDataSize = reader.ReadUInt64();
si.JumpID = reader.ReadBytes(8);
si.Reserved = reader.ReadBytes(0x30);
return si;
}
catch
{
return null;
}
}
}
}

View File

@@ -1,49 +0,0 @@
using System;
using System.IO;
namespace ThreeDS
{
class Program
{
public static void Main(string[] args)
{
if (args.Length < 2 || (args[0] != "encrypt" && args[0] != "decrypt"))
{
Console.WriteLine("Usage: 3dsdecrypt.exe (decrypt|encrypt) [-dev] <file|dir> ...");
return;
}
bool development = false;
int start = 1;
if (args[1] == "-dev")
{
development = true;
start = 2;
}
for (int i = start; i < args.Length; i++)
{
if (File.Exists(args[i]))
{
ThreeDSTool tool = new ThreeDSTool(args[i], development);
if (args[0] == "decrypt")
tool.Decrypt();
else if (args[0] == "encrypt")
tool.Encrypt();
}
else if (Directory.Exists(args[i]))
{
foreach (string file in Directory.EnumerateFiles(args[i], "*", SearchOption.AllDirectories))
{
ThreeDSTool tool = new ThreeDSTool(file, development);
if (args[0] == "decrypt")
tool.Decrypt();
else if (args[0] == "encrypt")
tool.Encrypt();
}
}
}
}
}
}

View File

@@ -1,36 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("3DSDecrypt")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("3DSDecrypt")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("2e30006a-3c60-4576-a262-937b21c83c06")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -1,623 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Numerics;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using ThreeDS.Data;
using ThreeDS.Headers;
namespace ThreeDS
{
public class ThreeDSTool
{
private readonly string filename;
private readonly bool development;
public ThreeDSTool(string filename, bool development)
{
this.filename = filename;
this.development = development;
}
public void Decrypt()
{
if (!File.Exists(filename))
return;
Console.WriteLine(filename);
using (BinaryReader f = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter g = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
{
NCSDHeader header = NCSDHeader.Read(f);
if (header == null)
{
Console.WriteLine("Error: Not a 3DS Rom!");
return;
}
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
if (!header.PartitionsTable[p].IsValid())
{
Console.WriteLine("Partition {0} Not found... Skipping...", p);
continue;
}
// Seek to the beginning of the NCCH partition
f.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize), SeekOrigin.Begin);
NCCHHeader partitionHeader = NCCHHeader.Read(f);
if (partitionHeader == null)
{
Console.WriteLine("Partition {0} Unable to read NCCH header", p);
continue;
}
// Check if the 'NoCrypto' bit is set
if ((partitionHeader.Flags.BitMasks & BitMasks.NoCrypto) != 0)
{
Console.WriteLine("Partition {0:d}: Already Decrypted?...", p);
continue;
}
// PartitionID is used as IV joined with the content type.
byte[] plainIV = partitionHeader.PartitionId.Concat(Constants.PlainCounter).ToArray(); // Get the IV for plain sector (TitleID + Plain Counter)
byte[] exefsIV = partitionHeader.PartitionId.Concat(Constants.ExefsCounter).ToArray(); // Get the IV for ExeFS (TitleID + ExeFS Counter)
byte[] romfsIV = partitionHeader.PartitionId.Concat(Constants.RomfsCounter).ToArray(); // Get the IV for RomFS (TitleID + RomFS Counter)
BigInteger KeyX = 0;
BigInteger KeyX2C = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
BigInteger KeyY = new BigInteger(partitionHeader.RSA2048Signature.Take(16).Reverse().ToArray()); // KeyY is the first 16 bytes of the partition RSA-2048 SHA-256 signature
BigInteger NormalKey = 0;
BigInteger NormalKey2C = RotateLeft((RotateLeft(KeyX2C, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
// Determine the Keys to be used
if ((partitionHeader.Flags.BitMasks & BitMasks.FixedCryptoKey) != 0)
{
NormalKey = 0x00;
NormalKey2C = 0x00;
if (p == 0)
Console.WriteLine("Encryption Method: Zero Key");
}
else
{
if (partitionHeader.Flags.CryptoMethod == CryptoMethod.Original)
{
KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
if (p == 0)
Console.WriteLine("Encryption Method: Key 0x2C");
}
else if (partitionHeader.Flags.CryptoMethod == CryptoMethod.Seven)
{
KeyX = (development ? Constants.KeyX0x25 : Constants.KeyX0x25);
if (p == 0)
Console.WriteLine("Encryption Method: Key 0x25");
}
else if (partitionHeader.Flags.CryptoMethod == CryptoMethod.NineThree)
{
KeyX = (development ? Constants.DevKeyX0x18 : Constants.KeyX0x18);
if (p == 0)
Console.WriteLine("Encryption Method: Key 0x18");
}
else if (partitionHeader.Flags.CryptoMethod == CryptoMethod.NineSix)
{
KeyX = (development ? Constants.DevKeyX0x1B : Constants.KeyX0x1B);
if (p == 0)
Console.WriteLine("Encryption Method: Key 0x1B");
}
NormalKey = RotateLeft((RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
// Decrypted extended header, if it exists
if (partitionHeader.ExtendedHeaderSizeInBytes > 0)
{
// Seek to the partition start and skip first part of the header
f.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x200, SeekOrigin.Begin);
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x200, SeekOrigin.Begin);
var str = BitConverter.ToString(plainIV).Replace("-", "");
var exefsctrmode2C = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode2C.Init(false, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey2C)), plainIV));
Console.WriteLine("Partition {0} ExeFS: Decrypting: ExHeader", p);
g.Write(exefsctrmode2C.ProcessBytes(f.ReadBytes(Constants.CXTExtendedDataHeaderLength)));
g.Flush();
}
// Decrypt the ExeFS, if it exists
if (partitionHeader.ExeFSSizeInBytes > 0)
{
f.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
g.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
var exefsctrmode2C = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode2C.Init(false, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey2C)), exefsIV));
g.Write(exefsctrmode2C.ProcessBytes(f.ReadBytes((int)header.SectorSize)));
g.Flush();
Console.WriteLine("Partition {0} ExeFS: Decrypting: ExeFS Filename Table", p);
if (partitionHeader.Flags.CryptoMethod != CryptoMethod.Original)
{
f.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
ExeFSHeader exefsHeader = ExeFSHeader.Read(f);
if (exefsHeader != null)
{
foreach (ExeFSFileHeader fileHeader in exefsHeader.FileHeaders)
{
if (!fileHeader.IsCodeBinary)
continue;
uint datalenM = ((fileHeader.FileSize) / (1024 * 1024));
uint datalenB = ((fileHeader.FileSize) % (1024 * 1024));
uint ctroffset = ((fileHeader.FileOffset + header.SectorSize) / 0x10);
byte[] exefsIVWithOffsetForHeader = AddToByteArray(exefsIV, (int)ctroffset);
var exefsctrmode = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode.Init(false, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey)), exefsIVWithOffsetForHeader));
exefsctrmode2C = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode2C.Init(true, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey2C)), exefsIVWithOffsetForHeader));
f.BaseStream.Seek((((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) + 1) * header.SectorSize) + fileHeader.FileOffset, SeekOrigin.Begin);
g.BaseStream.Seek((((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) + 1) * header.SectorSize) + fileHeader.FileOffset, SeekOrigin.Begin);
if (datalenM > 0)
{
for (int i = 0; i < datalenM; i++)
{
g.Write(exefsctrmode2C.ProcessBytes(exefsctrmode.ProcessBytes(f.ReadBytes(1024 * 1024))));
g.Flush();
Console.Write("\rPartition {0} ExeFS: Decrypting: {1}... {2} / {3} mb...", p, fileHeader.FileName, i, datalenM + 1);
}
}
if (datalenB > 0)
{
g.Write(exefsctrmode2C.ProcessBytes(exefsctrmode.ProcessBytes(f.ReadBytes((int)datalenB))));
g.Flush();
}
Console.Write("\rPartition {0} ExeFS: Decrypting: {1}... {2} / {3} mb... Done!\r\n", p, fileHeader.FileName, datalenM + 1, datalenM + 1);
}
}
}
// decrypt exefs
int exefsSizeM = (int)((partitionHeader.ExeFSSizeInMediaUnits - 1) * header.SectorSize) / (1024 * 1024);
int exefsSizeB = (int)((partitionHeader.ExeFSSizeInMediaUnits - 1) * header.SectorSize) % (1024 * 1024);
int ctroffsetE = (int)(header.SectorSize / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(exefsIV, ctroffsetE);
exefsctrmode2C = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode2C.Init(false, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey2C)), exefsIVWithOffset));
f.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits + 1) * header.SectorSize, SeekOrigin.Begin);
g.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits + 1) * header.SectorSize, SeekOrigin.Begin);
if (exefsSizeM > 0)
{
for (int i = 0; i < exefsSizeM; i++)
{
g.Write(exefsctrmode2C.ProcessBytes(f.ReadBytes(1024 * 1024)));
g.Flush();
Console.Write("\rPartition {0} ExeFS: Decrypting: {1} / {2} mb", p, i, exefsSizeM + 1);
}
}
if (exefsSizeB > 0)
{
g.Write(exefsctrmode2C.ProcessBytes(f.ReadBytes(exefsSizeB)));
g.Flush();
}
Console.Write("\rPartition {0} ExeFS: Decrypting: {1} / {2} mb... Done!\r\n", p, exefsSizeM + 1, exefsSizeM + 1);
}
else
{
Console.WriteLine("Partition {0} ExeFS: No Data... Skipping...", p);
}
if (partitionHeader.RomFSOffsetInMediaUnits != 0)
{
int romfsSizeM = (int)(partitionHeader.RomFSSizeInMediaUnits * header.SectorSize) / (1024 * 1024);
int romfsSizeB = (int)(partitionHeader.RomFSSizeInMediaUnits * header.SectorSize) % (1024 * 1024);
var romfsctrmode = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
romfsctrmode.Init(false, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey)), romfsIV));
f.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.RomFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
g.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.RomFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
if (romfsSizeM > 0)
{
for (int i = 0; i < romfsSizeM; i++)
{
g.Write(romfsctrmode.ProcessBytes(f.ReadBytes(1024 * 1024)));
g.Flush();
Console.Write("\rPartition {0} RomFS: Decrypting: {1} / {2} mb", p, i, romfsSizeM + 1);
}
}
if (romfsSizeB > 0)
{
g.Write(romfsctrmode.ProcessBytes(f.ReadBytes(romfsSizeB)));
g.Flush();
}
Console.Write("\rPartition {0} RomFS: Decrypting: {1} / {2} mb... Done!\r\n", p, romfsSizeM + 1, romfsSizeM + 1);
}
else
{
Console.WriteLine("Partition {0} RomFS: No Data... Skipping...", p);
}
// Write the new CryptoMethod
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x18B, SeekOrigin.Begin);
g.Write((byte)CryptoMethod.Original);
g.Flush();
// Write the new BitMasks flag
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = partitionHeader.Flags.BitMasks;
flag = flag & (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag = (flag | BitMasks.NoCrypto);
g.Write((byte)flag);
g.Flush();
}
Console.WriteLine("Press Enter to Exit...");
Console.Read();
}
}
/// <summary>
/// Writes wrong header values
/// </summary>
public void Encrypt()
{
if (!File.Exists(filename))
return;
Console.WriteLine(filename);
using (BinaryReader f = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter g = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
{
NCSDHeader header = NCSDHeader.Read(f);
if (header == null)
{
Console.WriteLine("Error: Not a 3DS Rom!");
return;
}
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
if (!header.PartitionsTable[p].IsValid())
{
Console.WriteLine("Partition {0} Not found... Skipping...", p);
continue;
}
// Seek to the beginning of the NCCH partition
f.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize), SeekOrigin.Begin);
NCCHHeader partitionHeader = NCCHHeader.Read(f);
if (partitionHeader == null)
{
Console.WriteLine("Partition {0} Unable to read NCCH header", p);
continue;
}
// Get the backup flags
f.BaseStream.Seek(0x1188, SeekOrigin.Begin);
NCCHHeaderFlags backupFlags = NCCHHeaderFlags.Read(f);
// Check if the 'NoCrypto' bit is not set
if ((partitionHeader.Flags.BitMasks & BitMasks.NoCrypto) == 0)
{
Console.WriteLine("Partition {0:d}: Already Encrypted?...", p);
continue;
}
// PartitionID is used as IV joined with the content type.
byte[] plainIV = partitionHeader.PartitionId.Concat(Constants.PlainCounter).ToArray(); // Get the IV for plain sector (TitleID + Plain Counter)
byte[] exefsIV = partitionHeader.PartitionId.Concat(Constants.ExefsCounter).ToArray(); // Get the IV for ExeFS (TitleID + ExeFS Counter)
byte[] romfsIV = partitionHeader.PartitionId.Concat(Constants.RomfsCounter).ToArray(); // Get the IV for RomFS (TitleID + RomFS Counter)
BigInteger KeyX = 0;
BigInteger KeyX2C = Constants.KeyX0x2C;
BigInteger KeyY = new BigInteger(partitionHeader.RSA2048Signature.Take(16).Reverse().ToArray()); // KeyY is the first 16 bytes of the partition RSA-2048 SHA-256 signature
BigInteger NormalKey = 0;
BigInteger NormalKey2C = RotateLeft((RotateLeft(KeyX2C, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
// Determine the Keys to be used
if ((backupFlags.BitMasks & BitMasks.FixedCryptoKey) != 0)
{
NormalKey = 0x00;
NormalKey2C = 0x00;
if (p == 0)
Console.WriteLine("Encryption Method: Zero Key");
}
else
{
if (backupFlags.CryptoMethod == CryptoMethod.Original)
{
KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
if (p == 0)
Console.WriteLine("Encryption Method: Key 0x2C");
}
else if (backupFlags.CryptoMethod == CryptoMethod.Seven)
{
KeyX = (development ? Constants.KeyX0x25 : Constants.KeyX0x25);
if (p == 0)
Console.WriteLine("Encryption Method: Key 0x25");
}
else if (backupFlags.CryptoMethod == CryptoMethod.NineThree)
{
KeyX = (development ? Constants.DevKeyX0x18 : Constants.KeyX0x18);
if (p == 0)
Console.WriteLine("Encryption Method: Key 0x18");
}
else if (backupFlags.CryptoMethod == CryptoMethod.NineSix)
{
KeyX = (development ? Constants.DevKeyX0x1B : Constants.KeyX0x1B);
if (p == 0)
Console.WriteLine("Encryption Method: Key 0x1B");
}
NormalKey = RotateLeft((RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
// Encrypt extended header, if it exists
if (partitionHeader.ExtendedHeaderSizeInBytes > 0)
{
// Seek to the partition start and skip first part of the header
f.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x200, SeekOrigin.Begin);
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x200, SeekOrigin.Begin);
var str = BitConverter.ToString(plainIV).Replace("-", "");
var exefsctrmode2C = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode2C.Init(true, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey2C)), plainIV));
Console.WriteLine("Partition {0} ExeFS: Encrypting: ExHeader", p);
g.Write(exefsctrmode2C.ProcessBytes(f.ReadBytes(Constants.CXTExtendedDataHeaderLength)));
g.Flush();
}
// Encrypt the ExeFS, if it exists
if (partitionHeader.ExeFSSizeInBytes > 0)
{
f.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
g.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
var exefsctrmode2C = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode2C.Init(true, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey2C)), exefsIV));
g.Write(exefsctrmode2C.ProcessBytes(f.ReadBytes((int)header.SectorSize)));
g.Flush();
Console.WriteLine("Partition {0} ExeFS: Encrypting: ExeFS Filename Table", p);
if (backupFlags.CryptoMethod != CryptoMethod.Original)
{
f.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
ExeFSHeader exefsHeader = ExeFSHeader.Read(f);
if (exefsHeader != null)
{
foreach (ExeFSFileHeader fileHeader in exefsHeader.FileHeaders)
{
if (!fileHeader.IsCodeBinary)
continue;
uint datalenM = ((fileHeader.FileSize) / (1024 * 1024));
uint datalenB = ((fileHeader.FileSize) % (1024 * 1024));
uint ctroffset = ((fileHeader.FileOffset + header.SectorSize) / 0x10);
byte[] exefsIVWithOffsetForHeader = AddToByteArray(exefsIV, (int)ctroffset);
var exefsctrmode = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode.Init(true, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey)), exefsIVWithOffsetForHeader));
exefsctrmode2C = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode2C.Init(false, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey2C)), exefsIVWithOffsetForHeader));
f.BaseStream.Seek((((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) + 1) * header.SectorSize) + fileHeader.FileOffset, SeekOrigin.Begin);
g.BaseStream.Seek((((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits) + 1) * header.SectorSize) + fileHeader.FileOffset, SeekOrigin.Begin);
if (datalenM > 0)
{
for (int i = 0; i < datalenM; i++)
{
g.Write(exefsctrmode2C.ProcessBytes(exefsctrmode.ProcessBytes(f.ReadBytes(1024 * 1024))));
g.Flush();
Console.Write("\rPartition {0} ExeFS: Encrypting: {1}... {2} / {3} mb...", p, fileHeader.FileName, i, datalenM + 1);
}
}
if (datalenB > 0)
{
g.Write(exefsctrmode2C.ProcessBytes(exefsctrmode.ProcessBytes(f.ReadBytes((int)datalenB))));
g.Flush();
}
Console.Write("\rPartition {0} ExeFS: Encrypting: {1}... {2} / {3} mb... Done!\r\n", p, fileHeader.FileName, datalenM + 1, datalenM + 1);
}
}
}
// decrypt exefs
int exefsSizeM = (int)((partitionHeader.ExeFSSizeInMediaUnits - 1) * header.SectorSize) / (1024 * 1024);
int exefsSizeB = (int)((partitionHeader.ExeFSSizeInMediaUnits - 1) * header.SectorSize) % (1024 * 1024);
int ctroffsetE = (int)(header.SectorSize / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(exefsIV, ctroffsetE);
exefsctrmode2C = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
exefsctrmode2C.Init(true, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey2C)), exefsIVWithOffset));
f.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits + 1) * header.SectorSize, SeekOrigin.Begin);
g.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.ExeFSOffsetInMediaUnits + 1) * header.SectorSize, SeekOrigin.Begin);
if (exefsSizeM > 0)
{
for (int i = 0; i < exefsSizeM; i++)
{
g.Write(exefsctrmode2C.ProcessBytes(f.ReadBytes(1024 * 1024)));
g.Flush();
Console.Write("\rPartition {0} ExeFS: Encrypting: {1} / {2} mb", p, i, exefsSizeM + 1);
}
}
if (exefsSizeB > 0)
{
g.Write(exefsctrmode2C.ProcessBytes(f.ReadBytes(exefsSizeB)));
g.Flush();
}
Console.Write("\rPartition {0} ExeFS: Encrypting: {1} / {2} mb... Done!\r\n", p, exefsSizeM + 1, exefsSizeM + 1);
}
else
{
Console.WriteLine("Partition {0} ExeFS: No Data... Skipping...", p);
}
if (partitionHeader.RomFSOffsetInMediaUnits != 0)
{
int romfsBlockSize = 16; // block size in mb
int romfsSizeM = (int)(partitionHeader.RomFSSizeInMediaUnits * header.SectorSize) / (romfsBlockSize * (1024 * 1024));
int romfsSizeB = (int)(partitionHeader.RomFSSizeInMediaUnits * header.SectorSize) % (romfsBlockSize * (1024 * 1024));
int romfsSizeTotalMb = (int)((partitionHeader.RomFSSizeInMediaUnits * header.SectorSize) / (1024 * 1024) + 1);
if (p > 0) // RomFS for partitions 1 and up always use Key0x2C
{
if ((backupFlags.BitMasks & BitMasks.FixedCryptoKey) != 0) // except if using zero-key
{
NormalKey = 0x00;
}
else
{
KeyX = KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
NormalKey = RotateLeft((RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
}
var romfsctrmode = CipherUtilities.GetCipher("AES/CTR"); // ("AES/CTR/NoPadding")
romfsctrmode.Init(true, new ParametersWithIV(new KeyParameter(TakeSixteen(NormalKey)), romfsIV));
f.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.RomFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
g.BaseStream.Seek((header.PartitionsTable[p].Offset + partitionHeader.RomFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
if (romfsSizeM > 0)
{
for (int i = 0; i < romfsSizeM; i++)
{
g.Write(romfsctrmode.ProcessBytes(f.ReadBytes(romfsBlockSize * 1024 * 1024)));
g.Flush();
Console.Write("\rPartition {0} RomFS: Encrypting: {1} / {2} mb", p, i * romfsBlockSize, romfsSizeTotalMb);
}
}
if (romfsSizeB > 0)
{
g.Write(romfsctrmode.ProcessBytes(f.ReadBytes(romfsSizeB)));
g.Flush();
}
Console.Write("\rPartition {0} RomFS: Encrypting: {1} / {2} mb... Done!\r\n", p, romfsSizeTotalMb, romfsSizeTotalMb);
}
else
{
Console.WriteLine("Partition {0} RomFS: No Data... Skipping...", p);
}
// Write the new CryptoMethod
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x18B, SeekOrigin.Begin);
if (p > 0)
{
g.Write((byte)CryptoMethod.Original); // For partitions 1 and up, set crypto-method to 0x00
g.Flush();
}
else
{
g.Write((byte)backupFlags.CryptoMethod); // If partition 0, restore crypto-method from backup flags
g.Flush();
}
// Write the new BitMasks flag
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = partitionHeader.Flags.BitMasks;
flag = (flag & ((BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF));
flag = (flag | (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupFlags.BitMasks);
g.Write((byte)flag);
g.Flush();
}
Console.WriteLine("Press Enter to Exit...");
Console.Read();
}
}
private static BigInteger RotateLeft(BigInteger val, int r_bits, int max_bits)
{
return (val << r_bits % max_bits) & (BigInteger.Pow(2, max_bits) - 1) | ((val & (BigInteger.Pow(2, max_bits) - 1)) >> (max_bits - (r_bits % max_bits)));
}
private static string ToBytes(int num)
{
string numstr = "";
int tmp = num;
while (numstr.Length < 16)
{
numstr += (char)(tmp & 0xFF);
tmp >>= 8;
}
return numstr;
}
private static byte[] AddToByteArray(byte[] input, int add)
{
int len = input.Length;
var bigint = new BigInteger(input.Reverse().ToArray());
bigint += add;
var arr = bigint.ToByteArray().Reverse().ToArray();
if (arr.Length < len)
{
byte[] temp = new byte[len];
for (int i = 0; i < (len - arr.Length); i++)
temp[i] = 0x00;
Array.Copy(arr, 0, temp, len - arr.Length, arr.Length);
arr = temp;
}
return arr;
}
private static byte[] TakeSixteen(BigInteger input)
{
var arr = input.ToByteArray().Take(16).Reverse().ToArray();
if (arr.Length < 16)
{
byte[] temp = new byte[16];
for (int i = 0; i < (16 - arr.Length); i++)
temp[i] = 0x00;
Array.Copy(arr, 0, temp, 16 - arr.Length, arr.Length);
arr = temp;
}
return arr;
}
}
}

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="BouncyCastle" version="1.8.4" targetFramework="net461" />
</packages>

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2018-2025 Matt Nadareski
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,253 @@
using System;
using System.IO;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using SabreTools.IO.Extensions;
namespace NDecrypt.Core
{
public static class CommonOperations
{
#region AES
/// <summary>
/// Create AES decryption cipher and intialize
/// </summary>
/// <param name="key">Byte array representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <returns>Initialized AES cipher</returns>
public static IBufferedCipher CreateAESDecryptionCipher(byte[] key, byte[] iv)
{
if (key.Length != 16)
throw new ArgumentOutOfRangeException(nameof(key));
var keyParam = new KeyParameter(key);
var cipher = CipherUtilities.GetCipher("AES/CTR");
cipher.Init(forEncryption: false, new ParametersWithIV(keyParam, iv));
return cipher;
}
/// <summary>
/// Create AES encryption cipher and intialize
/// </summary>
/// <param name="key">Byte array representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <returns>Initialized AES cipher</returns>
public static IBufferedCipher CreateAESEncryptionCipher(byte[] key, byte[] iv)
{
if (key.Length != 16)
throw new ArgumentOutOfRangeException(nameof(key));
var keyParam = new KeyParameter(key);
var cipher = CipherUtilities.GetCipher("AES/CTR");
cipher.Init(forEncryption: true, new ParametersWithIV(keyParam, iv));
return cipher;
}
/// <summary>
/// Perform an AES operation using an existing cipher
/// </summary>
public static void PerformAESOperation(uint size,
IBufferedCipher cipher,
Stream input,
Stream output,
Action<string>? progress)
{
// Get MiB-aligned block count and extra byte count
int blockCount = (int)((long)size / (1024 * 1024));
int extraBytes = (int)((long)size % (1024 * 1024));
// Process MiB-aligned data
if (blockCount > 0)
{
for (int i = 0; i < blockCount; i++)
{
byte[] readBytes = input.ReadBytes(1024 * 1024);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.Write(processedBytes);
output.Flush();
progress?.Invoke($"{i} / {blockCount + 1} MB");
}
}
// Process additional data
if (extraBytes > 0)
{
byte[] readBytes = input.ReadBytes(extraBytes);
byte[] finalBytes = cipher.DoFinal(readBytes);
output.Write(finalBytes);
output.Flush();
}
progress?.Invoke($"{blockCount + 1} / {blockCount + 1} MB... Done!\r\n");
}
/// <summary>
/// Perform an AES operation using two existing ciphers
/// </summary>
public static void PerformAESOperation(uint size,
IBufferedCipher firstCipher,
IBufferedCipher secondCipher,
Stream input,
Stream output,
Action<string> progress)
{
// Get MiB-aligned block count and extra byte count
int blockCount = (int)((long)size / (1024 * 1024));
int extraBytes = (int)((long)size % (1024 * 1024));
// Process MiB-aligned data
if (blockCount > 0)
{
for (int i = 0; i < blockCount; i++)
{
byte[] readBytes = input.ReadBytes(1024 * 1024);
byte[] firstProcessedBytes = firstCipher.ProcessBytes(readBytes);
byte[] secondProcessedBytes = secondCipher.ProcessBytes(firstProcessedBytes);
output.Write(secondProcessedBytes);
output.Flush();
progress($"{i} / {blockCount + 1} MB");
}
}
// Process additional data
if (extraBytes > 0)
{
byte[] readBytes = input.ReadBytes(extraBytes);
byte[] firstFinalBytes = firstCipher.DoFinal(readBytes);
byte[] secondFinalBytes = secondCipher.DoFinal(firstFinalBytes);
output.Write(secondFinalBytes);
output.Flush();
}
progress($"{blockCount + 1} / {blockCount + 1} MB... Done!\r\n");
}
#endregion
#region Byte Arrays
/// <summary>
/// Add an integer value to a number represented by a byte array
/// </summary>
/// <param name="input">Byte array to add to</param>
/// <param name="add">Amount to add</param>
/// <returns>Byte array representing the new value</returns>
public static byte[] Add(byte[] input, uint add)
{
byte[] addBytes = BitConverter.GetBytes(add);
Array.Reverse(addBytes);
byte[] paddedBytes = new byte[16];
Array.Copy(addBytes, 0, paddedBytes, 12, 4);
return Add(input, paddedBytes);
}
/// <summary>
/// Add two numbers represented by byte arrays
/// </summary>
/// <param name="left">Byte array to add to</param>
/// <param name="right">Amount to add</param>
/// <returns>Byte array representing the new value</returns>
public static byte[] Add(byte[] left, byte[] right)
{
int addBytes = Math.Min(left.Length, right.Length);
int outLength = Math.Max(left.Length, right.Length);
byte[] output = new byte[outLength];
uint carry = 0;
for (int i = addBytes - 1; i >= 0; i--)
{
uint addValue = (uint)(left[i] + right[i]) + carry;
output[i] = (byte)addValue;
carry = addValue >> 8;
}
if (outLength != addBytes && left.Length == outLength)
Array.Copy(left, addBytes, output, addBytes, outLength - addBytes);
else if (outLength != addBytes && right.Length == outLength)
Array.Copy(right, addBytes, output, addBytes, outLength - addBytes);
return output;
}
/// <summary>
/// Perform a rotate left on a byte array
/// </summary>
/// <param name="val">Byte array value to rotate</param>
/// <param name="r_bits">Number of bits to rotate</param>
/// <returns>Rotated byte array value</returns>
public static byte[] RotateLeft(byte[] val, int r_bits)
{
byte[] output = new byte[val.Length];
Array.Copy(val, output, output.Length);
// Shift by bytes
while (r_bits >= 8)
{
byte temp = output[0];
for (int i = 0; i < output.Length - 1; i++)
{
output[i] = output[i + 1];
}
output[output.Length - 1] = temp;
r_bits -= 8;
}
// Shift by bits
if (r_bits > 0)
{
byte bitMask = (byte)(8 - r_bits), carry, wrap = 0;
for (int i = 0; i < output.Length; i++)
{
carry = (byte)((255 << bitMask & output[i]) >> bitMask);
// Make sure the first byte carries to the end
if (i == 0)
wrap = carry;
// Otherwise, move to the last byte
else
output[i - 1] |= carry;
// Shift the current bits
output[i] <<= r_bits;
}
// Make sure the wrap happens
output[output.Length - 1] |= wrap;
}
return output;
}
/// <summary>
/// XOR two numbers represented by byte arrays
/// </summary>
/// <param name="left">Byte array to XOR to</param>
/// <param name="right">Amount to XOR</param>
/// <returns>Byte array representing the new value</returns>
public static byte[] Xor(byte[] left, byte[] right)
{
int xorBytes = Math.Min(left.Length, right.Length);
int outLength = Math.Max(left.Length, right.Length);
byte[] output = new byte[outLength];
for (int i = 0; i < xorBytes; i++)
{
output[i] = (byte)(left[i] ^ right[i]);
}
if (outLength != xorBytes && left.Length == outLength)
Array.Copy(left, xorBytes, output, xorBytes, outLength - xorBytes);
else if (outLength != xorBytes && right.Length == outLength)
Array.Copy(right, xorBytes, output, xorBytes, outLength - xorBytes);
return output;
}
#endregion
}
}

View File

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

542
NDecrypt.Core/DSTool.cs Normal file
View File

@@ -0,0 +1,542 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.Nitro;
using SabreTools.Serialization.Deserializers;
namespace NDecrypt.Core
{
public class DSTool : ITool
{
#region Encryption process variables
private uint[] _cardHash = new uint[0x412];
private uint[] _arg2 = new uint[3];
#endregion
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs _decryptArgs;
public DSTool(DecryptArgs decryptArgs)
{
_decryptArgs = decryptArgs;
}
#region Encrypt
/// <inheritdoc/>
public bool EncryptFile(string filename, bool force)
{
try
{
// Open the read and write on the same file for inplace processing
using var reader = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var commonHeader = Nitro.ParseCommonHeader(reader);
if (commonHeader == null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
}
// Reset state variables
_cardHash = new uint[0x412];
_arg2 = new uint[3];
// Encrypt the secure area
EncryptSecureArea(commonHeader, force, reader, writer);
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid DS or DSi file and try again.");
return false;
}
}
/// <summary>
/// Encrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="commonHeader">CommonHeader representing the DS file header</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void EncryptSecureArea(CommonHeader commonHeader, bool force, Stream reader, Stream writer)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
// If we're not forcing the operation, check to see if we should be proceeding
else
{
bool? isDecrypted = CheckIfDecrypted(reader);
if (isDecrypted == null)
{
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (!isDecrypted.Value)
{
Console.WriteLine("File is already encrypted");
return;
}
}
EncryptARM9(commonHeader, reader, writer);
Console.WriteLine("File has been encrypted");
}
/// <summary>
/// Encrypt the secure ARM9 region of the file, if possible
/// </summary>
/// <param name="commonHeader">CommonHeader representing the DS header</param>
/// <param name="encrypt">Indicates if the file should be encrypted or decrypted</param>
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void EncryptARM9(CommonHeader commonHeader, Stream reader, Stream writer)
{
// Seek to the beginning of the secure area
reader.Seek(0x4000, SeekOrigin.Begin);
writer.Seek(0x4000, SeekOrigin.Begin);
// Grab the first two blocks
uint p0 = reader.ReadUInt32();
uint p1 = reader.ReadUInt32();
// Perform the initialization steps
Init1(commonHeader);
_arg2[1] <<= 1;
_arg2[2] >>= 1;
Init2();
// Ensure alignment
reader.Seek(0x4008, SeekOrigin.Begin);
writer.Seek(0x4008, SeekOrigin.Begin);
// Loop throgh the main encryption step
uint size = 0x800 - 8;
while (size > 0)
{
p0 = reader.ReadUInt32();
p1 = reader.ReadUInt32();
Encrypt(ref p1, ref p0);
writer.Write(p0);
writer.Write(p1);
size -= 8;
}
// Replace the header explicitly
reader.Seek(0x4000, SeekOrigin.Begin);
writer.Seek(0x4000, SeekOrigin.Begin);
p0 = reader.ReadUInt32();
p1 = reader.ReadUInt32();
if (p0 == 0xE7FFDEFF && p1 == 0xE7FFDEFF)
{
p0 = Constants.MAGIC30;
p1 = Constants.MAGIC34;
}
Encrypt(ref p1, ref p0);
Init1(commonHeader);
Encrypt(ref p1, ref p0);
writer.Write(p0);
writer.Write(p1);
}
/// <summary>
/// Perform an encryption step
/// </summary>
/// <param name="arg1">First unsigned value to use in encryption</param>
/// <param name="arg2">Second unsigned value to use in encryption</param>
private void Encrypt(ref uint arg1, ref uint arg2)
{
uint a = arg1;
uint b = arg2;
for (int i = 0; i < 16; i++)
{
uint c = _cardHash[i] ^ a;
a = b ^ Lookup(c);
b = c;
}
arg2 = a ^ _cardHash[16];
arg1 = b ^ _cardHash[17];
}
#endregion
#region Decrypt
/// <inheritdoc/>
public bool DecryptFile(string filename, bool force)
{
try
{
// Open the read and write on the same file for inplace processing
using var reader = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var commonHeader = Nitro.ParseCommonHeader(reader);
if (commonHeader == null)
{
Console.WriteLine("Error: Not a DS or DSi Rom!");
return false;
}
// Reset state variables
_cardHash = new uint[0x412];
_arg2 = new uint[3];
// Decrypt the secure area
DecryptSecureArea(commonHeader, force, reader, writer);
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid DS or DSi file and try again.");
return false;
}
}
/// <summary>
/// Decrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="commonHeader">CommonHeader representing the DS file header</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void DecryptSecureArea(CommonHeader commonHeader, bool force, Stream reader, Stream writer)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
// If we're not forcing the operation, check to see if we should be proceeding
else
{
bool? isDecrypted = CheckIfDecrypted(reader);
if (isDecrypted == null)
{
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (isDecrypted.Value)
{
Console.WriteLine("File is already decrypted");
return;
}
}
DecryptARM9(commonHeader, reader, writer);
Console.WriteLine("File has been decrypted");
}
/// <summary>
/// Decrypt the secure ARM9 region of the file, if possible
/// </summary>
/// <param name="commonHeader">CommonHeader representing the DS header</param>
/// <param name="">Indicates if the file should be encrypted or decrypted</param>
/// <param name="reader">Stream representing the input</param>
/// <param name="writer">Stream representing the output</param>
private void DecryptARM9(CommonHeader commonHeader, Stream reader, Stream writer)
{
// Seek to the beginning of the secure area
reader.Seek(0x4000, SeekOrigin.Begin);
writer.Seek(0x4000, SeekOrigin.Begin);
// Grab the first two blocks
uint p0 = reader.ReadUInt32();
uint p1 = reader.ReadUInt32();
// Perform the initialization steps
Init1(commonHeader);
Decrypt(ref p1, ref p0);
_arg2[1] <<= 1;
_arg2[2] >>= 1;
Init2();
// Set the proper flags
Decrypt(ref p1, ref p0);
if (p0 == Constants.MAGIC30 && p1 == Constants.MAGIC34)
{
p0 = 0xE7FFDEFF;
p1 = 0xE7FFDEFF;
}
writer.Write(p0);
writer.Write(p1);
// Ensure alignment
reader.Seek(0x4008, SeekOrigin.Begin);
writer.Seek(0x4008, SeekOrigin.Begin);
// Loop throgh the main encryption step
uint size = 0x800 - 8;
while (size > 0)
{
p0 = reader.ReadUInt32();
p1 = reader.ReadUInt32();
Decrypt(ref p1, ref p0);
writer.Write(p0);
writer.Write(p1);
size -= 8;
}
}
/// <summary>
/// Perform a decryption step
/// </summary>
/// <param name="arg1">First unsigned value to use in decryption</param>
/// <param name="arg2">Second unsigned value to use in decryption</param>
private void Decrypt(ref uint arg1, ref uint arg2)
{
uint a = arg1;
uint b = arg2;
for (int i = 17; i > 1; i--)
{
uint c = _cardHash[i] ^ a;
a = b ^ Lookup(c);
b = c;
}
arg1 = b ^ _cardHash[0];
arg2 = a ^ _cardHash[1];
}
#endregion
#region Info
/// <inheritdoc/>
public string? GetInformation(string filename)
{
try
{
// Open the file for reading
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Get a string builder for the status
var sb = new StringBuilder();
sb.Append("\tSecure Area: ");
// Get the encryption status
bool? decrypted = CheckIfDecrypted(input);
if (decrypted == null)
sb.Append("Empty");
else if (decrypted == true)
sb.Append("Decrypted");
else
sb.Append("Encrypted");
// Return the status for the secure area
sb.Append(Environment.NewLine);
return sb.ToString();
}
catch (Exception ex)
{
Console.WriteLine(ex);
return null;
}
}
#endregion
#region Common
/// <summary>
/// Determine if the current file is already decrypted or not (or has an empty secure area)
/// </summary>
/// <param name="reader">Stream representing the input</param>
/// <returns>True if the file has known values for a decrypted file, null if it's empty, false otherwise</returns>
private static bool? CheckIfDecrypted(Stream reader)
{
reader.Seek(0x4000, SeekOrigin.Begin);
uint firstValue = reader.ReadUInt32();
uint secondValue = reader.ReadUInt32();
// Empty secure area standard
if (firstValue == 0x00000000 && secondValue == 0x00000000)
{
Console.WriteLine("Empty secure area found. Cannot be encrypted or decrypted.");
return null;
}
// Improperly decrypted empty secure area (decrypt empty with woodsec)
else if ((firstValue == 0xE386C397 && secondValue == 0x82775B7E)
|| (firstValue == 0xF98415B8 && secondValue == 0x698068FC)
|| (firstValue == 0xA71329EE && secondValue == 0x2A1D4C38)
|| (firstValue == 0xC44DCC48 && secondValue == 0x38B6F8CB)
|| (firstValue == 0x3A9323B5 && secondValue == 0xC0387241))
{
Console.WriteLine("Improperly decrypted empty secure area found. Should be encrypted to get proper value.");
return true;
}
// Improperly encrypted empty secure area (encrypt empty with woodsec)
else if ((firstValue == 0x4BCE88BE && secondValue == 0xD3662DD1)
|| (firstValue == 0x2543C534 && secondValue == 0xCC4BE38E))
{
Console.WriteLine("Improperly encrypted empty secure area found. Should be decrypted to get proper value.");
return false;
}
// Properly decrypted nonstandard value (mastering issue)
else if ((firstValue == 0xD0D48B67 && secondValue == 0x39392F23) // Dragon Quest 5 (EU)
|| (firstValue == 0x014A191A && secondValue == 0xA5C470B9) // Dragon Quest 5 (USA)
|| (firstValue == 0x7829BC8D && secondValue == 0x9968EF44) // Dragon Quest 5 (JP)
|| (firstValue == 0xC4A15AB8 && secondValue == 0xD2E667C8) // Prince of Persia (EU)
|| (firstValue == 0xD5E97D20 && secondValue == 0x21B2A159)) // Prince of Persia (USA)
{
Console.WriteLine("Decrypted secure area for known, nonstandard value found.");
return true;
}
// Properly decrypted prototype value
else if (firstValue == 0xBA35F813 && secondValue == 0xB691AAE8)
{
Console.WriteLine("Decrypted secure area for prototype found.");
return true;
}
// Strange, unlicenced values that can't determine decryption state
else if ((firstValue == 0xE1D830D8 && secondValue == 0xE3530000) // Aquela Ball (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xDC002A02 && secondValue == 0x2900E612) // Bahlz (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03BA3 && secondValue == 0xE2011CFF) // Battle Ship (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3A01001 && secondValue == 0xE1A02001) // Breakout!! DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE793200C && secondValue == 0xE4812004) // Bubble Fusion (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE583C0DC && secondValue == 0x0A00000B) // Carre Rouge (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x0202453C && secondValue == 0x02060164) // ChainReaction (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xEBFFF218 && secondValue == 0xE31000FF) // Collection (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x4A6CD003 && secondValue == 0x425B2301) // DiggerDS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3A00001 && secondValue == 0xEBFFFF8C) // Double Skill (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x21043701 && secondValue == 0x45BA448C) // DSChess (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE59D0010 && secondValue == 0xE0833000) // Hexa-Virus (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE5C3A006 && secondValue == 0xE5C39007) // Invasion (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1D920F4 && secondValue == 0xE06A3000) // JoggleDS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE59F32EC && secondValue == 0xE5DD7011) // London Underground (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE08A3503 && secondValue == 0xE1D3C4B8) // NumberMinds (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A0C001 && secondValue == 0xE0031001) // Paddle Battle (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03005 && secondValue == 0xE88D0180) // Pop the Balls (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE8BD4030 && secondValue == 0xE12FFF1E) // Solitaire DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE0A88006 && secondValue == 0xE1A00003) // Squash DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE51F3478 && secondValue == 0xEB004A02) // Super Snake DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x1C200052 && secondValue == 0xFD12F013) // Tales of Dagur (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x601F491E && secondValue == 0x041B880B) // Tetris & Touch (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03843 && secondValue == 0xE0000293) // Tic Tac Toe (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3530000 && secondValue == 0x13A03003) // Warrior Training (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x02054A80 && secondValue == 0x02054B80)) // Zi (World) (Unl) (Datel Games n' Music)
{
Console.WriteLine("Unlicensed invalid value found. Unknown if encrypted or decrypted.");
return null;
}
// Standard decryption values
return firstValue == 0xE7FFDEFF && secondValue == 0xE7FFDEFF;
}
/// <summary>
/// First common initialization step
/// </summary>
/// <param name="commonHeader">CommonHeader representing the DS file</param>
private void Init1(CommonHeader commonHeader)
{
Buffer.BlockCopy(_decryptArgs.NitroEncryptionData, 0, _cardHash, 0, 4 * (1024 + 18));
_arg2 = [commonHeader.GameCode, commonHeader.GameCode >> 1, commonHeader.GameCode << 1];
Init2();
Init2();
}
/// <summary>
/// Second common initialization step
/// </summary>
private void Init2()
{
Encrypt(ref _arg2[2], ref _arg2[1]);
Encrypt(ref _arg2[1], ref _arg2[0]);
byte[] allBytes =[.. BitConverter.GetBytes(_arg2[0]),
.. BitConverter.GetBytes(_arg2[1]),
.. BitConverter.GetBytes(_arg2[2])];
UpdateHashtable(allBytes);
}
/// <summary>
/// Lookup the value from the hashtable
/// </summary>
/// <param name="v">Value to lookup in the hashtable</param>
/// <returns>Processed value through the hashtable</returns>
private uint Lookup(uint v)
{
uint a = (v >> 24) & 0xFF;
uint b = (v >> 16) & 0xFF;
uint c = (v >> 8) & 0xFF;
uint d = (v >> 0) & 0xFF;
a = _cardHash[a + 18 + 0];
b = _cardHash[b + 18 + 256];
c = _cardHash[c + 18 + 512];
d = _cardHash[d + 18 + 768];
return d + (c ^ (b + a));
}
/// <summary>
/// Update the hashtable
/// </summary>
/// <param name="arg1">Value to update the hashtable with</param>
private void UpdateHashtable(byte[] arg1)
{
for (int j = 0; j < 18; j++)
{
uint r3 = 0;
for (int i = 0; i < 4; i++)
{
r3 <<= 8;
r3 |= arg1[(j * 4 + i) & 7];
}
_cardHash[j] ^= r3;
}
uint tmp1 = 0;
uint tmp2 = 0;
for (int i = 0; i < 18; i += 2)
{
Encrypt(ref tmp1, ref tmp2);
_cardHash[i + 0] = tmp1;
_cardHash[i + 1] = tmp2;
}
for (int i = 0; i < 0x400; i += 2)
{
Encrypt(ref tmp1, ref tmp2);
_cardHash[i + 18 + 0] = tmp1;
_cardHash[i + 18 + 1] = tmp2;
}
}
#endregion
}
}

View File

@@ -0,0 +1,868 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.IO.Readers;
using SabreTools.Matching;
namespace NDecrypt.Core
{
public class DecryptArgs
{
#region Common Fields
/// <summary>
/// Represents if all of the keys have been initialized properly
/// </summary>
public bool? IsReady { get; private set; }
#endregion
#region DS-Specific Fields
/// <summary>
/// Blowfish Table
/// </summary>
public byte[] NitroEncryptionData { get; private set; } = [];
/// <summary>
/// Encryption data taken from woodsec
/// </summary>
private static readonly byte[] _nitroEncryptionData = [
0x99,0xD5,0x20,0x5F,0x57,0x44,0xF5,0xB9,0x6E,0x19,0xA4,0xD9,0x9E,0x6A,0x5A,0x94,
0xD8,0xAE,0xF1,0xEB,0x41,0x75,0xE2,0x3A,0x93,0x82,0xD0,0x32,0x33,0xEE,0x31,0xD5,
0xCC,0x57,0x61,0x9A,0x37,0x06,0xA2,0x1B,0x79,0x39,0x72,0xF5,0x55,0xAE,0xF6,0xBE,
0x5F,0x1B,0x69,0xFB,0xE5,0x9D,0xF1,0xE9,0xCE,0x2C,0xD9,0xA1,0x5E,0x32,0x05,0xE6,
0xFE,0xD3,0xFE,0xCF,0xD4,0x62,0x04,0x0D,0x8B,0xF5,0xEC,0xB7,0x2B,0x60,0x79,0xBB,
0x12,0x95,0x31,0x0D,0x6E,0x3F,0xDA,0x2B,0x88,0x84,0xF0,0xF1,0x3D,0x12,0x7E,0x25,
0x45,0x22,0xF1,0xBB,0x24,0x06,0x1A,0x06,0x11,0xAD,0xDF,0x28,0x8B,0x64,0x81,0x34,
0x2B,0xEB,0x33,0x29,0x99,0xAA,0xF2,0xBD,0x9C,0x14,0x95,0x9D,0x9F,0xF7,0xF5,0x8C,
0x72,0x97,0xA1,0x29,0x9D,0xD1,0x5F,0xCF,0x66,0x4D,0x07,0x1A,0xDE,0xD3,0x4A,0x4B,
0x85,0xC9,0xA7,0xA3,0x17,0x95,0x05,0x3A,0x3D,0x49,0x0A,0xBF,0x0A,0x89,0x8B,0xA2,
0x4A,0x82,0x49,0xDD,0x27,0x90,0xF1,0x0B,0xE9,0xEB,0x1C,0x6A,0x83,0x76,0x45,0x05,
0xBA,0x81,0x70,0x61,0x17,0x3F,0x4B,0xDE,0xAE,0xCF,0xAB,0x39,0x57,0xF2,0x3A,0x56,
0x48,0x11,0xAD,0x8A,0x40,0xE1,0x45,0x3F,0xFA,0x9B,0x02,0x54,0xCA,0xA6,0x93,0xFB,
0xEF,0x4D,0xFE,0x6F,0xA3,0xD8,0x87,0x9C,0x08,0xBA,0xD5,0x48,0x6A,0x8D,0x2D,0xFD,
0x6E,0x15,0xF8,0x74,0xBD,0xBE,0x52,0x8B,0x18,0x22,0x8A,0x9E,0xFB,0x74,0x37,0x07,
0x1B,0x36,0x6C,0x4A,0x19,0xBA,0x42,0x62,0xB9,0x79,0x91,0x10,0x7B,0x67,0x65,0x96,
0xFE,0x02,0x23,0xE8,0xEE,0x99,0x8C,0x77,0x3E,0x5C,0x86,0x64,0x4D,0x6D,0x78,0x86,
0xA5,0x4F,0x65,0xE2,0x1E,0xB2,0xDF,0x5A,0x0A,0xD0,0x7E,0x08,0x14,0xB0,0x71,0xAC,
0xBD,0xDB,0x83,0x1C,0xB9,0xD7,0xA1,0x62,0xCD,0xC6,0x63,0x7C,0x52,0x69,0xC3,0xE6,
0xBF,0x75,0xCE,0x12,0x44,0x5D,0x21,0x04,0xFA,0xFB,0xD3,0x3C,0x38,0x11,0x63,0xD4,
0x95,0x85,0x41,0x49,0x46,0x09,0xF2,0x08,0x43,0x11,0xDC,0x1F,0x76,0xC0,0x15,0x6D,
0x1F,0x3C,0x63,0x70,0xEA,0x87,0x80,0x6C,0xC3,0xBD,0x63,0x8B,0xC2,0x37,0x21,0x37,
0xDC,0xEE,0x09,0x23,0x2E,0x37,0x6A,0x4D,0x73,0x90,0xF7,0x50,0x30,0xAC,0x1C,0x92,
0x04,0x10,0x23,0x91,0x4F,0xD2,0x07,0xAA,0x68,0x3E,0x4F,0x9A,0xC9,0x64,0x60,0x6A,
0xC8,0x14,0x21,0xF3,0xD6,0x22,0x41,0x12,0x44,0x24,0xCF,0xE6,0x8A,0x56,0xDD,0x0D,
0x53,0x4D,0xE1,0x85,0x1E,0x8C,0x52,0x5A,0x9C,0x19,0x84,0xC2,0x03,0x57,0xF1,0x6F,
0xE3,0x00,0xBE,0x58,0xF6,0x4C,0xED,0xD5,0x21,0x64,0x9C,0x1F,0xBE,0x55,0x03,0x3C,
0x4A,0xDC,0xFF,0xAA,0xC9,0xDA,0xE0,0x5D,0x5E,0xBF,0xE6,0xDE,0xF5,0xD8,0xB1,0xF8,
0xFF,0x36,0xB3,0xB9,0x62,0x67,0x95,0xDB,0x31,0x5F,0x37,0xED,0x4C,0x70,0x67,0x99,
0x90,0xB5,0x18,0x31,0x6C,0x3D,0x99,0x99,0xE4,0x42,0xDA,0xD3,0x25,0x42,0x13,0xA0,
0xAE,0xD7,0x70,0x6C,0xB1,0x55,0xCF,0xC7,0xD7,0x46,0xD5,0x43,0x61,0x17,0x3D,0x44,
0x28,0xE9,0x33,0x85,0xD5,0xD0,0xA2,0x93,0xAA,0x25,0x12,0x1F,0xFB,0xC5,0x0B,0x46,
0xF5,0x97,0x76,0x56,0x45,0xA6,0xBE,0x87,0xB1,0x94,0x6B,0xE8,0xB1,0xFE,0x33,0x99,
0xAE,0x1F,0x3E,0x6C,0x39,0x71,0x1D,0x09,0x00,0x90,0x37,0xE4,0x10,0x3E,0x75,0x74,
0xFF,0x8C,0x83,0x3B,0xB0,0xF1,0xB0,0xF9,0x01,0x05,0x47,0x42,0x95,0xF1,0xD6,0xAC,
0x7E,0x38,0xE6,0x9E,0x95,0x74,0x26,0x3F,0xB4,0x68,0x50,0x18,0xD0,0x43,0x30,0xB4,
0x4C,0x4B,0xE3,0x68,0xBF,0xE5,0x4D,0xB6,0x95,0x8B,0x0A,0xA0,0x74,0x25,0x32,0x77,
0xCF,0xA1,0xF7,0x2C,0xD8,0x71,0x13,0x5A,0xAB,0xEA,0xC9,0x51,0xE8,0x0D,0xEE,0xEF,
0xE9,0x93,0x7E,0x19,0xA7,0x1E,0x43,0x38,0x81,0x16,0x2C,0xA1,0x48,0xE3,0x73,0xCC,
0x29,0x21,0x6C,0xD3,0x5D,0xCE,0xA0,0xD9,0x61,0x71,0x43,0xA0,0x15,0x13,0xB5,0x64,
0x92,0xCF,0x2A,0x19,0xDC,0xAD,0xB7,0xA5,0x9F,0x86,0x65,0xF8,0x1A,0x9F,0xE7,0xFB,
0xF7,0xFD,0xB8,0x13,0x6C,0x27,0xDB,0x6F,0xDF,0x35,0x1C,0xF7,0x8D,0x2C,0x5B,0x9B,
0x12,0xAB,0x38,0x64,0x06,0xCC,0xDE,0x31,0xE8,0x4E,0x75,0x11,0x64,0xE3,0xFA,0xEA,
0xEB,0x34,0x54,0xC2,0xAD,0x3F,0x34,0xEB,0x93,0x2C,0x7D,0x26,0x36,0x9D,0x56,0xF3,
0x5A,0xE1,0xF6,0xB3,0x98,0x63,0x4A,0x9E,0x32,0x83,0xE4,0x9A,0x84,0x60,0x7D,0x90,
0x2E,0x13,0x0E,0xEE,0x93,0x4B,0x36,0xA2,0x85,0xEC,0x16,0x38,0xE8,0x88,0x06,0x02,
0xBF,0xF0,0xA0,0x3A,0xED,0xD7,0x6A,0x9A,0x73,0xE1,0x57,0xCF,0xF8,0x44,0xB8,0xDC,
0x2E,0x23,0x59,0xD1,0xDF,0x95,0x52,0x71,0x99,0x61,0xA0,0x4B,0xD5,0x7F,0x6E,0x78,
0xBA,0xA9,0xC5,0x30,0xD3,0x40,0x86,0x32,0x9D,0x32,0x0C,0x9C,0x37,0xB7,0x02,0x2F,
0xBA,0x54,0x98,0xA9,0xC4,0x13,0x04,0xC9,0x8D,0xBE,0xC8,0xE7,0x5D,0x97,0x50,0x2E,
0x93,0xD6,0x22,0x59,0x0C,0x27,0xBC,0x22,0x92,0xE0,0xA7,0x20,0x0F,0x93,0x6F,0x7F,
0x4C,0x9F,0xD3,0xB5,0xA6,0x2A,0x0B,0x74,0x67,0x49,0x7D,0x10,0x26,0xCB,0xD1,0xC5,
0x86,0x71,0xE7,0x8C,0xA0,0x9C,0xE9,0x5B,0xB2,0x1A,0xF6,0x01,0xEE,0x8C,0x9E,0x5E,
0x83,0xF2,0x1A,0xDB,0xE6,0xE5,0xEA,0x84,0x59,0x76,0xD2,0x7C,0xF6,0x8D,0xA5,0x49,
0x36,0x48,0xC2,0x16,0x52,0xBB,0x83,0xA3,0x74,0xB9,0x07,0x0C,0x3B,0xFF,0x61,0x28,
0xE1,0x61,0xE9,0xE4,0xEF,0x6E,0x15,0xAA,0x4E,0xBA,0xE8,0x5D,0x05,0x96,0xBB,0x32,
0x56,0xB0,0xFB,0x72,0x52,0x0F,0x0E,0xC8,0x42,0x25,0x65,0x76,0x89,0xAF,0xF2,0xDE,
0x10,0x27,0xF0,0x01,0x4B,0x74,0xA7,0x97,0x07,0xD5,0x26,0x54,0x54,0x09,0x1F,0x82,
0x0A,0x86,0x7D,0x30,0x39,0x0E,0xB3,0x26,0x9B,0x0B,0x57,0xBB,0x36,0x06,0x31,0xAF,
0xFD,0x79,0xFC,0xD9,0x30,0x10,0x2B,0x0C,0xB3,0xE1,0x9B,0xD7,0x7B,0xDC,0x5F,0xEF,
0xD2,0xF8,0x13,0x45,0x4D,0x47,0x75,0xBD,0x46,0x96,0x3C,0x7E,0x75,0xF3,0x3E,0xB5,
0x67,0xC5,0x9A,0x3B,0xB0,0x5B,0x29,0x6B,0xDE,0x80,0x5B,0xC8,0x15,0x05,0xB1,0x31,
0xB6,0xCE,0x49,0xDD,0xAD,0x84,0xB5,0xAE,0x60,0xDC,0x67,0x31,0x34,0x30,0xFE,0x4E,
0xBD,0x80,0x2F,0xA6,0xBF,0x63,0x39,0x21,0x86,0xD9,0x35,0x7F,0x16,0x68,0x22,0x05,
0x54,0xE9,0x90,0x26,0x8C,0x07,0x6C,0x51,0xA4,0x31,0x55,0xD7,0x09,0x07,0xA8,0x3E,
0x2E,0x53,0x66,0xC1,0xF8,0xF2,0x7B,0xC4,0xF2,0x58,0xCF,0xF1,0x87,0xC5,0xA2,0xE7,
0x27,0x8F,0x30,0x87,0x58,0xA0,0x64,0x62,0x23,0x18,0xB9,0x88,0x7C,0xFA,0xCE,0xC4,
0x98,0xAE,0xAD,0x17,0xCC,0x4A,0x5B,0xF3,0xE9,0x48,0xD5,0x56,0xD3,0x0D,0xF2,0xC8,
0x92,0x73,0x8C,0xDB,0xD7,0x2F,0x56,0xAC,0x81,0xF9,0x92,0x69,0x4D,0xC6,0x32,0xF6,
0xE6,0xC0,0x8D,0x21,0xE2,0x76,0x80,0x61,0x11,0xBC,0xDC,0x6C,0x93,0xAF,0x19,0x69,
0x9B,0xD0,0xBF,0xB9,0x31,0x9F,0x02,0x67,0xA3,0x51,0xEE,0x83,0x06,0x22,0x7B,0x0C,
0xAB,0x49,0x42,0x40,0xB8,0xD5,0x01,0x7D,0xCE,0x5E,0xF7,0x55,0x53,0x39,0xC5,0x99,
0x46,0xD8,0x87,0x9F,0xBA,0xF7,0x64,0xB4,0xE3,0x9A,0xFA,0xA1,0x6D,0x90,0x68,0x10,
0x30,0xCA,0x8A,0x54,0xA7,0x9F,0x60,0xC3,0x19,0xF5,0x6B,0x0D,0x7A,0x51,0x98,0xE6,
0x98,0x43,0x51,0xB4,0xD6,0x35,0xE9,0x4F,0xC3,0xDF,0x0F,0x7B,0xD6,0x2F,0x5C,0xBD,
0x3A,0x15,0x61,0x19,0xF1,0x4B,0xCB,0xAA,0xDC,0x6D,0x64,0xC9,0xD3,0xC6,0x1E,0x56,
0xEF,0x38,0x4C,0x50,0x71,0x86,0x75,0xCC,0x0D,0x0D,0x4E,0xE9,0x28,0xF6,0x06,0x5D,
0x70,0x1B,0xAA,0xD3,0x45,0xCF,0xA8,0x39,0xAC,0x95,0xA6,0x2E,0xB4,0xE4,0x22,0xD4,
0x74,0xA8,0x37,0x5F,0x48,0x7A,0x04,0xCC,0xA5,0x4C,0x40,0xD8,0x28,0xB4,0x28,0x08,
0x0D,0x1C,0x72,0x52,0x41,0xF0,0x7D,0x47,0x19,0x3A,0x53,0x4E,0x58,0x84,0x62,0x6B,
0x93,0xB5,0x8A,0x81,0x21,0x4E,0x0D,0xDC,0xB4,0x3F,0xA2,0xC6,0xFC,0xC9,0x2B,0x40,
0xDA,0x38,0x04,0xE9,0x5E,0x5A,0x86,0x6B,0x0C,0x22,0x25,0x85,0x68,0x11,0x8D,0x7C,
0x92,0x1D,0x95,0x55,0x4D,0xAB,0x8E,0xBB,0xDA,0xA6,0xE6,0xB7,0x51,0xB6,0x32,0x5A,
0x05,0x41,0xDD,0x05,0x2A,0x0A,0x56,0x50,0x91,0x17,0x47,0xCC,0xC9,0xE6,0x7E,0xB5,
0x61,0x4A,0xDB,0x73,0x67,0x51,0xC8,0x33,0xF5,0xDA,0x6E,0x74,0x2E,0x54,0xC3,0x37,
0x0D,0x6D,0xAF,0x08,0xE8,0x15,0x8A,0x5F,0xE2,0x59,0x21,0xCD,0xA8,0xDE,0x0C,0x06,
0x5A,0x77,0x6B,0x5F,0xDB,0x18,0x65,0x3E,0xC8,0x50,0xDE,0x78,0xE0,0xB8,0x82,0xB3,
0x5D,0x4E,0x72,0x32,0x07,0x4F,0xC1,0x34,0x23,0xBA,0x96,0xB7,0x67,0x4E,0xA4,0x28,
0x1E,0x34,0x62,0xEB,0x2D,0x6A,0x70,0xE9,0x2F,0x42,0xC4,0x70,0x4E,0x5A,0x31,0x9C,
0xF9,0x5B,0x47,0x28,0xAA,0xDA,0x71,0x6F,0x38,0x1F,0xB3,0x78,0xC4,0x92,0x6B,0x1C,
0x9E,0xF6,0x35,0x9A,0xB7,0x4D,0x0E,0xBF,0xCC,0x18,0x29,0x41,0x03,0x48,0x35,0x5D,
0x55,0xD0,0x2B,0xC6,0x29,0xAF,0x5C,0x60,0x74,0x69,0x8E,0x5E,0x9B,0x7C,0xD4,0xBD,
0x7B,0x44,0x64,0x7D,0x3F,0x92,0x5D,0x69,0xB6,0x1F,0x00,0x4B,0xD4,0x83,0x35,0xCF,
0x7E,0x64,0x4E,0x17,0xAE,0x8D,0xD5,0x2E,0x9A,0x28,0x12,0x4E,0x2E,0x2B,0x49,0x08,
0x5C,0xAE,0xC6,0x46,0x85,0xAE,0x41,0x61,0x1E,0x6F,0x82,0xD2,0x51,0x37,0x16,0x1F,
0x0B,0xF6,0x59,0xA4,0x9A,0xCA,0x5A,0xAF,0x0D,0xD4,0x33,0x8B,0x20,0x63,0xF1,0x84,
0x80,0x5C,0xCB,0xCF,0x08,0xB4,0xB9,0xD3,0x16,0x05,0xBD,0x62,0x83,0x31,0x9B,0x56,
0x51,0x98,0x9F,0xBA,0xB2,0x5B,0xAA,0xB2,0x22,0x6B,0x2C,0xB5,0xD4,0x48,0xFA,0x63,
0x2B,0x5F,0x58,0xFA,0x61,0xFA,0x64,0x09,0xBB,0x38,0xE0,0xB8,0x9D,0x92,0x60,0xA8,
0x0D,0x67,0x6F,0x0E,0x37,0xF5,0x0D,0x01,0x9F,0xC2,0x77,0xD4,0xFE,0xEC,0xF1,0x73,
0x30,0x39,0xE0,0x7D,0xF5,0x61,0x98,0xE4,0x2C,0x28,0x55,0x04,0x56,0x55,0xDB,0x2F,
0x6B,0xEC,0xE5,0x58,0x06,0xB6,0x64,0x80,0x6A,0x2A,0x1A,0x4E,0x5B,0x0F,0xD8,0xC4,
0x0A,0x2E,0x52,0x19,0xD9,0x62,0xF5,0x30,0x48,0xBE,0x8C,0x7B,0x4F,0x38,0x9B,0xA2,
0xC3,0xAF,0xC9,0xD3,0xC7,0xC1,0x62,0x41,0x86,0xB9,0x61,0x21,0x57,0x6F,0x99,0x4F,
0xC1,0xBA,0xCE,0x7B,0xB5,0x3B,0x4D,0x5E,0x8A,0x8B,0x44,0x57,0x5F,0x13,0x5F,0x70,
0x6D,0x5B,0x29,0x47,0xDC,0x38,0xE2,0xEC,0x04,0x55,0x65,0x12,0x2A,0xE8,0x17,0x43,
0xE1,0x8E,0xDD,0x2A,0xB3,0xE2,0x94,0xF7,0x09,0x6E,0x5C,0xE6,0xEB,0x8A,0xF8,0x6D,
0x89,0x49,0x54,0x48,0xF5,0x2F,0xAD,0xBF,0xEA,0x94,0x4B,0xCA,0xFC,0x39,0x87,0x82,
0x5F,0x8A,0x01,0xF2,0x75,0xF2,0xE6,0x71,0xD6,0xD8,0x42,0xDE,0xF1,0x2D,0x1D,0x28,
0xA6,0x88,0x7E,0xA3,0xA0,0x47,0x1D,0x30,0xD9,0xA3,0x71,0xDF,0x49,0x1C,0xCB,0x01,
0xF8,0x36,0xB1,0xF2,0xF0,0x22,0x58,0x5D,0x45,0x6B,0xBD,0xA0,0xBB,0xB2,0x88,0x42,
0xC7,0x8C,0x28,0xCE,0x93,0xE8,0x90,0x63,0x08,0x90,0x7C,0x89,0x3C,0xF5,0x7D,0xB7,
0x04,0x2D,0x4F,0x55,0x51,0x16,0xFD,0x7E,0x79,0xE8,0xBE,0xC1,0xF2,0x12,0xD4,0xF8,
0xB4,0x84,0x05,0x23,0xA0,0xCC,0xD2,0x2B,0xFD,0xE1,0xAB,0xAD,0x0D,0xD1,0x55,0x6C,
0x23,0x41,0x94,0x4D,0x77,0x37,0x4F,0x05,0x28,0x0C,0xBF,0x17,0xB3,0x12,0x67,0x6C,
0x8C,0xC3,0x5A,0xF7,0x41,0x84,0x2A,0x6D,0xD0,0x94,0x12,0x27,0x2C,0xB4,0xED,0x9C,
0x4D,0xEC,0x47,0x82,0x97,0xD5,0x67,0xB9,0x1B,0x9D,0xC0,0x55,0x07,0x7E,0xE5,0x8E,
0xE2,0xA8,0xE7,0x3E,0x12,0xE4,0x0E,0x3A,0x2A,0x45,0x55,0x34,0xA2,0xF9,0x2D,0x5A,
0x1B,0xAB,0x52,0x7C,0x83,0x10,0x5F,0x55,0xD2,0xF1,0x5A,0x43,0x2B,0xC6,0xA7,0xA4,
0x89,0x15,0x95,0xE8,0xB4,0x4B,0x9D,0xF8,0x75,0xE3,0x9F,0x60,0x78,0x5B,0xD6,0xE6,
0x0D,0x44,0xE6,0x21,0x06,0xBD,0x47,0x22,0x53,0xA4,0x00,0xAD,0x8D,0x43,0x13,0x85,
0x39,0xF7,0xAA,0xFC,0x38,0xAF,0x7B,0xED,0xFC,0xE4,0x2B,0x54,0x50,0x98,0x4C,0xFC,
0x85,0x80,0xF7,0xDF,0x3C,0x80,0x22,0xE1,0x94,0xDA,0xDE,0x24,0xC6,0xB0,0x7A,0x39,
0x38,0xDC,0x0F,0xA1,0xA7,0xF4,0xF9,0x6F,0x63,0x18,0x57,0x8B,0x84,0x41,0x2A,0x2E,
0xD4,0x53,0xF2,0xD9,0x00,0x0F,0xD0,0xDD,0x99,0x6E,0x19,0xA6,0x0A,0xD0,0xEC,0x5B,
0x58,0x24,0xAB,0xC0,0xCB,0x06,0x65,0xEC,0x1A,0x13,0x38,0x94,0x0A,0x67,0x03,0x2F,
0x3F,0xF7,0xE3,0x77,0x44,0x77,0x33,0xC6,0x14,0x39,0xD0,0xE3,0xC0,0xA2,0x08,0x79,
0xBB,0x40,0x99,0x57,0x41,0x0B,0x01,0x90,0xCD,0xE1,0xCC,0x48,0x67,0xDB,0xB3,0xAF,
0x88,0x74,0xF3,0x4C,0x82,0x8F,0x72,0xB1,0xB5,0x23,0x29,0xC4,0x12,0x6C,0x19,0xFC,
0x8E,0x46,0xA4,0x9C,0xC4,0x25,0x65,0x87,0xD3,0x6D,0xBE,0x8A,0x93,0x11,0x03,0x38,
0xED,0x83,0x2B,0xF3,0x46,0xA4,0x93,0xEA,0x3B,0x53,0x85,0x1D,0xCE,0xD4,0xF1,0x08,
0x83,0x27,0xED,0xFC,0x9B,0x1A,0x18,0xBC,0xF9,0x8B,0xAE,0xDC,0x24,0xAB,0x50,0x38,
0xE9,0x72,0x4B,0x10,0x22,0x17,0x7B,0x46,0x5D,0xAB,0x59,0x64,0xF3,0x40,0xAE,0xF8,
0xBB,0xE5,0xC8,0xF9,0x26,0x03,0x4E,0x55,0x7D,0xEB,0xEB,0xFE,0xF7,0x39,0xE6,0xE0,
0x0A,0x11,0xBE,0x2E,0x28,0xFF,0x98,0xED,0xC0,0xC9,0x42,0x56,0x42,0xC3,0xFD,0x00,
0xF6,0xAF,0x87,0xA2,0x5B,0x01,0x3F,0x32,0x92,0x47,0x95,0x9A,0x72,0xA5,0x32,0x3D,
0xAE,0x6B,0xD0,0x9B,0x07,0xD2,0x49,0x92,0xE3,0x78,0x4A,0xFA,0xA1,0x06,0x7D,0xF2,
0x41,0xCF,0x77,0x74,0x04,0x14,0xB2,0x0C,0x86,0x84,0x64,0x16,0xD5,0xBB,0x51,0xA1,
0xE5,0x6F,0xF1,0xD1,0xF2,0xE2,0xF7,0x5F,0x58,0x20,0x4D,0xB8,0x57,0xC7,0xCF,0xDD,
0xC5,0xD8,0xBE,0x76,0x3D,0xF6,0x5F,0x7E,0xE7,0x2A,0x8B,0x88,0x24,0x1B,0x38,0x3F,
0x0E,0x41,0x23,0x77,0xF5,0xF0,0x4B,0xD4,0x0C,0x1F,0xFA,0xA4,0x0B,0x80,0x5F,0xCF,
0x45,0xF6,0xE0,0xDA,0x2F,0x34,0x59,0x53,0xFB,0x20,0x3C,0x52,0x62,0x5E,0x35,0xB5,
0x62,0xFE,0x8B,0x60,0x63,0xE3,0x86,0x5A,0x15,0x1A,0x6E,0xD1,0x47,0x45,0xBC,0x32,
0xB4,0xEB,0x67,0x38,0xAB,0xE4,0x6E,0x33,0x3A,0xB5,0xED,0xA3,0xAD,0x67,0xE0,0x4E,
0x41,0x95,0xEE,0x62,0x62,0x71,0x26,0x1D,0x31,0xEF,0x62,0x30,0xAF,0xD7,0x82,0xAC,
0xC2,0xDC,0x05,0x04,0xF5,0x97,0x07,0xBF,0x11,0x59,0x23,0x07,0xC0,0x64,0x02,0xE8,
0x97,0xE5,0x3E,0xAF,0x18,0xAC,0x59,0xA6,0x8B,0x4A,0x33,0x90,0x1C,0x6E,0x7C,0x9C,
0x20,0x7E,0x4C,0x3C,0x3E,0x61,0x64,0xBB,0xC5,0x6B,0x7C,0x7E,0x3E,0x9F,0xC5,0x4C,
0x9F,0xEA,0x73,0xF5,0xD7,0x89,0xC0,0x4C,0xF4,0xFB,0xF4,0x2D,0xEC,0x14,0x1B,0x51,
0xD5,0xC1,0x12,0xC8,0x10,0xDF,0x0B,0x4A,0x8B,0x9C,0xBC,0x93,0x45,0x6A,0x3E,0x3E,
0x7D,0xC1,0xA9,0xBA,0xCD,0xC1,0xB4,0x07,0xE4,0xE1,0x68,0x86,0x43,0xB2,0x6D,0x38,
0xF3,0xFB,0x0C,0x5C,0x66,0x37,0x71,0xDE,0x56,0xEF,0x6E,0xA0,0x10,0x40,0x65,0xA7,
0x98,0xF7,0xD0,0xBE,0x0E,0xC8,0x37,0x36,0xEC,0x10,0xCA,0x7C,0x9C,0xAB,0x84,0x1E,
0x05,0x17,0x76,0x02,0x1C,0x4F,0x52,0xAA,0x5F,0xC1,0xC6,0xA0,0x56,0xB9,0xD8,0x04,
0x84,0x44,0x4D,0xA7,0x59,0xD8,0xDE,0x60,0xE6,0x38,0x0E,0x05,0x8F,0x03,0xE1,0x3B,
0x6D,0x81,0x04,0x33,0x6F,0x30,0x0B,0xCE,0x69,0x05,0x21,0x33,0xFB,0x26,0xBB,0x89,
0x7D,0xB6,0xAE,0x87,0x7E,0x51,0x07,0xE0,0xAC,0xF7,0x96,0x0A,0x6B,0xF9,0xC4,0x5C,
0x1D,0xE4,0x44,0x47,0xB8,0x5E,0xFA,0xE3,0x78,0x84,0x55,0x42,0x4B,0x48,0x5E,0xF7,
0x7D,0x47,0x35,0x86,0x1D,0x2B,0x43,0x05,0x03,0xEC,0x8A,0xB8,0x1E,0x06,0x3C,0x76,
0x0C,0x48,0x1A,0x43,0xA7,0xB7,0x8A,0xED,0x1E,0x13,0xC6,0x43,0xEE,0x10,0xEF,0xDB,
0xEC,0xFB,0x3C,0x83,0xB2,0x95,0x44,0xEF,0xD8,0x54,0x51,0x4E,0x2D,0x11,0x44,0x1D,
0xFB,0x36,0x59,0x1E,0x7A,0x34,0xC1,0xC3,0xCA,0x57,0x00,0x61,0xEA,0x67,0xA5,0x16,
0x9B,0x55,0xD0,0x55,0xE1,0x7F,0xD9,0x36,0xD2,0x40,0x76,0xAE,0xDC,0x01,0xCE,0xB0,
0x7A,0x83,0xD5,0xCB,0x20,0x98,0xEC,0x6B,0xC1,0x72,0x92,0x34,0xF3,0x82,0x57,0x37,
0x62,0x8A,0x32,0x36,0x0C,0x90,0x43,0xAE,0xAE,0x5C,0x9B,0x78,0x8E,0x13,0x65,0x02,
0xFD,0x68,0x71,0xC1,0xFE,0xB0,0x31,0xA0,0x24,0x82,0xB0,0xC3,0xB1,0x79,0x69,0xA7,
0xF5,0xD2,0xEB,0xD0,0x82,0xC0,0x32,0xDC,0x9E,0xC7,0x26,0x3C,0x6D,0x8D,0x98,0xC1,
0xBB,0x22,0xD4,0xD0,0x0F,0x33,0xEC,0x3E,0xB9,0xCC,0xE1,0xDC,0x6A,0x4C,0x77,0x36,
0x14,0x1C,0xF9,0xBF,0x81,0x9F,0x28,0x5F,0x71,0x85,0x32,0x29,0x90,0x75,0x48,0xC4,
0xB3,0x4A,0xCE,0xD8,0x44,0x8F,0x14,0x2F,0xFD,0x40,0x57,0xEF,0xAA,0x08,0x75,0xD9,
0x46,0xD1,0xD6,0x6E,0x32,0x55,0x1F,0xC3,0x18,0xFE,0x84,0x1F,0xFC,0x84,0xD5,0xFF,
0x71,0x5E,0x1B,0x48,0xC3,0x86,0x95,0x0E,0x28,0x08,0x27,0xD3,0x38,0x83,0x71,0x7B,
0x4C,0x80,0x63,0x54,0x9A,0x56,0xB0,0xAC,0xCF,0x80,0xCA,0x31,0x09,0xEF,0xFE,0xF3,
0xBE,0xAF,0x24,0x7E,0xA6,0xFE,0x53,0x3F,0xC2,0x8D,0x4A,0x33,0x68,0xD1,0x22,0xA6,
0x66,0xAD,0x7B,0xEA,0xDE,0xB6,0x43,0xB0,0xA1,0x25,0x95,0x00,0xA3,0x3F,0x75,0x46,
0x14,0x11,0x44,0xEC,0xD7,0x95,0xBC,0x92,0xF0,0x4F,0xA9,0x16,0x53,0x62,0x97,0x60,
0x2A,0x0F,0x41,0xF1,0x71,0x24,0xBE,0xEE,0x94,0x7F,0x08,0xCD,0x60,0x93,0xB3,0x85,
0x5B,0x07,0x00,0x3F,0xD8,0x0F,0x28,0x83,0x9A,0xD1,0x69,0x9F,0xD1,0xDA,0x2E,0xC3,
0x90,0x01,0xA2,0xB9,0x6B,0x4E,0x2A,0x66,0x9D,0xDA,0xAE,0xA6,0xEA,0x2A,0xD3,0x68,
0x2F,0x0C,0x0C,0x9C,0xD2,0x8C,0x4A,0xED,0xE2,0x9E,0x57,0x65,0x9D,0x09,0x87,0xA3,
0xB4,0xC4,0x32,0x5D,0xC9,0xD4,0x32,0x2B,0xB1,0xE0,0x71,0x1E,0x64,0x4D,0xE6,0x90,
0x71,0xE3,0x1E,0x40,0xED,0x7D,0xF3,0x84,0x0E,0xED,0xC8,0x78,0x76,0xAE,0xC0,0x71,
0x27,0x72,0xBB,0x05,0xEA,0x02,0x64,0xFB,0xF3,0x48,0x6B,0xB5,0x42,0x93,0x3F,0xED,
0x9F,0x13,0x53,0xD2,0xF7,0xFE,0x2A,0xEC,0x1D,0x47,0x25,0xDB,0x3C,0x91,0x86,0xC6,
0x8E,0xF0,0x11,0xFD,0x23,0x74,0x36,0xF7,0xA4,0xF5,0x9E,0x7A,0x7E,0x53,0x50,0x44,
0xD4,0x47,0xCA,0xD3,0xEB,0x38,0x6D,0xE6,0xD9,0x71,0x94,0x7F,0x4A,0xC6,0x69,0x4B,
0x11,0xF4,0x52,0xEA,0x22,0xFE,0x8A,0xB0,0x36,0x67,0x8B,0x59,0xE8,0xE6,0x80,0x2A,
0xEB,0x65,0x04,0x13,0xEE,0xEC,0xDC,0x9E,0x5F,0xB1,0xEC,0x05,0x6A,0x59,0xE6,0x9F,
0x5E,0x59,0x6B,0x89,0xBF,0xF7,0x1A,0xCA,0x44,0xF9,0x5B,0x6A,0x71,0x85,0x03,0xE4,
0x29,0x62,0xE0,0x70,0x6F,0x41,0xC4,0xCF,0xB2,0xB1,0xCC,0xE3,0x7E,0xA6,0x07,0xA8,
0x87,0xE7,0x7F,0x84,0x93,0xDB,0x52,0x4B,0x6C,0xEC,0x7E,0xDD,0xD4,0x24,0x48,0x10,
0x69,0x9F,0x04,0x60,0x74,0xE6,0x48,0x18,0xF3,0xE4,0x2C,0xB9,0x4F,0x2E,0x50,0x7A,
0xDF,0xD4,0x54,0x69,0x2B,0x8B,0xA7,0xF3,0xCE,0xFF,0x1F,0xF3,0x3E,0x26,0x01,0x39,
0x17,0x95,0x84,0x89,0xB0,0xF0,0x4C,0x4B,0x82,0x91,0x9F,0xC4,0x4B,0xAC,0x9D,0xA5,
0x74,0xAF,0x17,0x25,0xC9,0xCA,0x32,0xD3,0xBC,0x89,0x8A,0x84,0x89,0xCC,0x0D,0xAE,
0x7C,0xA2,0xDB,0x9C,0x6A,0x78,0x91,0xEE,0xEA,0x76,0x5D,0x4E,0x87,0x60,0xF5,0x69,
0x15,0x67,0xD4,0x02,0xCF,0xAF,0x48,0x36,0x07,0xEA,0xBF,0x6F,0x66,0x2D,0x06,0x8F,
0xC4,0x9A,0xFE,0xF9,0xF6,0x90,0x87,0x75,0xB8,0xF7,0xAD,0x0F,0x76,0x10,0x5A,0x3D,
0x59,0xB0,0x2E,0xB3,0xC7,0x35,0x2C,0xCC,0x70,0x56,0x2B,0xCB,0xE3,0x37,0x96,0xC5,
0x2F,0x46,0x1B,0x8A,0x22,0x46,0xC7,0x88,0xA7,0x26,0x32,0x98,0x61,0xDF,0x86,0x22,
0x8A,0xF4,0x1C,0x2F,0x87,0xA1,0x09,0xAA,0xCC,0xA9,0xAE,0xD3,0xBD,0x00,0x45,0x1C,
0x9A,0x54,0x87,0x86,0x52,0x87,0xEF,0xFF,0x1E,0x8F,0xA1,0x8F,0xC1,0x89,0x5C,0x35,
0x1B,0xDA,0x2D,0x3A,0x2C,0x16,0xB2,0xC2,0xF1,0x56,0xE2,0x78,0xC1,0x6B,0x63,0x97,
0xC5,0x56,0x8F,0xC9,0x32,0x7F,0x2C,0xAA,0xAF,0xA6,0xA8,0xAC,0x20,0x91,0x22,0x88,
0xDE,0xE4,0x60,0x8B,0xF9,0x4B,0x42,0x25,0x1A,0xE3,0x7F,0x9C,0x2C,0x19,0x89,0x3A,
0x7E,0x05,0xD4,0x36,0xCC,0x69,0x58,0xC2,0xC1,0x32,0x8B,0x2F,0x90,0x85,0xEB,0x7A,
0x39,0x50,0xA5,0xA1,0x27,0x92,0xC5,0x66,0xB0,0x20,0x4F,0x58,0x7E,0x55,0x83,0x43,
0x2B,0x45,0xE2,0x9C,0xE4,0xD8,0x12,0x90,0x2C,0x16,0x83,0x56,0x16,0x79,0x03,0xB3,
0xAD,0x2D,0x61,0x18,0x1A,0x13,0x1F,0x37,0xE2,0xE1,0x9C,0x73,0x7B,0x80,0xD5,0xFD,
0x2D,0x51,0x87,0xFC,0x7B,0xAA,0xD7,0x1F,0x2C,0x7A,0x8E,0xAF,0xF4,0x8D,0xBB,0xCD,
0x95,0x11,0x7C,0x72,0x0B,0xEE,0x6F,0xE2,0xB9,0xAF,0xDE,0x37,0x83,0xDE,0x8C,0x8D,
0x62,0x05,0x67,0xB7,0x96,0xC6,0x8D,0x56,0xB6,0x0D,0xD7,0x62,0xBA,0xD6,0x46,0x36,
0xBD,0x8E,0xC8,0xE6,0xEA,0x2A,0x6C,0x10,0x14,0xFF,0x6B,0x5B,0xFA,0x82,0x3C,0x46,
0xB1,0x30,0x43,0x46,0x51,0x8A,0x7D,0x9B,0x92,0x3E,0x83,0x79,0x5B,0x55,0x5D,0xB2,
0x6C,0x5E,0xCE,0x90,0x62,0x8E,0x53,0x98,0xC9,0x0D,0x6D,0xE5,0x2D,0x57,0xCD,0xC5,
0x81,0x57,0xBA,0xE1,0xE8,0xB8,0x8F,0x72,0xE5,0x4F,0x13,0xDC,0xEA,0x9D,0x71,0x15,
0x10,0xB2,0x11,0x88,0xD5,0x09,0xD4,0x7F,0x5B,0x65,0x7F,0x2C,0x3B,0x38,0x4C,0x11,
0x68,0x50,0x8D,0xFB,0x9E,0xB0,0x59,0xBF,0x94,0x80,0x89,0x4A,0xC5,0x1A,0x18,0x12,
0x89,0x53,0xD1,0x4A,0x10,0x29,0xE8,0x8C,0x1C,0xEC,0xB6,0xEA,0x46,0xC7,0x17,0x8B,
0x25,0x15,0x31,0xA8,0xA2,0x6B,0x43,0xB1,0x9D,0xE2,0xDB,0x0B,0x87,0x9B,0xB0,0x11,
0x04,0x0E,0x71,0xD2,0x29,0x77,0x89,0x82,0x0A,0x66,0x41,0x7F,0x1D,0x0B,0x48,0xFF,
0x72,0xBB,0x24,0xFD,0xC2,0x48,0xA1,0x9B,0xFE,0x7B,0x7F,0xCE,0x88,0xDB,0x86,0xD9,
0x85,0x3B,0x1C,0xB0,0xDC,0xA8,0x33,0x07,0xBF,0x51,0x2E,0xE3,0x0E,0x9A,0x00,0x97,
0x1E,0x06,0xC0,0x97,0x43,0x9D,0xD8,0xB6,0x45,0xC4,0x86,0x67,0x5F,0x00,0xF8,0x88,
0x9A,0xA4,0x52,0x9E,0xC7,0xAA,0x8A,0x83,0x75,0xEC,0xC5,0x18,0xAE,0xCE,0xC3,0x2F,
0x1A,0x2B,0xF9,0x18,0xFF,0xAE,0x1A,0xF5,0x53,0x0B,0xB5,0x33,0x51,0xA7,0xFD,0xE8,
0xA8,0xE1,0xA2,0x64,0xB6,0x22,0x17,0x43,0x80,0xCC,0x0A,0xD8,0xAE,0x3B,0xBA,0x40,
0xD7,0xD9,0x92,0x4A,0x89,0xDF,0x04,0x10,0xEE,0x9B,0x18,0x2B,0x6A,0x77,0x69,0x8A,
0x68,0xF4,0xF9,0xB9,0xA2,0x21,0x15,0x6E,0xE6,0x1E,0x3B,0x03,0x62,0x30,0x9B,0x60,
0x41,0x7E,0x25,0x9B,0x9E,0x8F,0xC5,0x52,0x10,0x08,0xF8,0xC2,0x69,0xA1,0x21,0x11,
0x88,0x37,0x5E,0x79,0x35,0x66,0xFF,0x10,0x42,0x18,0x6E,0xED,0x97,0xB6,0x6B,0x1C,
0x4E,0x36,0xE5,0x6D,0x7D,0xB4,0xE4,0xBF,0x20,0xB9,0xE0,0x05,0x3A,0x69,0xD5,0xB8,
0xE3,0xD5,0xDC,0xE0,0xB9,0xAC,0x53,0x3E,0x07,0xA4,0x57,0xAD,0x77,0xFF,0x48,0x18,
0x76,0x2A,0xAC,0x49,0x2A,0x8E,0x47,0x75,0x6D,0x9F,0x67,0x63,0x30,0x35,0x8C,0x39,
0x05,0x39,0xD5,0x6F,0x64,0x3A,0x5B,0xAD,0xCA,0x0B,0xBB,0x82,0x52,0x99,0x45,0xB1,
0x93,0x36,0x36,0x99,0xAF,0x13,0x20,0x44,0x36,0xD8,0x02,0x44,0x09,0x39,0x92,0x85,
0xFF,0x4A,0x4A,0x97,0x87,0xA6,0x63,0xD7,0xC7,0xB5,0xB5,0x24,0xED,0x0F,0xB4,0x6F,
0x0C,0x58,0x52,0x14,0xD9,0xA6,0x7B,0xD3,0x79,0xBC,0x38,0x58,0xA1,0xBD,0x3B,0x84,
0x06,0xD8,0x1A,0x06,0xFD,0x6B,0xA8,0xEA,0x4B,0x69,0x28,0x04,0x37,0xAD,0x82,0x99,
0xFB,0x0E,0x1B,0x85,0xBD,0xA8,0x5D,0x73,0xCD,0xDC,0x58,0x75,0x0A,0xBE,0x63,0x6C,
0x48,0xE7,0x4C,0xE4,0x30,0x2B,0x04,0x60,0xB9,0x15,0xD8,0xDA,0x86,0x81,0x75,0x8F,
0x96,0xD4,0x8D,0x1C,0x5D,0x70,0x85,0x7C,0x1C,0x67,0x7B,0xD5,0x08,0x67,0xA6,0xCE,
0x4B,0x0A,0x66,0x70,0xB7,0xE5,0x63,0xD4,0x5B,0x8A,0x82,0xEA,0x10,0x67,0xCA,0xE2,
0xF4,0xEF,0x17,0x85,0x2F,0x2A,0x5F,0x8A,0x97,0x82,0xF8,0x6A,0xD6,0x34,0x10,0xEA,
0xEB,0xC9,0x5C,0x3C,0xE1,0x49,0xF8,0x46,0xEB,0xDE,0xBD,0xF6,0xA9,0x92,0xF1,0xAA,
0xA6,0xA0,0x18,0xB0,0x3A,0xD3,0x0F,0x1F,0xF3,0x6F,0xFF,0x31,0x45,0x43,0x44,0xD3,
0x50,0x9A,0xF7,0x88,0x09,0x96,0xC1,0xCE,0x76,0xCC,0xF2,0x2C,0x2C,0xBA,0xAD,0x82,
0x77,0x8F,0x18,0x84,0xC0,0xD2,0x07,0x9C,0x36,0x90,0x83,0x4E,0x0B,0xA5,0x4F,0x43,
0x3E,0x04,0xAB,0x78,0x4F,0xD6,0xFB,0x09,0x01,0x24,0x90,0xDA,0x6F,0x3C,0x3A,0x61,
0x0D,0x7F,0x69,0x4A,0xEB,0x2B,0x30,0x02,0xB4,0xDB,0xE0,0x84,0xA9,0xEC,0xD7,0x35,
0xBF,0x37,0x7D,0x85,0x58,0xCE,0xA9,0x4E,0xE4,0x80,0xC7,0xA8,0xD3,0x30,0x67,0x48,
0xEB,0x29,0xAF,0x2F,0x74,0x6A,0xB4,0xA7,0x3F,0x0F,0x3F,0x92,0xAF,0xF3,0xCA,0xAC,
0xAF,0x4B,0xD9,0x94,0xC0,0x43,0xCA,0x81,0x0D,0x2F,0x48,0xA1,0xB0,0x27,0xD5,0xD2,
0xEF,0x4B,0x05,0x85,0xA3,0xDE,0x4D,0x93,0x30,0x3C,0xF0,0xBB,0x4A,0x8F,0x30,0x27,
0x4C,0xEB,0xE3,0x3E,0x64,0xED,0x9A,0x2F,0x3B,0xF1,0x82,0xF0,0xBA,0xF4,0xCF,0x7F,
0x40,0xCB,0xB0,0xE1,0x7F,0xBC,0xAA,0x57,0xD3,0xC9,0x74,0xF2,0xFA,0x43,0x0D,0x22,
0xD0,0xF4,0x77,0x4E,0x93,0xD7,0x85,0x70,0x1F,0x99,0xBF,0xB6,0xDE,0x35,0xF1,0x30,
0xA7,0x5E,0x71,0xF0,0x6B,0x01,0x2D,0x7B,0x64,0xF0,0x33,0x53,0x0A,0x39,0x88,0xF3,
0x6B,0x3A,0xA6,0x6B,0x35,0xD2,0x2F,0x43,0xCD,0x02,0xFD,0xB5,0xE9,0xBC,0x5B,0xAA,
0xD8,0xA4,0x19,0x7E,0x0E,0x5D,0x94,0x81,0x9E,0x6F,0x77,0xAD,0xD6,0x0E,0x74,0x93,
0x96,0xE7,0xC4,0x18,0x5F,0xAD,0xF5,0x19,
];
#endregion
#region 3DS-Specific Fields
/// <summary>
/// AES Hardware Constant
/// </summary>
public byte[] AESHardwareConstant { get; private set; } = [];
/// <summary>
/// KeyX 0x18 (New 3DS 9.3)
/// </summary>
public byte[] KeyX0x18 { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x18 (New 3DS 9.3)
/// </summary>
public byte[] DevKeyX0x18 { get; private set; } = [];
/// <summary>
/// KeyX 0x1B (New 3DS 9.6)
/// </summary>
public byte[] KeyX0x1B { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x1B New 3DS 9.6)
/// </summary>
public byte[] DevKeyX0x1B { get; private set; } = [];
/// <summary>
/// KeyX 0x25 (> 7.x)
/// </summary>
public byte[] KeyX0x25 { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x25 (> 7.x)
/// </summary>
public byte[] DevKeyX0x25 { get; private set; } = [];
/// <summary>
/// KeyX 0x2C (< 6.x)
/// </summary>
public byte[] KeyX0x2C { get; private set; } = [];
/// <summary>
/// Dev KeyX 0x2C (< 6.x)
/// </summary>
public byte[] DevKeyX0x2C { get; private set; } = [];
#endregion
#region Internal Test Values
/// <summary>
/// Expected hash for NitroEncryptionData
/// </summary>
private static readonly byte[] ExpectedNitroSha512Hash =
[
0x1A, 0xD6, 0x40, 0x21, 0xFC, 0x3D, 0x1A, 0x9A,
0x9B, 0xC0, 0x88, 0x8E, 0x2E, 0x68, 0xDE, 0x4E,
0x8A, 0x60, 0x6B, 0x86, 0x63, 0x22, 0xD2, 0xC7,
0xC6, 0xD7, 0xD6, 0xCE, 0x65, 0xF5, 0xBA, 0xA7,
0xEA, 0x69, 0x63, 0x7E, 0xC9, 0xE4, 0x57, 0x7B,
0x01, 0xFD, 0xCE, 0xC2, 0x26, 0x3B, 0xD9, 0x0D,
0x84, 0x57, 0xC2, 0x00, 0xB8, 0x56, 0x9F, 0xE5,
0x56, 0xDA, 0x8D, 0xDE, 0x84, 0xB8, 0x8E, 0xE4,
];
/// <summary>
/// Initial value for key validation tests
/// </summary>
private static readonly byte[] TestIV =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
];
/// <summary>
/// Pattern to use for key validation tests
/// </summary>
private static readonly byte[] TestPattern =
[
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
];
/// <summary>
/// Expected output value for KeyX0x18
/// </summary>
private static readonly byte[] ExpectedKeyX0x18 =
[
0x06, 0xF1, 0xB2, 0x3B, 0x12, 0xAD, 0x80, 0xC1,
0x13, 0xC6, 0x18, 0x3D, 0x27, 0xB8, 0xB9, 0x95,
0x49, 0x73, 0x59, 0x82, 0xEF, 0xFE, 0x16, 0x48,
0x91, 0x2A, 0x89, 0x55, 0x9A, 0xDC, 0x3C, 0xA0,
0x84, 0x46, 0x14, 0xE0, 0x16, 0x59, 0x8E, 0x4F,
0xC2, 0x6C, 0x52, 0xA4, 0x7D, 0xAD, 0x4F, 0x23,
0xF1, 0xC6, 0x99, 0x44, 0x39, 0xB7, 0x42, 0xF0,
0x1F, 0xBB, 0x02, 0xF6, 0x0A, 0x8A, 0xC2, 0x9A,
];
/// <summary>
/// Expected output value for DevKeyX0x18
/// </summary>
private static readonly byte[] ExpectedDevKeyX0x18 =
[
0x99, 0x6E, 0x3C, 0x54, 0x97, 0x3C, 0xEA, 0xE8,
0xBA, 0xAE, 0x18, 0x5C, 0x93, 0x27, 0x65, 0x50,
0xF6, 0x6D, 0x67, 0xD7, 0xEF, 0xBD, 0x7C, 0xCB,
0x8A, 0xC1, 0x1A, 0x54, 0xFC, 0x3B, 0x8B, 0x3A,
0x0E, 0xE5, 0xEF, 0x27, 0x4A, 0x73, 0x7E, 0x0A,
0x2E, 0x2E, 0x9D, 0xAF, 0x6C, 0x03, 0xF2, 0x91,
0xC4, 0xFA, 0x73, 0xFD, 0x6B, 0xA0, 0x07, 0xD4,
0x75, 0x5B, 0x6F, 0x2E, 0x8B, 0x68, 0x4C, 0xD1,
];
/// <summary>
/// Expected output value for KeyX0x1B
/// </summary>
private static readonly byte[] ExpectedKeyX0x1B =
[
0x0A, 0xE4, 0x79, 0x02, 0x1B, 0xFA, 0x25, 0x4B,
0x2D, 0x92, 0x4F, 0xA8, 0x41, 0x59, 0xCE, 0x10,
0x09, 0xE6, 0x08, 0x61, 0x23, 0xC7, 0xD2, 0x30,
0x84, 0x37, 0xD5, 0x49, 0x42, 0x94, 0xB2, 0x70,
0x6A, 0xF3, 0x75, 0xB0, 0x1F, 0x4F, 0xA1, 0xCE,
0x03, 0xA2, 0x6A, 0x19, 0x5D, 0x32, 0x0D, 0xB5,
0x79, 0xCD, 0xFD, 0xF0, 0xDE, 0x49, 0x26, 0x2D,
0x29, 0x36, 0x30, 0x69, 0x8B, 0x45, 0xE1, 0xFC,
];
/// <summary>
/// Expected output value for DevKeyX0x1B
/// </summary>
private static readonly byte[] ExpectedDevKeyX0x1B =
[
0x16, 0x4F, 0xD9, 0x58, 0xC9, 0x20, 0xB3, 0xED,
0xC4, 0xEB, 0x57, 0x39, 0x10, 0xEF, 0xA8, 0xCC,
0xE5, 0x49, 0xBF, 0x52, 0x10, 0xA9, 0xCC, 0xE1,
0x65, 0x3B, 0x2D, 0x51, 0x45, 0xFB, 0x60, 0x52,
0x3E, 0x29, 0xEB, 0xEB, 0x3F, 0xF2, 0x76, 0x08,
0x00, 0x05, 0x7F, 0x64, 0x29, 0x4A, 0x17, 0x22,
0x56, 0x7F, 0x49, 0x94, 0x1A, 0x8C, 0x56, 0x35,
0x38, 0xBE, 0xA4, 0x2E, 0x58, 0xD3, 0x81, 0x8C,
];
/// <summary>
/// Expected output value for KeyX0x25
/// </summary>
private static readonly byte[] ExpectedKeyX0x25 =
[
0x37, 0xBC, 0x73, 0xD6, 0xEE, 0x73, 0xE0, 0x94,
0x42, 0x84, 0x74, 0xE5, 0xD8, 0xFB, 0x5F, 0x65,
0xF4, 0xCF, 0x2E, 0xC1, 0x43, 0x48, 0x6C, 0xAA,
0xC8, 0xF9, 0x96, 0xE6, 0x33, 0xDD, 0xE7, 0xBF,
0xD2, 0x21, 0x89, 0x39, 0x13, 0xD1, 0xEC, 0xCA,
0x1D, 0x5D, 0x1F, 0x77, 0x95, 0xD2, 0x8B, 0x27,
0x92, 0x79, 0xC5, 0x1D, 0x72, 0xA7, 0x28, 0x57,
0x41, 0x0E, 0x46, 0xB8, 0x80, 0x7B, 0x7C, 0x0D,
];
/// <summary>
/// Expected output value for DevKeyX0x25
/// </summary>
private static readonly byte[] ExpectedDevKeyX0x25 =
[
0x71, 0x65, 0x30, 0xF2, 0x68, 0xEC, 0x65, 0x0A,
0x8C, 0x9E, 0xC5, 0x5A, 0xFA, 0x37, 0x8E, 0xDA,
0x7B, 0x58, 0x3B, 0x66, 0x7C, 0x9D, 0x16, 0xD9,
0x2D, 0x8F, 0xCF, 0x04, 0x66, 0x7F, 0x27, 0x41,
0xBF, 0x5F, 0x1E, 0x11, 0x4C, 0xD6, 0xB9, 0x0A,
0xC5, 0x42, 0xCF, 0x2B, 0x87, 0x6B, 0xD4, 0x72,
0x4D, 0x9C, 0x29, 0x2E, 0xF8, 0xB0, 0x6F, 0x22,
0x35, 0x5B, 0x96, 0x83, 0xD1, 0xE4, 0x5E, 0xDB,
];
/// <summary>
/// Expected output value for KeyX0x2C
/// </summary>
private static readonly byte[] ExpectedKeyX0x2C =
[
0xAE, 0x44, 0x20, 0xDB, 0xA5, 0x96, 0xDC, 0xF3,
0xD8, 0x23, 0x9E, 0x3C, 0x44, 0x73, 0x3D, 0xCD,
0x07, 0xD5, 0xF8, 0xD0, 0xC6, 0xB3, 0x5A, 0x80,
0xB5, 0x5A, 0x55, 0x30, 0x5D, 0x4A, 0xBE, 0x61,
0xBF, 0xEF, 0x64, 0x17, 0x28, 0xD6, 0x26, 0x52,
0x42, 0x4D, 0x8F, 0x1C, 0xBC, 0x63, 0xD3, 0x91,
0x7D, 0xA6, 0x4F, 0xAF, 0x26, 0x38, 0x60, 0xEE,
0x79, 0x92, 0x2F, 0xD8, 0xCA, 0x4E, 0xE7, 0xEC,
];
/// <summary>
/// Expected output value for DevKeyX0x2C
/// </summary>
private static readonly byte[] ExpectedDevKeyX0x2C =
[
0x5F, 0x73, 0xD5, 0x9A, 0x67, 0xFF, 0x8C, 0x12,
0x31, 0x58, 0x0B, 0x58, 0x46, 0xFE, 0x05, 0x16,
0x92, 0xE4, 0x84, 0x06, 0x18, 0x9B, 0x58, 0x91,
0xE7, 0xF8, 0xCD, 0xA9, 0x95, 0xAC, 0x07, 0xCD,
0x43, 0x20, 0x7A, 0x8C, 0xCC, 0xAB, 0x48, 0x50,
0x29, 0x2F, 0x96, 0x73, 0xB0, 0xD9, 0xE5, 0xCB,
0xE6, 0x9A, 0x0D, 0xF7, 0xD0, 0x1E, 0xC2, 0xEC,
0xC1, 0xE2, 0x8E, 0xEE, 0x89, 0xB9, 0xB1, 0x97,
];
#endregion
/// <summary>
/// Setup all of the necessary constants
/// </summary>
/// <param name="keyfile">Path to the keyfile</param>
public DecryptArgs(string? config)
{
InitConfigJson(config);
ValidateKeys();
}
/// <summary>
/// Setup all of the necessary constants
/// </summary>
/// <param name="keyfile">Path to the keyfile</param>
/// <param name="useAesKeysTxt">Indicates if the keyfile format is aeskeys.txt</param>
public DecryptArgs(string? keyfile, bool useAesKeysTxt)
{
// Set the default DS values
NitroEncryptionData = _nitroEncryptionData;
// Read the proper keyfile format
if (useAesKeysTxt)
InitAesKeysTxt(keyfile);
else
InitKeysBin(keyfile);
// Perform validation tests
ValidateKeys();
}
/// <summary>
/// Setup all of the necessary constants from aes_keys.txt
/// </summary>
/// <param name="keyfile">Path to aes_keys.txt</param>
private void InitAesKeysTxt(string? keyfile)
{
if (keyfile == null || !File.Exists(keyfile))
{
IsReady = false;
return;
}
try
{
using var reader = new IniReader(keyfile);
while (reader.ReadNextLine())
{
// Ignore comments in the file
if (reader.RowType == IniRowType.Comment)
continue;
if (reader.KeyValuePair == null || string.IsNullOrEmpty(reader.KeyValuePair?.Key))
break;
var kvp = reader.KeyValuePair!.Value;
byte[] value = StringToByteArray(kvp.Value);
switch (kvp.Key)
{
// Hardware constant
case "generator":
AESHardwareConstant = value;
break;
// Retail Keys
case "slot0x18KeyX":
KeyX0x18 = value;
break;
case "slot0x1BKeyX":
KeyX0x1B = value;
break;
case "slot0x25KeyX":
KeyX0x25 = value;
break;
case "slot0x2CKeyX":
KeyX0x2C = value;
break;
// Currently Unused KeyX
case "slot0x03KeyX":
case "slot0x19KeyX":
case "slot0x1AKeyX":
case "slot0x1CKeyX":
case "slot0x1DKeyX":
case "slot0x1EKeyX":
case "slot0x1FKeyX":
case "slot0x2DKeyX":
case "slot0x2EKeyX":
case "slot0x2FKeyX":
case "slot0x30KeyX":
case "slot0x31KeyX":
case "slot0x32KeyX":
case "slot0x33KeyX":
case "slot0x34KeyX":
case "slot0x35KeyX":
case "slot0x36KeyX":
case "slot0x37KeyX":
case "slot0x38KeyX":
case "slot0x3AKeyX":
case "slot0x3BKeyX":
break;
// Currently Unused KeyY
case "slot0x03KeyY":
case "slot0x06KeyY":
case "slot0x07KeyY":
case "slot0x2EKeyY":
case "slot0x2FKeyY":
case "slot0x31KeyY":
break;
// Currently Unused KeyN
case "slot0x0DKeyN":
case "slot0x15KeyN":
case "slot0x16KeyN":
case "slot0x19KeyN":
case "slot0x1AKeyN":
case "slot0x1BKeyN":
case "slot0x1CKeyN":
case "slot0x1DKeyN":
case "slot0x1EKeyN":
case "slot0x1FKeyN":
case "slot0x24KeyN":
case "slot0x2DKeyN":
case "slot0x2EKeyN":
case "slot0x2FKeyN":
case "slot0x31KeyN":
case "slot0x32KeyN":
case "slot0x36KeyN":
case "slot0x37KeyN":
case "slot0x38KeyN":
case "slot0x3BKeyN":
break;
}
}
}
catch
{
IsReady = false;
return;
}
IsReady = true;
}
/// <summary>
/// Setup all of the necessary constants from config.json
/// </summary>
/// <param name="config">Path to config.json</param>
private void InitConfigJson(string? config)
{
if (config == null || !File.Exists(config))
{
IsReady = false;
return;
}
// Try to read the configuration file
var configObj = Configuration.Create(config);
if (configObj == null)
{
IsReady = false;
return;
}
// Set the fields from the configuration
NitroEncryptionData = StringToByteArray(configObj.NitroEncryptionData);
AESHardwareConstant = StringToByteArray(configObj.AESHardwareConstant);
KeyX0x18 = StringToByteArray(configObj.KeyX0x18);
KeyX0x1B = StringToByteArray(configObj.KeyX0x1B);
KeyX0x25 = StringToByteArray(configObj.KeyX0x25);
KeyX0x2C = StringToByteArray(configObj.KeyX0x2C);
DevKeyX0x18 = StringToByteArray(configObj.DevKeyX0x18);
DevKeyX0x1B = StringToByteArray(configObj.DevKeyX0x1B);
DevKeyX0x25 = StringToByteArray(configObj.DevKeyX0x25);
DevKeyX0x2C = StringToByteArray(configObj.DevKeyX0x2C);
IsReady = true;
}
/// <summary>
/// Setup all of the necessary constants from keys.bin
/// </summary>
/// <param name="keyfile">Path to keys.bin</param>
/// <remarks>keys.bin should be in little endian format</remarks>
private void InitKeysBin(string? keyfile)
{
if (keyfile == null || !File.Exists(keyfile))
{
IsReady = false;
return;
}
try
{
using Stream reader = File.Open(keyfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Hardware constant
AESHardwareConstant = reader.ReadBytes(16);
Array.Reverse(AESHardwareConstant);
// Retail keys
KeyX0x18 = reader.ReadBytes(16);
Array.Reverse(KeyX0x18);
KeyX0x1B = reader.ReadBytes(16);
Array.Reverse(KeyX0x1B);
KeyX0x25 = reader.ReadBytes(16);
Array.Reverse(KeyX0x25);
KeyX0x2C = reader.ReadBytes(16);
Array.Reverse(KeyX0x2C);
// Development keys
DevKeyX0x18 = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x18);
DevKeyX0x1B = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x1B);
DevKeyX0x25 = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x25);
DevKeyX0x2C = reader.ReadBytes(16);
Array.Reverse(DevKeyX0x2C);
}
catch
{
IsReady = false;
return;
}
IsReady = true;
}
/// <summary>
/// Validate that all keys provided are going to be valid
/// </summary>
/// <remarks>Does not know what the keys are, just the result</remarks>
private void ValidateKeys()
{
// NitroEncryptionData
if (NitroEncryptionData.Length > 0)
{
using var hasher = System.Security.Cryptography.SHA512.Create();
byte[] actual = hasher.ComputeHash(NitroEncryptionData);
if (!Extensions.EqualsExactly(ExpectedNitroSha512Hash, actual))
{
Console.WriteLine($"NitroEncryptionData invalid value, disabling...");
NitroEncryptionData = [];
}
}
// KeyX0x18
if (KeyX0x18.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x18, actual))
{
Console.WriteLine($"KeyX0x18 invalid value, disabling...");
KeyX0x18 = [];
}
}
// DevKeyX0x18
if (DevKeyX0x18.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x18, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x18, actual))
{
Console.WriteLine($"DevKeyX0x18 invalid value, disabling...");
DevKeyX0x18 = [];
}
}
// KeyX0x1B
if (KeyX0x1B.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x1B, actual))
{
Console.WriteLine($"KeyX0x1B invalid value, disabling...");
KeyX0x1B = [];
}
}
// DevKeyX0x1B
if (DevKeyX0x1B.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x1B, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x1B, actual))
{
Console.WriteLine($"DevKeyX0x1B invalid value, disabling...");
DevKeyX0x1B = [];
}
}
// KeyX0x25
if (KeyX0x25.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x25, actual))
{
Console.WriteLine($"KeyX0x25 invalid value, disabling...");
KeyX0x25 = [];
}
}
// DevKeyX0x25
if (DevKeyX0x25.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x25, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x25, actual))
{
Console.WriteLine($"DevKeyX0x25 invalid value, disabling...");
DevKeyX0x25 = [];
}
}
// KeyX0x2C
if (KeyX0x2C.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(KeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedKeyX0x2C, actual))
{
Console.WriteLine($"KeyX0x2C invalid value, disabling...");
KeyX0x2C = [];
}
}
// DevKeyX0x2C
if (DevKeyX0x2C.Length > 0)
{
var cipher = CommonOperations.CreateAESEncryptionCipher(DevKeyX0x2C, TestIV);
byte[] actual = cipher.ProcessBytes(TestPattern);
if (!Extensions.EqualsExactly(ExpectedDevKeyX0x2C, actual))
{
Console.WriteLine($"DevKeyX0x2C invalid value, disabling...");
DevKeyX0x2C = [];
}
}
}
// https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa
private static byte[] StringToByteArray(string? hex)
{
// Handle null values
if (hex == null)
return [];
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
}
}

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

@@ -0,0 +1,28 @@
namespace NDecrypt.Core
{
public interface ITool
{
/// <summary>
/// Attempts to encrypt an input file
/// </summary>
/// <param name="filename">Name of the file to encrypt</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <returns>True if the file could be encrypted, false otherwise</returns>
bool EncryptFile(string filename, bool force);
/// <summary>
/// Attempts to decrypt an input file
/// </summary>
/// <param name="filename">Name of the file to decrypt</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <returns>True if the file could be decrypted, false otherwise</returns>
bool DecryptFile(string filename, bool force);
/// <summary>
/// Attempts to get information on an input file
/// </summary>
/// <param name="filename">Name of the file get information on</param>
/// <returns>String representing the info, null on error</returns>
string? GetInformation(string filename);
}
}

View File

@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<IncludeSymbols>true</IncludeSymbols>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.3.2</VersionPrefix>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Common code for all NDecrypt processors</Description>
<Copyright>Copyright (c) Matt Nadareski 2019-2025</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<RepositoryUrl>https://github.com/SabreTools/NDecrypt</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<!-- Support All Frameworks -->
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net4`))">
<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`netcoreapp`)) OR $(TargetFramework.StartsWith(`net5`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net6`)) OR $(TargetFramework.StartsWith(`net7`)) OR $(TargetFramework.StartsWith(`net8`)) OR $(TargetFramework.StartsWith(`net9`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(RuntimeIdentifier.StartsWith(`osx-arm`))">
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.NetCore" Version="1.9.0" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="SabreTools.Hashing" Version="1.4.2" />
<PackageReference Include="SabreTools.IO" Version="1.6.2" />
<PackageReference Include="SabreTools.Models" Version="1.5.8" />
<PackageReference Include="SabreTools.Serialization" Version="1.8.6" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,139 @@
using System;
using Org.BouncyCastle.Crypto;
using SabreTools.Models.N3DS;
using static NDecrypt.Core.CommonOperations;
namespace NDecrypt.Core
{
/// <summary>
/// Set of all keys associated with a partition
/// </summary>
public class PartitionKeys
{
public byte[] KeyX { get; private set; }
public byte[] KeyX2C { get; }
public byte[] KeyY { get; }
public byte[] NormalKey { get; private set; }
public byte[] NormalKey2C { get; }
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs _decryptArgs;
/// <summary>
/// Indicates if development images are expected
/// </summary>
private readonly bool _development;
/// <summary>
/// Create a new set of keys for a given partition
/// </summary>
/// <param name="args">Decryption args representing available keys</param>
/// <param name="signature">RSA-2048 signature from the partition</param>
/// <param name="masks">BitMasks from the partition or backup header</param>
/// <param name="method">CryptoMethod from the partition or backup header</param>
/// <param name="development">Determine if development keys are used</param>
public PartitionKeys(DecryptArgs args, byte[]? signature, BitMasks masks, CryptoMethod method, bool development)
{
// Validate inputs
if (args.IsReady != true)
throw new InvalidOperationException($"{nameof(args)} must be initialized before use");
if (signature != null && signature.Length < 16)
throw new DataLengthException($"{nameof(signature)} must be at least 16 bytes");
// Set fields for future use
_decryptArgs = args;
_development = development;
// Set the standard KeyX values
KeyX = new byte[16];
KeyX2C = development ? args.DevKeyX0x2C : args.KeyX0x2C;
// Backup headers can't have a KeyY value set
KeyY = new byte[16];
if (signature != null)
Array.Copy(signature, KeyY, 16);
// Set the standard normal key values
NormalKey = new byte[16];
NormalKey2C = RotateLeft(KeyX2C, 2);
NormalKey2C = Xor(NormalKey2C, KeyY);
NormalKey2C = Add(NormalKey2C, args.AESHardwareConstant);
NormalKey2C = RotateLeft(NormalKey2C, 87);
// Special case for zero-key
#if NET20 || NET35
if ((masks & BitMasks.FixedCryptoKey) > 0)
#else
if (masks.HasFlag(BitMasks.FixedCryptoKey))
#endif
{
Console.WriteLine("Encryption Method: Zero Key");
NormalKey = new byte[16];
NormalKey2C = new byte[16];
return;
}
// Set KeyX values based on crypto method
switch (method)
{
case CryptoMethod.Original:
Console.WriteLine("Encryption Method: Key 0x2C");
KeyX = development ? args.DevKeyX0x2C : args.KeyX0x2C;
break;
case CryptoMethod.Seven:
Console.WriteLine("Encryption Method: Key 0x25");
KeyX = development ? args.DevKeyX0x25 : args.KeyX0x25;
break;
case CryptoMethod.NineThree:
Console.WriteLine("Encryption Method: Key 0x18");
KeyX = development ? args.DevKeyX0x18 : args.KeyX0x18;
break;
case CryptoMethod.NineSix:
Console.WriteLine("Encryption Method: Key 0x1B");
KeyX = development ? args.DevKeyX0x1B : args.KeyX0x1B;
break;
}
// Set the normal key based on the new KeyX value
NormalKey = RotateLeft(KeyX, 2);
NormalKey = Xor(NormalKey, KeyY);
NormalKey = Add(NormalKey, args.AESHardwareConstant);
NormalKey = RotateLeft(NormalKey, 87);
}
/// <summary>
/// Set RomFS values based on the bit masks
/// </summary>
public void SetRomFSValues(BitMasks masks)
{
// NormalKey has a constant value for zero-key
#if NET20 || NET35
if ((masks & BitMasks.FixedCryptoKey) > 0)
#else
if (masks.HasFlag(BitMasks.FixedCryptoKey))
#endif
{
NormalKey = new byte[16];
return;
}
// Encrypting RomFS for partitions 1 and up always use Key0x2C
KeyX = _development ? _decryptArgs.DevKeyX0x2C : _decryptArgs.KeyX0x2C;
NormalKey = RotateLeft(KeyX, 2);
NormalKey = Xor(NormalKey, KeyY);
NormalKey = Add(NormalKey, _decryptArgs.AESHardwareConstant);
NormalKey = RotateLeft(NormalKey, 87);
}
}
}

View File

@@ -0,0 +1,917 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.N3DS;
using SabreTools.Serialization.Wrappers;
using static NDecrypt.Core.CommonOperations;
using static SabreTools.Models.N3DS.Constants;
namespace NDecrypt.Core
{
public class ThreeDSTool : ITool
{
/// <summary>
/// Decryption args to use while processing
/// </summary>
private readonly DecryptArgs _decryptArgs;
/// <summary>
/// Indicates if development images are expected
/// </summary>
private readonly bool _development;
/// <summary>
/// Set of all partition keys
/// </summary>
private readonly PartitionKeys[] KeysMap = new PartitionKeys[8];
public ThreeDSTool(bool development, DecryptArgs decryptArgs)
{
_development = development;
_decryptArgs = decryptArgs;
}
#region Decrypt
/// <inheritdoc/>
public bool DecryptFile(string filename, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
{
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
try
{
// Open the read and write on the same file for inplace processing
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var output = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DS.Create(input);
if (cart?.Model == null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
}
// Decrypt all 8 NCCH partitions
DecryptAllPartitions(cart, force, input, output);
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid 3DS or New 3DS cart image and try again.");
return false;
}
}
/// <summary>
/// Decrypt all partitions in the partition table of an NCSD header
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptAllPartitions(N3DS cart, bool force, Stream input, Stream output)
{
// Check the partitions table
if (cart.PartitionsTable == null || cart.Partitions == null)
{
Console.WriteLine("Invalid partitions table!");
return;
}
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
var partition = cart.Partitions[p];
if (partition == null || partition.MagicID != NCCHMagicNumber)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Check the partition has data
var partitionEntry = cart.PartitionsTable[p];
if (partitionEntry == null || partitionEntry.Length == 0)
{
Console.WriteLine($"Partition {p} No data... Skipping...");
continue;
}
// Decrypt the partition, if possible
if (ShouldDecryptPartition(cart, p, force))
DecryptPartition(cart, p, input, output);
}
}
/// <summary>
/// Determine if the current partition should be decrypted
/// </summary>s
private static bool ShouldDecryptPartition(N3DS cart, int index, bool force)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine($"Partition {index} is not verified due to force flag being set.");
return true;
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (cart.PossiblyDecrypted(index))
{
Console.WriteLine($"Partition {index}: Already Decrypted?...");
return false;
}
// By default, it passes
return true;
}
/// <summary>
/// Decrypt a single partition
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptPartition(N3DS cart, int index, Stream input, Stream output)
{
// Determine the keys needed for this partition
SetDecryptionKeys(cart, index);
// Decrypt the parts of the partition
DecryptExtendedHeader(cart, index, input, output);
DecryptExeFS(cart, index, input, output);
DecryptRomFS(cart, index, input, output);
// Update the flags
UpdateDecryptCryptoAndMasks(cart, index, output);
}
/// <summary>
/// Determine the set of keys to be used for decryption
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
private void SetDecryptionKeys(N3DS cart, int index)
{
// Get the partition
var partition = cart.Partitions?[index];
if (partition?.Flags == null)
return;
// Get partition-specific values
byte[]? rsaSignature = partition.RSA2048Signature;
BitMasks masks = cart.GetBitMasks(index);
CryptoMethod method = cart.GetCryptoMethod(index);
// Get the partition keys
KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
}
/// <summary>
/// Decrypt the extended header, if it exists
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExtendedHeader(N3DS cart, int index, Stream input, Stream output)
{
// Get required offsets
uint partitionOffset = cart.GetPartitionOffset(index);
if (partitionOffset == 0 || partitionOffset > input.Length)
{
Console.WriteLine($"Partition {index} No Data... Skipping...");
return false;
}
uint extHeaderSize = cart.GetExtendedHeaderSize(index);
if (extHeaderSize == 0)
{
Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
return false;
}
// Seek to the extended header
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index}: Decrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, cart.PlainIV(index));
// Process the extended header
PerformAESOperation(Constants.CXTExtendedDataHeaderLength, cipher, input, output, null);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
input.Seek(0, SeekOrigin.Begin);
#endif
output.Flush();
return true;
}
/// <summary>
/// Decrypt the ExeFS, if it exists
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptExeFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the ExeFS
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
uint exeFsSize = cart.GetExeFSSize(index);
if (exeFsSize == 0)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
// Decrypt the filename table
DecryptExeFSFilenameTable(cart, index, input, output);
// For all but the original crypto method, process each of the files in the table
if (cart.GetCryptoMethod(index) != CryptoMethod.Original)
DecryptExeFSFileEntries(cart, index, input, output);
// Get the ExeFS files offset
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Seek to the ExeFS
input.Seek(exeFsFilesOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = Add(cart.ExeFSIV(index), ctroffsetE);
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the decryption
exeFsSize -= cart.MediaUnitSize;
PerformAESOperation(exeFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {s}"));
return true;
}
/// <summary>
/// Decrypt the ExeFS Filename Table
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFilenameTable(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsOffset = cart.GetExeFSOffset(index);
if (exeFsOffset == 0 || exeFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Seek to the ExeFS header
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Decrypting - ExeFS Filename Table");
// Create the ExeFS AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
byte[] readBytes = input.ReadBytes((int)cart.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.Write(processedBytes);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
input.Seek(0, SeekOrigin.Begin);
#endif
output.Flush();
}
/// <summary>
/// Decrypt the ExeFS file entries
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void DecryptExeFSFileEntries(N3DS cart, int index, Stream input, Stream output)
{
if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Reread the decrypted ExeFS header
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
input.Seek(exeFsHeaderOffset, SeekOrigin.Begin);
cart.ExeFSHeaders[index] = SabreTools.Serialization.Deserializers.N3DS.ParseExeFSHeader(input);
// Get the ExeFS header
var exeFsHeader = cart.ExeFSHeaders[index];
if (exeFsHeader?.FileHeaders == null)
{
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
}
// Get the ExeFS files offset
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Loop through and process all headers
for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
{
// Only attempt to process code binary files
if (!cart.IsCodeBinary(index, i))
continue;
// Get the file header
var fileHeader = exeFsHeader.FileHeaders[i];
if (fileHeader == null)
continue;
// Create the ExeFS AES ciphers for this partition
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize) / 0x10;
byte[] exefsIVWithOffsetForHeader = Add(cart.ExeFSIV(index), ctroffset);
var firstCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
// Seek to the file entry
input.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
// Setup and perform the encryption
PerformAESOperation(fileHeader.FileSize,
firstCipher,
secondCipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Decrypting - {fileHeader.FileName}...{s}"));
}
}
/// <summary>
/// Decrypt the RomFS, if it exists
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool DecryptRomFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the RomFS
uint romFsOffset = cart.GetRomFSOffset(index);
if (romFsOffset == 0 || romFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
uint romFsSize = cart.GetRomFSSize(index);
if (romFsSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
// Seek to the RomFS
input.Seek(romFsOffset, SeekOrigin.Begin);
output.Seek(romFsOffset, SeekOrigin.Begin);
// Create the RomFS AES cipher for this partition
var cipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey, cart.RomFSIV(index));
// Setup and perform the decryption
PerformAESOperation(romFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Decrypting - {s}"));
return true;
}
/// <summary>
/// Update the CryptoMethod and BitMasks for the decrypted partition
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="output">Stream representing the output</param>
private static void UpdateDecryptCryptoAndMasks(N3DS cart, int index, Stream output)
{
// Get required offsets
uint partitionOffset = cart.GetPartitionOffset(index);
// Seek to the CryptoMethod location
output.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
// Write the new CryptoMethod
output.Write((byte)CryptoMethod.Original);
output.Flush();
// Seek to the BitMasks location
output.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
// Write the new BitMasks flag
BitMasks flag = cart.GetBitMasks(index);
flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag |= BitMasks.NoCrypto;
output.Write((byte)flag);
output.Flush();
}
#endregion
#region Encrypt
/// <inheritdoc/>
public bool EncryptFile(string filename, bool force)
{
// Ensure the constants are all set
if (_decryptArgs.IsReady != true)
{
Console.WriteLine("Could not read keys. Please make sure the file exists and try again.");
return false;
}
try
{
// Open the read and write on the same file for inplace processing
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var output = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DS.Create(input);
if (cart?.Model == null)
{
Console.WriteLine("Error: Not a 3DS cart image!");
return false;
}
// Encrypt all 8 NCCH partitions
EncryptAllPartitions(cart, force, input, output);
return true;
}
catch
{
Console.WriteLine($"An error has occurred. {filename} may be corrupted if it was partially processed.");
Console.WriteLine("Please check that the file was a valid 3DS or New 3DS cart image and try again.");
return false;
}
}
/// <summary>
/// Encrypt all partitions in the partition table of an NCSD header
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptAllPartitions(N3DS cart, bool force, Stream input, Stream output)
{
// Check the partitions table
if (cart.PartitionsTable == null || cart.Partitions == null)
{
Console.WriteLine("Invalid partitions table!");
return;
}
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
// Check the partition exists
var partition = cart.Partitions[p];
if (partition == null || partition.MagicID != NCCHMagicNumber)
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Check the partition has data
var partitionEntry = cart.PartitionsTable[p];
if (partitionEntry == null || partitionEntry.Length == 0)
{
Console.WriteLine($"Partition {p} No data... Skipping...");
continue;
}
// Encrypt the partition, if possible
if (ShouldEncryptPartition(cart, p, force))
EncryptPartition(cart, p, input, output);
}
}
/// <summary>
/// Determine if the current partition should be encrypted
/// </summary>
private static bool ShouldEncryptPartition(N3DS cart, int index, bool force)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine($"Partition {index} is not verified due to force flag being set.");
return true;
}
// If we're not forcing the operation, check if the 'NoCrypto' bit is set
else if (!cart.PossiblyDecrypted(index))
{
Console.WriteLine($"Partition {index}: Already Encrypted?...");
return false;
}
// By default, it passes
return true;
}
/// <summary>
/// Encrypt a single partition
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptPartition(N3DS cart, int index, Stream input, Stream output)
{
// Determine the keys needed for this partition
SetEncryptionKeys(cart, index);
// Encrypt the parts of the partition
EncryptExtendedHeader(cart, index, input, output);
EncryptExeFS(cart, index, input, output);
EncryptRomFS(cart, index, input, output);
// Update the flags
UpdateEncryptCryptoAndMasks(cart, index, output);
}
/// <summary>
/// Determine the set of keys to be used for encryption
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
private void SetEncryptionKeys(N3DS cart, int index)
{
// Get the partition
var partition = cart.Partitions?[index];
if (partition == null)
return;
// Get the backup header
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == null)
return;
// Get partition-specific values
byte[]? rsaSignature = partition.RSA2048Signature;
BitMasks masks = backupHeader.Flags.BitMasks;
CryptoMethod method = backupHeader.Flags.CryptoMethod;
// Get the partition keys
KeysMap[index] = new PartitionKeys(_decryptArgs, rsaSignature, masks, method, _development);
}
/// <summary>
/// Encrypt the extended header, if it exists
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExtendedHeader(N3DS cart, int index, Stream input, Stream output)
{
// Get required offsets
uint partitionOffset = cart.GetPartitionOffset(index);
if (partitionOffset == 0 || partitionOffset > input.Length)
{
Console.WriteLine($"Partition {index} No Data... Skipping...");
return false;
}
uint extHeaderSize = cart.GetExtendedHeaderSize(index);
if (extHeaderSize == 0)
{
Console.WriteLine($"Partition {index} No Extended Header... Skipping...");
return false;
}
// Seek to the extended header
input.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
output.Seek(partitionOffset + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {index}: Encrypting - ExHeader");
// Create the Plain AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, cart.PlainIV(index));
// Process the extended header
PerformAESOperation(Constants.CXTExtendedDataHeaderLength, cipher, input, output, null);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
input.Seek(0, SeekOrigin.Begin);
#endif
output.Flush();
return true;
}
/// <summary>
/// Encrypt the ExeFS, if it exists
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptExeFS(N3DS cart, int index, Stream input, Stream output)
{
if (cart.ExeFSHeaders == null || index < 0 || index > cart.ExeFSHeaders.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return false;
}
// Get the ExeFS header
var exefsHeader = cart.ExeFSHeaders[index];
if (exefsHeader == null)
{
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return false;
}
// For all but the original crypto method, process each of the files in the table
var backupHeader = cart.BackupHeader;
if (backupHeader!.Flags!.CryptoMethod != CryptoMethod.Original)
EncryptExeFSFileEntries(cart, index, input, output);
// Encrypt the filename table
EncryptExeFSFilenameTable(cart, index, input, output);
// Get the ExeFS files offset
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// Seek to the ExeFS
input.Seek(exeFsFilesOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset, SeekOrigin.Begin);
// Create the ExeFS AES cipher for this partition
uint ctroffsetE = cart.MediaUnitSize / 0x10;
byte[] exefsIVWithOffset = Add(cart.ExeFSIV(index), ctroffsetE);
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffset);
// Setup and perform the encryption
uint exeFsSize = cart.GetExeFSSize(index) - cart.MediaUnitSize;
PerformAESOperation(exeFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {s}"));
return true;
}
/// <summary>
/// Encrypt the ExeFS Filename Table
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFilenameTable(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsOffset = cart.GetExeFSOffset(index);
if (exeFsOffset == 0 || exeFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Seek to the ExeFS header
input.Seek(exeFsOffset, SeekOrigin.Begin);
output.Seek(exeFsOffset, SeekOrigin.Begin);
Console.WriteLine($"Partition {index} ExeFS: Encrypting - ExeFS Filename Table");
// Create the ExeFS AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey2C, cart.ExeFSIV(index));
// Process the filename table
byte[] readBytes = input.ReadBytes((int)cart.MediaUnitSize);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.Write(processedBytes);
#if NET6_0_OR_GREATER
// In .NET 6.0, this operation is not picked up by the reader, so we have to force it to reload its buffer
input.Seek(0, SeekOrigin.Begin);
#endif
output.Flush();
}
/// <summary>
/// Encrypt the ExeFS file entries
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private void EncryptExeFSFileEntries(N3DS cart, int index, Stream input, Stream output)
{
// Get ExeFS offset
uint exeFsHeaderOffset = cart.GetExeFSOffset(index);
if (exeFsHeaderOffset == 0 || exeFsHeaderOffset > input.Length)
{
Console.WriteLine($"Partition {index} ExeFS: No Data... Skipping...");
return;
}
// Get to the start of the files
uint exeFsFilesOffset = exeFsHeaderOffset + cart.MediaUnitSize;
// If the header failed to read, log and return
var exeFsHeader = cart.ExeFSHeaders?[index];
if (exeFsHeader?.FileHeaders == null)
{
Console.WriteLine($"Partition {index} ExeFS header does not exist. Skipping...");
return;
}
// Loop through and process all headers
for (int i = 0; i < exeFsHeader.FileHeaders.Length; i++)
{
// Only attempt to process code binary files
if (!cart.IsCodeBinary(index, i))
continue;
// Get the file header
var fileHeader = exeFsHeader.FileHeaders[i];
if (fileHeader == null)
continue;
// Create the ExeFS AES ciphers for this partition
uint ctroffset = (fileHeader.FileOffset + cart.MediaUnitSize) / 0x10;
byte[] exefsIVWithOffsetForHeader = Add(cart.ExeFSIV(index), ctroffset);
var firstCipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey, exefsIVWithOffsetForHeader);
var secondCipher = CreateAESDecryptionCipher(KeysMap[index].NormalKey2C, exefsIVWithOffsetForHeader);
// Seek to the file entry
input.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
output.Seek(exeFsFilesOffset + fileHeader.FileOffset, SeekOrigin.Begin);
// Setup and perform the encryption
PerformAESOperation(fileHeader.FileSize,
firstCipher,
secondCipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} ExeFS: Encrypting - {fileHeader.FileName}...{s}"));
}
}
/// <summary>
/// Encrypt the RomFS, if it exists
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="input">Stream representing the input</param>
/// <param name="output">Stream representing the output</param>
private bool EncryptRomFS(N3DS cart, int index, Stream input, Stream output)
{
// Validate the RomFS
uint romFsOffset = cart.GetRomFSOffset(index);
if (romFsOffset == 0 || romFsOffset > input.Length)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
uint romFsSize = cart.GetRomFSSize(index);
if (romFsSize == 0)
{
Console.WriteLine($"Partition {index} RomFS: No Data... Skipping...");
return false;
}
// Seek to the RomFS
input.Seek(romFsOffset, SeekOrigin.Begin);
output.Seek(romFsOffset, SeekOrigin.Begin);
// Force setting encryption keys for partitions 1 and above
if (index > 0)
{
var backupHeader = cart.BackupHeader;
KeysMap[index].SetRomFSValues(backupHeader!.Flags!.BitMasks);
}
// Create the RomFS AES cipher for this partition
var cipher = CreateAESEncryptionCipher(KeysMap[index].NormalKey, cart.RomFSIV(index));
// Setup and perform the decryption
PerformAESOperation(romFsSize,
cipher,
input,
output,
(string s) => Console.WriteLine($"\rPartition {index} RomFS: Encrypting - {s}"));
return true;
}
/// <summary>
/// Update the CryptoMethod and BitMasks for the encrypted partition
/// </summary>
/// <param name="cart">Cart representing the 3DS file</param>
/// <param name="index">Index of the partition</param>
/// <param name="output">Stream representing the output</param>
private static void UpdateEncryptCryptoAndMasks(N3DS cart, int index, Stream output)
{
// Get required offsets
uint partitionOffset = cart.GetPartitionOffset(index);
// Get the backup header
var backupHeader = cart.BackupHeader;
if (backupHeader?.Flags == null)
return;
// Seek to the CryptoMethod location
output.Seek(partitionOffset + 0x18B, SeekOrigin.Begin);
// Write the new CryptoMethod
// - For partitions 1 and up, set crypto-method to 0x00
// - If partition 0, restore crypto-method from backup flags
byte cryptoMethod = index > 0 ? (byte)CryptoMethod.Original : (byte)backupHeader.Flags.CryptoMethod;
output.Write(cryptoMethod);
output.Flush();
// Seek to the BitMasks location
output.Seek(partitionOffset + 0x18F, SeekOrigin.Begin);
// Write the new BitMasks flag
BitMasks flag = cart.GetBitMasks(index);
flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF;
flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupHeader.Flags.BitMasks;
output.Write((byte)flag);
output.Flush();
}
#endregion
#region Info
/// <inheritdoc/>
public string? GetInformation(string filename)
{
try
{
// Open the file for reading
using var input = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Deserialize the cart information
var cart = N3DS.Create(input);
if (cart?.Model == null)
return "Error: Not a 3DS cart image!";
// Get a string builder for the status
var sb = new StringBuilder();
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
bool decrypted = cart.PossiblyDecrypted(p);
sb.AppendLine($"\tPartition {p}: {(decrypted ? "Decrypted" : "Encrypted")}");
}
// Return the status for all partitions
return sb.ToString();
}
catch (Exception ex)
{
Console.WriteLine(ex);
return null;
}
}
#endregion
}
}

36
NDecrypt.sln Normal file
View File

@@ -0,0 +1,36 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32922.545
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{31CE0445-F693-4C9A-B6CD-499C38CFF7FE}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDecrypt", "NDecrypt\NDecrypt.csproj", "{05566793-831F-4AE1-A6D2-F9214F36618E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDecrypt.Core", "NDecrypt.Core\NDecrypt.Core.csproj", "{91C54370-5741-4742-B2E9-EC498551AD1C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{05566793-831F-4AE1-A6D2-F9214F36618E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05566793-831F-4AE1-A6D2-F9214F36618E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05566793-831F-4AE1-A6D2-F9214F36618E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05566793-831F-4AE1-A6D2-F9214F36618E}.Release|Any CPU.Build.0 = Release|Any CPU
{91C54370-5741-4742-B2E9-EC498551AD1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91C54370-5741-4742-B2E9-EC498551AD1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91C54370-5741-4742-B2E9-EC498551AD1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91C54370-5741-4742-B2E9-EC498551AD1C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5AB50D9B-BA18-4F96-804B-52E7E0845B37}
EndGlobalSection
EndGlobal

26
NDecrypt/Enumerations.cs Normal file
View File

@@ -0,0 +1,26 @@
namespace NDecrypt
{
/// <summary>
/// Functionality to use from the program
/// </summary>
internal enum Feature
{
NULL,
Decrypt,
Encrypt,
Info,
}
/// <summary>
/// Type of the detected file
/// </summary>
internal enum FileType
{
NULL,
NDS,
NDSi,
iQueDS,
N3DS,
iQue3DS,
}
}

33
NDecrypt/HashingHelper.cs Normal file
View File

@@ -0,0 +1,33 @@
using System.IO;
using SabreTools.Hashing;
namespace NDecrypt
{
internal static class HashingHelper
{
/// <summary>
/// Retrieve file information for a single file
/// </summary>
/// <param name="input">Filename to get information from</param>
/// <returns>Formatted string representing the hashes, null on error</returns>
public static string? GetInfo(string input)
{
// If the file doesn't exist, return null
if (!File.Exists(input))
return null;
// Get the file information, if possible
HashType[] hashTypes = [HashType.CRC32, HashType.MD5, HashType.SHA1, HashType.SHA256];
var hashDict = HashTool.GetFileHashesAndSize(input, hashTypes, out long size);
if (hashDict == null)
return null;
// Get the results
return $"Size: {size}\n"
+ $"CRC-32: {(hashDict.ContainsKey(HashType.CRC32) ? hashDict[HashType.CRC32] : string.Empty)}\n"
+ $"MD5: {(hashDict.ContainsKey(HashType.MD5) ? hashDict[HashType.MD5] : string.Empty)}\n"
+ $"SHA-1: {(hashDict.ContainsKey(HashType.SHA1) ? hashDict[HashType.SHA1] : string.Empty)}\n"
+ $"CSHA-256: {(hashDict.ContainsKey(HashType.SHA256) ? hashDict[HashType.SHA256] : string.Empty)}\n";
}
}
}

49
NDecrypt/NDecrypt.csproj Normal file
View File

@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.3.2</VersionPrefix>
<!-- Package Properties -->
<Title>NDecrypt</Title>
<Authors>Matt Nadareski</Authors>
<Description>DS/3DS Encryption Tool</Description>
<Copyright>Copyright (c) Matt Nadareski 2018-2025</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<RepositoryUrl>https://github.com/SabreTools/NDecrypt</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<!-- Support All Frameworks -->
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net4`))">
<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`netcoreapp`)) OR $(TargetFramework.StartsWith(`net5`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net6`)) OR $(TargetFramework.StartsWith(`net7`)) OR $(TargetFramework.StartsWith(`net8`)) OR $(TargetFramework.StartsWith(`net9`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(RuntimeIdentifier.StartsWith(`osx-arm`))">
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NDecrypt.Core\NDecrypt.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

381
NDecrypt/Program.cs Normal file
View File

@@ -0,0 +1,381 @@
using System;
using System.Collections.Generic;
using System.IO;
#if NET20 || NET35 || NET40 || NET452
using System.Reflection;
#endif
using NDecrypt.Core;
namespace NDecrypt
{
class Program
{
/// <summary>
/// Mapping of reusable tools
/// </summary>
private static readonly Dictionary<FileType, ITool> _tools = [];
public static void Main(string[] args)
{
if (args.Length < 2)
{
DisplayHelp("Not enough arguments");
return;
}
Feature feature;
if (args[0] == "decrypt" || args[0] == "d")
{
feature = Feature.Decrypt;
}
else if (args[0] == "encrypt" || args[0] == "e")
{
feature = Feature.Encrypt;
}
else if (args[0] == "info" || args[0] == "i")
{
feature = Feature.Info;
}
else
{
DisplayHelp($"Invalid operation: {args[0]}");
return;
}
bool development = false,
force = false,
outputHashes = false,
useAesKeysTxt = false;
string? config = null;
string? keyfile = null;
int start = 1;
for (; start < args.Length; start++)
{
if (args[start] == "-a" || args[start] == "--aes-keys")
{
useAesKeysTxt = true;
}
else if (args[start] == "-d" || args[start] == "--development")
{
development = true;
}
else if (args[start] == "-f" || args[start] == "--force")
{
force = true;
}
else if (args[start] == "-h" || args[start] == "--hash")
{
outputHashes = true;
}
else if (args[start] == "-k" || args[start] == "--keyfile")
{
if (start == args.Length - 1)
Console.WriteLine("Invalid keyfile path: no additional arguments found!");
start++;
string tempPath = args[start];
if (string.IsNullOrEmpty(tempPath))
Console.WriteLine($"Invalid keyfile path: null or empty path found!");
tempPath = Path.GetFullPath(tempPath);
if (!File.Exists(tempPath))
Console.WriteLine($"Invalid keyfile path: file {tempPath} not found!");
else
keyfile = tempPath;
}
else if (args[start] == "-c" || args[start] == "--config")
{
if (start == args.Length - 1)
Console.WriteLine("Invalid config path: no additional arguments found!");
start++;
string tempPath = args[start];
if (string.IsNullOrEmpty(tempPath))
Console.WriteLine($"Invalid config path: null or empty path found!");
tempPath = Path.GetFullPath(tempPath);
if (!File.Exists(tempPath))
Console.WriteLine($"Invalid config path: file {tempPath} not found!");
else
config = tempPath;
}
else
{
break;
}
}
// Derive the config path based on the runtime folder if not already set
config = DeriveConfigFile(config);
// Derive the keyfile path based on the runtime folder if not already set
keyfile = DeriveKeyFile(keyfile, useAesKeysTxt);
// If we are using a Citra keyfile, there are no development keys
if (development && useAesKeysTxt)
{
Console.WriteLine("AES keyfiles don't contain development keys; disabling the option...");
development = false;
}
// Initialize the decrypt args, if possible
DecryptArgs decryptArgs;
if (config != null)
decryptArgs = new DecryptArgs(config);
else
decryptArgs = new DecryptArgs(keyfile, useAesKeysTxt);
// Create reusable tools
_tools[FileType.NDS] = new DSTool(decryptArgs);
_tools[FileType.N3DS] = new ThreeDSTool(development, decryptArgs);
for (int i = start; i < args.Length; i++)
{
if (File.Exists(args[i]))
{
ProcessPath(args[i], feature, force, outputHashes);
}
else if (Directory.Exists(args[i]))
{
foreach (string file in Directory.GetFiles(args[i], "*", SearchOption.AllDirectories))
{
ProcessPath(file, feature, force, outputHashes);
}
}
else
{
Console.WriteLine($"{args[i]} is not a file or folder. Please check your spelling and formatting and try again.");
}
}
}
/// <summary>
/// Process a single file path
/// </summary>
/// <param name="path">File path to process</param>
/// <param name="feature">Indicates what should be done to the file</param>
/// <param name="force">Indicates if the operation should be forced</param>
/// <param name="outputHashes">Indicates if hashes should be output after a successful operation</param>
private static void ProcessPath(string path, Feature feature, bool force, bool outputHashes)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(path);
if (tool == null)
return;
Console.WriteLine($"Processing {path}");
// Encrypt or decrypt the file as requested
if (feature == Feature.Encrypt && !tool.EncryptFile(path, force))
{
Console.WriteLine("Encryption failed!");
return;
}
else if (feature == Feature.Decrypt && !tool.DecryptFile(path, force))
{
Console.WriteLine("Decryption failed!");
return;
}
else if (feature == Feature.Info)
{
string? infoString = tool.GetInformation(path);
infoString ??= "There was a problem getting file information!";
Console.WriteLine(infoString);
}
// Output the file hashes, if expected
if (outputHashes)
WriteHashes(path);
}
/// <summary>
/// Display a basic help text
/// </summary>
/// <param name="err">Additional error text to display, can be null to ignore</param>
private static void DisplayHelp(string? err = null)
{
if (!string.IsNullOrEmpty(err))
Console.WriteLine($"Error: {err}");
Console.WriteLine(@"Usage: NDecrypt <operation> [flags] <path> ...
Possible values for <operation>:
e, encrypt - Encrypt the input files
d, decrypt - Decrypt the input files
i, info - Output file information
Possible values for [flags] (one or more can be used):
-c, --config <path> Path to config.json
-a, --aes-keys Enable using aes_keys.txt instead of keys.bin
-k, --keyfile <path> Path to keys.bin or aes_keys.txt
-d, --development Enable using development keys, if available
-f, --force Force operation by avoiding sanity checks
-h, --hash Output size and hashes to a companion file
<path> can be any file or folder that contains uncompressed items.
More than one path can be specified at a time.");
}
/// <summary>
/// Derive the full path to the config file, if possible
/// </summary>
private static string? DeriveConfigFile(string? config)
{
// If a path is passed in
if (!string.IsNullOrEmpty(config))
{
config = Path.GetFullPath(config);
if (File.Exists(config))
return config;
}
// Derive the keyfile path, if possible
return GetFileLocation("config.json");
}
/// <summary>
/// Derive the full path to the keyfile, if possible
/// </summary>
private static string? DeriveKeyFile(string? keyfile, bool useAesKeysTxt)
{
// If a path is passed in
if (!string.IsNullOrEmpty(keyfile))
{
keyfile = Path.GetFullPath(keyfile);
if (File.Exists(keyfile))
return keyfile;
}
// Derive the keyfile path, if possible
return GetFileLocation(useAesKeysTxt ? "aes_keys.txt" : "keys.bin");
}
/// <summary>
/// Derive the encryption tool to be used for the given file
/// </summary>
/// <param name="filename">Filename to derive the tool from</param>
private static ITool? DeriveTool(string filename)
{
if (!File.Exists(filename))
{
Console.WriteLine($"{filename} does not exist! Skipping...");
return null;
}
FileType type = DetermineFileType(filename);
return type switch
{
FileType.NDS => _tools[FileType.NDS],
FileType.NDSi => _tools[FileType.NDS],
FileType.iQueDS => _tools[FileType.NDS],
FileType.N3DS => _tools[FileType.N3DS],
_ => null,
};
}
/// <summary>
/// Determine the file type from the filename extension
/// </summary>
/// <param name="filename">Filename to derive the type from</param>
/// <returns>FileType value, if possible</returns>
private static FileType DetermineFileType(string filename)
{
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".nds.dec", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area decrypted
|| filename.EndsWith(".nds.enc", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area encrypted
|| filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo DS");
return FileType.NDS;
}
else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as Nintendo DSi");
return FileType.NDSi;
}
else if (filename.EndsWith(".ids", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as iQue DS");
return FileType.iQueDS;
}
else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".3ds.dec", StringComparison.OrdinalIgnoreCase) // Decrypted carts/images
|| filename.EndsWith(".3ds.enc", StringComparison.OrdinalIgnoreCase) // Encrypted carts/images
|| filename.EndsWith(".cci", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo 3DS");
return FileType.N3DS;
}
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds, *.cci");
return FileType.NULL;
}
/// <summary>
/// Search for a file in local and config directories
/// </summary>
/// <param name="filename">Filename to check in local and config directories</param>
/// <returns>The full path to the file if found, null otherwise</returns>
/// <remarks>
/// This method looks in the following locations:
/// - %HOME%/.config/ndecrypt
/// - Assembly location directory
/// - Process runtime directory
/// </remarks>
private static string? GetFileLocation(string filename)
{
// User home directory
#if NET20 || NET35
string homeDir = Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%");
homeDir = Path.Combine(Path.Combine(homeDir, ".config"), "ndecrypt");
#else
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
homeDir = Path.Combine(homeDir, ".config", "ndecrypt");
#endif
if (File.Exists(Path.Combine(homeDir, filename)))
return Path.Combine(homeDir, filename);
// Local directory
#if NET20 || NET35 || NET40 || NET452
string runtimeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
#else
string runtimeDir = AppContext.BaseDirectory;
#endif
if (File.Exists(Path.Combine(runtimeDir, filename)))
return Path.Combine(runtimeDir, filename);
// Process directory
using var processModule = System.Diagnostics.Process.GetCurrentProcess().MainModule;
string applicationDirectory = Path.GetDirectoryName(processModule?.FileName) ?? string.Empty;
if (File.Exists(Path.Combine(applicationDirectory, filename)))
return Path.Combine(applicationDirectory, filename);
// No file was found
return null;
}
/// <summary>
/// Write out the hashes of a file to a named file
/// </summary>
/// <param name="filename">Filename to get hashes for/param>
private static void WriteHashes(string filename)
{
// If the file doesn't exist, don't try anything
if (!File.Exists(filename))
return;
// Get the hash string from the file
string? hashString = HashingHelper.GetInfo(filename);
if (hashString == null)
return;
// Open the output file and write the hashes
using (var fs = File.Create(Path.GetFullPath(filename) + ".hash"))
using (var sw = new StreamWriter(fs))
{
sw.WriteLine(hashString);
}
}
}
}

112
README.md Normal file
View File

@@ -0,0 +1,112 @@
# NDecrypt
[![Build and Test](https://github.com/SabreTools/NDecrypt/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/SabreTools/NDecrypt/actions/workflows/build_and_test.yml)
A simple tool for simple people.
## What is this?
This is a code port of 3 different programs:
- `3ds_encrypt.py`
- `3ds_decrypt.py`
- `woodsec` (part of [wooddumper](https://github.com/TuxSH/wooddumper))
## No really, what is this?
This tool allows you to encrypt and decrypt your personally dumped Nintendo DS, 3DS, and New 3DS cart images with minimal hassle.
## Where do I find it?
For the most recent stable build, download the latest release here: [Releases Page](https://github.com/SabreTools/NDecrypt/releases)
For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/NDecrypt/releases/tag/rolling)
## So how do I use this?
Usage: NDecrypt <operation> [flags] <path> ...
Possible values for <operation>:
e, encrypt - Encrypt the input files
d, decrypt - Decrypt the input files
i, info - Output file information
Possible values for [flags] (one or more can be used):
-c, --config <path> Path to config.json
-a, --aes-keys Enable using aes_keys.txt instead of keys.bin
-k, --keyfile <path> Path to keys.bin or aes_keys.txt
-d, --development Enable using development keys, if available
-f, --force Force operation by avoiding sanity checks
-h, --hash Output size and hashes to a companion file
<path> can be any file or folder that contains uncompressed items.
More than one path can be specified at a time.
### Additional Notes
- Input files are overwritten, even if they are only partially processed. You should make backups of the files you're working on if you're worried about this.
- Mixed folders or inputs are also accepted, you can decrypt or encrypt multiple files, regardless of their type. This being said, you can only do encrypt _OR_ decrypt at one time.
- Required files will automatically be searched for in the application runtime directory as well as `%HOME%/.config/ndecrypt`, also known as `%USERPROFILE%\.config\ndecrypt` on Windows.
- If found, `config.json` will take priority over both `keys.bin` and `aes_keys.txt`, even if `-a` and/or `-k` are defined. You've been warned.
## I feel like something is missing
There are 3 major files that you can use to give NDecrypt that extra _oomph_ of functionality that it really needs. That is, you can't do any encryption or decryption without at least one of these present. I can't give you the files and I can't generate them for you on the fly with the correct values. Keys are a thorny thing and I just do not want to deal with them. Values are validated, at least, but you'll only get yelled at on run if one of them is wrong. Don't worry, they're just disabled, not removed.
This convenient table gives an overview of the 3 supported types, the keys that they provide, as well as an even more convenient map to a well-known external tool's configuration format.
| `config.json` | `keys.bin` order | `aes_keys.txt` | rom-properties `keys.conf` |
| --- | --- | --- | --- |
| `NitroEncryptionData` | **N/A** | **UNMAPPED** | **UNMAPPED** |
| `AESHardwareConstant` | 1 | `generator` | `ctr-scrambler` |
| `KeyX0x18` | 2 | `slot0x18KeyX` | `ctr-Slot0x18KeyX` |
| `KeyX0x1B` | 3 | `slot0x1BKeyX` | `ctr-Slot0x1BKeyX` |
| `KeyX0x25` | 4 | `slot0x25KeyX` | `ctr-Slot0x25KeyX` |
| `KeyX0x2C` | 5 | `slot0x2CKeyX` | `ctr-Slot0x2CKeyX` |
| `DevKeyX0x18` | 6 | **UNMAPPED** | `ctr-dev-Slot0x18KeyX` |
| `DevKeyX0x1B` | 7 | **UNMAPPED** | `ctr-dev-Slot0x1BKeyX` |
| `DevKeyX0x25` | 8 | **UNMAPPED** | `ctr-dev-Slot0x25KeyX` |
| `DevKeyX0x2C` | 9 | **UNMAPPED** | `ctr-dev-Slot0x2CKeyX` |
**Note:** `Dev*` keys are not required for the vast majority of normal operations. They're only used if the `-d` option is included. Working with your own retail carts will pretty much never require these, so don't drive yourself silly dealing with them.
**Note:** The `NitroEncryptionData` field is also known as the "Blowfish table" for Nintendo DS carts. It's stored in the same hex string format as the other keys. There's some complicated stuff about how it's used and where it's stored, but all you need to know is that it wasn't required for `keys.bin` and `aes_keys.txt` but will be for `config.json`.
**Community Note:** If you would like to try out the new `config.json` format below and already have either `keys.bin` or `aes_keys.txt`, consider using [this helpful community-made script](https://gist.github.com/Dimensional/82f212a0b35bcf9caaa2bc9a70b3a92a) to make your life a bit easier.
### `config.json`
The up-and-coming, shiny, new, exciting, JSON-based format for storing the encryption keys that you need for Nintendo DS, 3DS, and New 3DS. This JSON file is not generated by anything, but maps pretty much one-to-one with the code inside of NDecrypt, making it super convenient to use. Keys provided need to be hex strings (e.g. `"AABBCCDD"`). Any keys that are left with `null` or `""` as the value will be ignored. See [the sample config](https://github.com/SabreTools/NDecrypt/blob/master/config-default.json) that I've nicely generated for you. You're welcome.
This is used if it's found, even if you have a `keys.bin` file or if you're using the `-a` flag. It's intentionally very bullish about being used because this will be the singular format for keys in the future. I know I mentioned this above as well, but I also know users don't like reading.
In the future, this file will be automatically generated on first run along with some cutesy little message telling you to fill it out when you get a chance. It's not doing it right now because I don't want to confuse users. Including those reading this. How meta.
### `keys.bin`
This is the OG of NDecrypt key file formats. It's a weird, binary blob of a format that is composed of little-endian values (most common extraction methods produce big endian, so keep that in mind). It's only compatible wtih Nintendo 3DS and New 3DS keys and is incredibly inflexible in its layout. The little-endianness of it is a relic of how keys were handled in-code previously and I really can't fix it now. If you don't have a key, it needs to be filled with `0x00` bytes so it doesn't mess up the read. Yeah.
Oddly, this gets confused with some similar format that GodMode9 works with, but it has nothing to do with it. If you try to use one of those files in place of this one, something will probably break. It wasn't intentional, I just didn't look ahead of time. See the table in the main part of this section for the order the keys need to be stored in.
### `aes_keys.txt`
This is an INI-based format that was super popular among 3DS emulators and probably still is. To use this over `keys.bin`, the `-a` flag has to be included or else it won't be found. Yes, even if `keys.bin` isn't even in the folder. Weird thing, I know, but just roll with it please. The one major downside to this is that development keys can't be defined in this format. If you forget this and use `-d` anyway, NDecrypt will disable that flag for you. You're welcome.
## But does it work?
As much as I'd like to think that this program is entirely without flaws, numbers need to speak for themselves sometimes. Here's a list of the supported sets and their current compatibility percentages with woodsec and the Python scripts (as of 2020-12-19):
- **Nintendo DS** - >99% compatible (Both encryption and decryption)
- **Nintendo DSi** - 100% compatible (Both encryption and decryption)
- **Nintendo 3DS** - 100% compatible (Both encryption and decryption)
- **Nintendo New 3DS** - 100% compatible (Both encryption and decryption)
Please note the above numbers are based on the current, documented values. The notable exceptions to this tend to be unlicensed carts which may be dumped incorrectly or have odd information stored in their secure area.
## Anything else?
I'd like to thank the developers of the original programs for doing the actual hard work to figure things out. I'd also like to thank everyone who helped to test this against the original programs and made code suggestions.
## Disclaimer
This program is **ONLY** for use with personally dumped files and keys and is not meant for enabling illegal activity. I do not condone using this program for anything other than personal use and research. If this program is used for anything other than that, I cannot be held liable for anything that happens.

12
config-default.json Normal file
View File

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

151
publish-nix.sh Executable file
View File

@@ -0,0 +1,151 @@
#!/bin/bash
# This batch file assumes the following:
# - .NET 9.0 (or newer) SDK is installed and in PATH
# - zip is installed and in PATH
# - Git is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
USE_ALL=false
INCLUDE_DEBUG=false
NO_BUILD=false
NO_ARCHIVE=false
while getopts "udba" OPTION; do
case $OPTION in
u)
USE_ALL=true
;;
d)
INCLUDE_DEBUG=true
;;
b)
NO_BUILD=true
;;
a)
NO_ARCHIVE=true
;;
*)
echo "Invalid option provided"
exit 1
;;
esac
done
# Set the current directory as a variable
BUILD_FOLDER=$PWD
# Set the current commit hash
COMMIT=$(git log --pretty=%H -1)
# Output the selected options
echo "Selected Options:"
echo " Use all frameworks (-u) $USE_ALL"
echo " Include debug builds (-d) $INCLUDE_DEBUG"
echo " No build (-b) $NO_BUILD"
echo " No archive (-a) $NO_ARCHIVE"
echo " "
# Create the build matrix arrays
FRAMEWORKS=("net9.0")
RUNTIMES=("win-x86" "win-x64" "win-arm64" "linux-x64" "linux-arm64" "osx-x64" "osx-arm64")
# Use expanded lists, if requested
if [ $USE_ALL = true ]; then
FRAMEWORKS=("net20" "net35" "net40" "net452" "net462" "net472" "net48" "netcoreapp3.1" "net5.0" "net6.0" "net7.0" "net8.0" "net9.0")
fi
# Create the filter arrays
SINGLE_FILE_CAPABLE=("net5.0" "net6.0" "net7.0" "net8.0" "net9.0")
VALID_APPLE_FRAMEWORKS=("net6.0" "net7.0" "net8.0" "net9.0")
VALID_CROSS_PLATFORM_FRAMEWORKS=("netcoreapp3.1" "net5.0" "net6.0" "net7.0" "net8.0" "net9.0")
VALID_CROSS_PLATFORM_RUNTIMES=("win-arm64" "linux-x64" "linux-arm64" "osx-x64" "osx-arm64")
# Only build if requested
if [ $NO_BUILD = false ]; then
# Restore Nuget packages for all builds
echo "Restoring Nuget packages"
dotnet restore
# Create Nuget Packages
dotnet pack NDecrypt.Core/NDecrypt.Core.csproj --output $BUILD_FOLDER
# Build Program
for FRAMEWORK in "${FRAMEWORKS[@]}"; do
for RUNTIME in "${RUNTIMES[@]}"; do
# Output the current build
echo "===== Build Program - $FRAMEWORK, $RUNTIME ====="
# If we have an invalid combination of framework and runtime
if [[ ! $(echo ${VALID_CROSS_PLATFORM_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then
if [[ $(echo ${VALID_CROSS_PLATFORM_RUNTIMES[@]} | fgrep -w $RUNTIME) ]]; then
echo "Skipped due to invalid combination"
continue
fi
fi
# If we have Apple silicon but an unsupported framework
if [[ ! $(echo ${VALID_APPLE_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then
if [ $RUNTIME = "osx-arm64" ]; then
echo "Skipped due to no Apple Silicon support"
continue
fi
fi
# Only .NET 5 and above can publish to a single file
if [[ $(echo ${SINGLE_FILE_CAPABLE[@]} | fgrep -w $FRAMEWORK) ]]; then
# Only include Debug if set
if [ $INCLUDE_DEBUG = true ]; then
dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true
fi
dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false
else
# Only include Debug if set
if [ $INCLUDE_DEBUG = true ]; then
dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT
fi
dotnet publish NDecrypt/NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:DebugType=None -p:DebugSymbols=false
fi
done
done
fi
# Only create archives if requested
if [ $NO_ARCHIVE = false ]; then
# Create Test archives
for FRAMEWORK in "${FRAMEWORKS[@]}"; do
for RUNTIME in "${RUNTIMES[@]}"; do
# Output the current build
echo "===== Archive Program - $FRAMEWORK, $RUNTIME ====="
# If we have an invalid combination of framework and runtime
if [[ ! $(echo ${VALID_CROSS_PLATFORM_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then
if [[ $(echo ${VALID_CROSS_PLATFORM_RUNTIMES[@]} | fgrep -w $RUNTIME) ]]; then
echo "Skipped due to invalid combination"
continue
fi
fi
# If we have Apple silicon but an unsupported framework
if [[ ! $(echo ${VALID_APPLE_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then
if [ $RUNTIME = "osx-arm64" ]; then
echo "Skipped due to no Apple Silicon support"
continue
fi
fi
# Only include Debug if set
if [ $INCLUDE_DEBUG = true ]; then
cd $BUILD_FOLDER/NDecrypt/bin/Debug/${FRAMEWORK}/${RUNTIME}/publish/
zip -r $BUILD_FOLDER/NDecrypt_${FRAMEWORK}_${RUNTIME}_debug.zip .
fi
cd $BUILD_FOLDER/NDecrypt/bin/Release/${FRAMEWORK}/${RUNTIME}/publish/
zip -r $BUILD_FOLDER/NDecrypt_${FRAMEWORK}_${RUNTIME}_release.zip .
done
done
# Reset the directory
cd $BUILD_FOLDER
fi

136
publish-win.ps1 Normal file
View File

@@ -0,0 +1,136 @@
# This batch file assumes the following:
# - .NET 9.0 (or newer) SDK is installed and in PATH
# - 7-zip commandline (7z.exe) is installed and in PATH
# - Git for Windows is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
param(
[Parameter(Mandatory = $false)]
[Alias("UseAll")]
[switch]$USE_ALL,
[Parameter(Mandatory = $false)]
[Alias("IncludeDebug")]
[switch]$INCLUDE_DEBUG,
[Parameter(Mandatory = $false)]
[Alias("NoBuild")]
[switch]$NO_BUILD,
[Parameter(Mandatory = $false)]
[Alias("NoArchive")]
[switch]$NO_ARCHIVE
)
# Set the current directory as a variable
$BUILD_FOLDER = $PSScriptRoot
# Set the current commit hash
$COMMIT = git log --pretty=format:"%H" -1
# Output the selected options
Write-Host "Selected Options:"
Write-Host " Use all frameworks (-UseAll) $USE_ALL"
Write-Host " Include debug builds (-IncludeDebug) $INCLUDE_DEBUG"
Write-Host " No build (-NoBuild) $NO_BUILD"
Write-Host " No archive (-NoArchive) $NO_ARCHIVE"
Write-Host " "
# Create the build matrix arrays
$FRAMEWORKS = @('net9.0')
$RUNTIMES = @('win-x86', 'win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')
# Use expanded lists, if requested
if ($USE_ALL.IsPresent) {
$FRAMEWORKS = @('net20', 'net35', 'net40', 'net452', 'net462', 'net472', 'net48', 'netcoreapp3.1', 'net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0')
}
# Create the filter arrays
$SINGLE_FILE_CAPABLE = @('net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0')
$VALID_APPLE_FRAMEWORKS = @('net6.0', 'net7.0', 'net8.0', 'net9.0')
$VALID_CROSS_PLATFORM_FRAMEWORKS = @('netcoreapp3.1', 'net5.0', 'net6.0', 'net7.0', 'net8.0', 'net9.0')
$VALID_CROSS_PLATFORM_RUNTIMES = @('win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')
# Only build if requested
if (!$NO_BUILD.IsPresent) {
# Restore Nuget packages for all builds
Write-Host "Restoring Nuget packages"
dotnet restore
# Create Nuget Package
dotnet pack NDecrypt.Core\NDecrypt.Core.csproj --output $BUILD_FOLDER
# Build Program
foreach ($FRAMEWORK in $FRAMEWORKS) {
foreach ($RUNTIME in $RUNTIMES) {
# Output the current build
Write-Host "===== Build Program - $FRAMEWORK, $RUNTIME ====="
# If we have an invalid combination of framework and runtime
if ($VALID_CROSS_PLATFORM_FRAMEWORKS -notcontains $FRAMEWORK -and $VALID_CROSS_PLATFORM_RUNTIMES -contains $RUNTIME) {
Write-Host "Skipped due to invalid combination"
continue
}
# If we have Apple silicon but an unsupported framework
if ($VALID_APPLE_FRAMEWORKS -notcontains $FRAMEWORK -and $RUNTIME -eq 'osx-arm64') {
Write-Host "Skipped due to no Apple Silicon support"
continue
}
# Only .NET 5 and above can publish to a single file
if ($SINGLE_FILE_CAPABLE -contains $FRAMEWORK) {
# Only include Debug if set
if ($INCLUDE_DEBUG.IsPresent) {
dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true
}
dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false
}
else {
# Only include Debug if set
if ($INCLUDE_DEBUG.IsPresent) {
dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT
}
dotnet publish NDecrypt\NDecrypt.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:DebugType=None -p:DebugSymbols=false
}
}
}
}
# Only create archives if requested
if (!$NO_ARCHIVE.IsPresent) {
# Create Program archives
foreach ($FRAMEWORK in $FRAMEWORKS) {
foreach ($RUNTIME in $RUNTIMES) {
# Output the current build
Write-Host "===== Archive Program - $FRAMEWORK, $RUNTIME ====="
# If we have an invalid combination of framework and runtime
if ($VALID_CROSS_PLATFORM_FRAMEWORKS -notcontains $FRAMEWORK -and $VALID_CROSS_PLATFORM_RUNTIMES -contains $RUNTIME) {
Write-Host "Skipped due to invalid combination"
continue
}
# If we have Apple silicon but an unsupported framework
if ($VALID_APPLE_FRAMEWORKS -notcontains $FRAMEWORK -and $RUNTIME -eq 'osx-arm64') {
Write-Host "Skipped due to no Apple Silicon support"
continue
}
# Only include Debug if set
if ($INCLUDE_DEBUG.IsPresent) {
Set-Location -Path $BUILD_FOLDER\NDecrypt\bin\Debug\${FRAMEWORK}\${RUNTIME}\publish\
7z a -tzip $BUILD_FOLDER\NDecrypt_${FRAMEWORK}_${RUNTIME}_debug.zip *
}
Set-Location -Path $BUILD_FOLDER\NDecrypt\bin\Release\${FRAMEWORK}\${RUNTIME}\publish\
7z a -tzip $BUILD_FOLDER\NDecrypt_${FRAMEWORK}_${RUNTIME}_release.zip *
}
}
# Reset the directory
Set-Location -Path $PSScriptRoot
}