diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index baf0af5..0fa1200 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -26,6 +26,7 @@ jobs: with: fetch-depth: 0 # We need full history for version number lfs: true + submodules: 'recursive' - name: "Setup .NET SDK" uses: actions/setup-dotnet@v1 @@ -72,6 +73,7 @@ jobs: with: fetch-depth: 0 # We need full history for version number lfs: true + submodules: 'recursive' - name: "Setup .NET SDK" uses: actions/setup-dotnet@v1 @@ -100,6 +102,7 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 # We need full history for version number + submodules: 'recursive' - name: "Download artifacts" uses: actions/download-artifact@v2 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3f59342 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/templates/material"] + path = docs/templates/material + url = https://github.com/ovasquez/docfx-material.git diff --git a/.vscode/settings.json b/.vscode/settings.json index 5136ca6..d81b5d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,16 +20,23 @@ "Ekona", "Encryptor", "ESRB", + "firmwares", "HMAC", "HMACSHA", "Itcm", "Modcrypt", "Modcrypted", "NAND", + "nitrocode", + "nitrorom", "Pegi", + "scenegate", "SCFG", "Texim", + "ulong", + "unlaunch", "Unswizzle", + "ushort", "WRAM" ], } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c5b9cc..a6037f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,54 +1,55 @@ -# Contributing to the project +# Contributing guidelines Thanks for taking the time to contribute! :sparkles: -In this document you will find all the information you need to make sure -the project continues to be the high-quality product we want to be! +In this document you will find all the information you need to make sure that +the projects continues to be consistent and with great quality! -## Reporting issues +## Reporting features and issues ### Issues When reporting a problem, be as specific as possible. Ideally, you should -provide an small snippet of code that reproduces the issue. Try to provide also -the following information: +provide an small snippet of code that reproduces the issue. -* OS: Linux / Windows / Mac OS -* Runtime: .NET Framework, Mono, .NET Core -* Version of the product -* Stacktrace if any -* What's happening and what you expect to happen +Please fill the default template so we can have all the required information to +address the issue. ### Features +Features are requested and handled as GitHub _issues_. + If you want to ask for a new feature, first make sure it hasn't been reported yet by using the search box in the issue tab. Make sure that the feature aligns with the direction of the project. +**Do not ask for tools for games or translations**. + ## Pull Request -Before starting a pull request, create an issue requesting the feature you would -like to see and implement. If you are fixing a bug, create also an issue to be -able to track the problem. State that you would like to work on that. The team -will reply to the issue as soon as possible, discussing the proposal if needed. -This guarantee that later on the Pull Request we don't reject the proposal -without having a discussion first and we don't waste time. +Before starting a pull request, create an issue +[requesting the feature](#features) you would like to see and implement. If you +are fixing a bug, create also an issue to be able to track the problem. + +In the issue or feature request specify that that you would like to work on it. +The team will reply as soon as possible to discuss the proposal. This guarantee +the Pull Request implementation match the direction the project is going. In general, the process to create a pull request is: 1. Create an issue describing the bug or feature and state you would like to work on that. 2. The team will cheer you and/or discuss with you the issue. -3. Fork the project. +3. Fork the project (if not done already). 4. Clone your forked project and create a git branch. 5. Make the necessary code changes in as many commits as you want. The commit message should follow this convention: -```plain -:emoji: Short description #IssueID + ```plain + :emoji: Short description #IssueID -Long description if needed. -``` + Long description if needed. + ``` 6. Create a pull request. After reviewing your changes and making any new commits if needed, the team will approve and merge it. @@ -58,404 +59,27 @@ For a complete list of emoji description see ## Code Guidelines -We follow the following standard guidelines with custom changes: - -* [Mono Code Guidelines](https://raw.githubusercontent.com/mono/website/gh-pages/community/contributing/coding-guidelines.md). -* [Microsoft Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/) -* [Microsoft C# Coding Convetions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions). - -As the -[mono team says](https://www.mono-project.com/community/contributing/coding-guidelines/#performance-and-readability): +The project includes a `.editorconfig` file that ensures the code style is +consistent. It is supported in any modern IDE. -* It is more important to be correct than to be fast. -* It is more important to be maintainable than to be fast. -* Fast code that is difficult to maintain is likely going to be looked down upon. +In general, we follow the following standard guidelines with custom changes: -And don't miss [The Zen of Python](https://www.python.org/dev/peps/pep-0020/#id3): +- [Mono Code Guidelines](https://raw.githubusercontent.com/mono/website/gh-pages/community/contributing/coding-guidelines.md). +- [Microsoft Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/) +- [Microsoft C# Coding Convetions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions). -```plain -Beautiful is better than ugly. -Explicit is better than implicit. -Simple is better than complex. -Complex is better than complicated. -Flat is better than nested. -Sparse is better than dense. -Readability counts. -Special cases aren't special enough to break the rules. -Although practicality beats purity. -Errors should never pass silently. -Unless explicitly silenced. -In the face of ambiguity, refuse the temptation to guess. -There should be one-- and preferably only one --obvious way to do it. -Although that way may not be obvious at first unless you're Dutch. -Now is better than never. -Although never is often better than *right* now. -If the implementation is hard to explain, it's a bad idea. -If the implementation is easy to explain, it may be a good idea. -Namespaces are one honking great idea -- let's do more of those! -``` +And as the +[mono team says](https://www.mono-project.com/community/contributing/coding-guidelines/#performance-and-readability): -### Quality +- It is more important to be correct than to be fast. +- It is more important to be maintainable than to be fast. +- Fast code that is difficult to maintain is likely going to be looked down + upon. -We focus on code-quality to make ours and others life easier. For that reason: +Make sure to follow these tips: -* :heavy_check_mark: **DO** write documentation for any public type and field. -* :heavy_check_mark: **DO** write a test for all the possible code branches of +- :heavy_check_mark: **DO** write documentation for any public type and method. +- :heavy_check_mark: **DO** write a test for all the possible code branches of your methods. Use a TDD approach. -* :heavy_check_mark: **DO** seek for 100% test coverage. -* :heavy_check_mark: **DO** seek for compiler warning free code. -* :heavy_check_mark: **DO** check the code with StyleCop for style issues. -* :heavy_check_mark: **DO** check the code with Gendarme for design issues. -* :heavy_check_mark: **DO** review the results of SonarQube in the Pull Request. -* :heavy_check_mark: **DO** make sure the CI pass. - -### Style Guidelines - -#### Indentation - -* :heavy_check_mark: **DO** use **spaces** with an indentation level of 4 spaces. -* :x: **DO NOT** use tabs. - -#### New lines - -* :heavy_check_mark: **DO** use Unix new lines: `\n` instead of Windows style - `\r\n`. In general, Git will handle that for you. -* :heavy_check_mark: **DO** make sure there is an empty line at the end of the - file. This ensure the latest line ends with the new line character and adding - new lines after it won't show that line as changed in the diff. - -#### Line length - -* :heavy_check_mark: **DO** use a limit of 80 columns. If you need to wrap, - move to the next line with one extra indentation level. -* :heavy_check_mark: **DO** put all the arguments in a new line if they don't - fit. -* :heavy_check_mark: **DO** use local variables to make small conditions. - -```csharp -void Method( - int a, - string b, - int c) -{ - OtherMethod( - a, - b, - c); - - bool z = (a > 3) && (a < 5); - bool w = b.StartsWith("hello"); - if (z && w) { - Code(); - } -} -``` - -#### Layout - -* :heavy_check_mark: **DO** define a type (class / struct / enum) per file. -* :heavy_check_mark: **DO** separate methods and properties with new lines. -* :heavy_check_mark: **DO** place the elements in this order: - private fields, constructors, properties, methods, nested types. Place first - static fields and order by visibility: public, protected, private. - -#### Spacing rules - -* :x: **DO NOT** leave any trailing spaces. -* :x: **DO NOT** use space before opening parenthesis calling methods or - indexers, between the parenthesis and the arguments or between the generic - types. - -```csharp -Method ( a ); -array [ 10 ]; -var list = new List (); -``` - -* :heavy_check_mark: **DO** use the following convention: - -```csharp -Method(a); -array[10]; -var list = new List(); -``` - -* :heavy_check_mark: **DO** use spaces and parenthesis for clarity in math - operations: - -```csharp -int b = (a + (5 * 2)) / (3 + 3); -``` - -* :heavy_check_mark: **DO** indent `case` statements: - -```csharp -switch (a) { - case 3: - c = "hello"; - break; - - case 5: - c = "world"; - break; - - default: - throw new Exception(); -} -``` - -#### Brace position - -* :heavy_check_mark: **DO** put the opening brace on the same line for - conditions, loops and try-catch. - -```csharp -if (a) { - Code(); - Code(); -} else if (b) { - Code(); -} else { - Code(); -} - -try { - Something(); -} catch (ArgumentNullException ex) { - Something(); -} finally { - Something(); -} - -for (int i = 0; i < 2; i++) { - Something(); -} -``` - -* :heavy_check_mark: **DO** use braces for one line conditions and loops. This - improves readability and avoid having changed lines just to add the brace when - it requires extra logic. The exception is for one line conditions for argument - checking. - -```csharp -if (a) { - Code(); -} -``` - -* :heavy_check_mark: **DO** put the brace in a new line when defining the - namespace, a type or a method. - -```csharp -namespace Program.Text -{ - public class Abc - { - public void MyMethod() - { - } - } -} -``` - -* :heavy_check_mark: **DO** put the brace in the same line for properties and - indexers. - -``` csharp -public int Property { - get { - return value; - } -} -``` - -* :heavy_check_mark: **DO** put each brace on a new line for empty methods. - -``` csharp -void EmptyMethod() -{ -} -``` - -#### Multiline comments - -* :heavy_check_mark: **DO** use always double slash comments. - -```csharp -// Blah -// Blah again -// and another Blah -``` - -### Properties - -* :x: **DO NOT** use public variables under any circumstance. - -* :heavy_check_mark: **DO** use static properties for constants. - -* :heavy_check_mark: **DO** put the getter and setter in a new line for - automatic or one line properties. - -```csharp -public int Property { - get { return value; } - set { x = value; } -} - -public int Text { - get; - private set; -} -``` - -### File headers - -* :heavy_check_mark: **DO** put the license in the file header with this format: - -```csharp -// -// .cs -// -// Author: -// -// -// Copyright (c) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -``` - -### Naming - -* :heavy_check_mark: **DO** use **always** camel casing. - -```csharp -void Method(string myArgument) - -class MyClass -{ - string myString; - int veryImportantValue; -} -``` - -* :x: **DO NOT** use `m_` or `_` as prefixes for private instance members. The - private visibility was created for that, really. - -### Keyword `this` - -* :x: **DO NOT** use `this` if it's not needed. -* :heavy_check_mark: **DO** use `this` if the method has an argument with the - same name. - -```csharp -class Foo -{ - int bar; - - public Foo(int bar) - { - this.bar = bar; - } - - void Update(int newValue) - { - bar = newValue; - Method(); - } - - public void Method() - { - } -} -``` - -### Keyword `var` - -* :heavy_check_mark: **DO** use the `var` on the left-hand side of an assignment - when the type name is repeated on the right hand side: - -``` csharp -var monkeyUUID = new NSUuid(uuid); -NSUuid something = RetrieveUUID(); -``` - -### Initializing instances - -* :heavy_check_mark: **DO** use the C# syntax to initialize instances. - -```csharp -var x = new Foo { - Label = "This", - Color = Color.Red -}; - -string[] array = { "a", "b", "c" }; -var array2 = new string[] { "d", "e", "f" }; - -var list = new List { - "hello", - "world" -}; - -var dict = new Dictionary { - { "hello": 0 }, - { "world": 1 } -}; -``` - -### Redundant visibility - -* :x: **DO NOT** use the `private` keyword to indicate internal fields since - it's already the default visibility. - -### Usings - -* :heavy_check_mark: **DO** put the `using` inside the namespace. -* :heavy_check_mark: **DO** include all the namespaces you are using. - -* :heavy_check_mark: **DO** use the `using` statement for `IDisposable` types. - -### Built-in types - -* :heavy_check_mark: **DO** use the built-in type alias instead of the class - names. - -```csharp -int a = 5; -long b = 5; -string c = "hello"; -string d = int.Parse("5"); -``` - -#### Integers - -* :heavy_check_mark: **DO** try to avoid unsigned integers in public method - arguments and properties. Some .NET language doesn't support them. - -#### Strings - -* :heavy_check_mark: **DO** use the new string interpolation: - -```csharp -int a = 5; -string b = $"The result is {a}"; -``` - -* :heavy_check_mark: **DO** use the `StringBuilder` class when creating strings - with many operations. +- :heavy_check_mark: **DO** seek for the maximum test coverage. +- :heavy_check_mark: **DO** clean compiler warning. diff --git a/README.md b/README.md index 5be1c8e..f7aaf13 100644 --- a/README.md +++ b/README.md @@ -12,27 +12,52 @@ The library supports .NET 6.0 and above on Linux, Window and MacOS. ## Supported formats -- DS cartridge: - - Filesystem: read and write - - Header: read and write, including extended header - - Banner and icon: read and write. - - ARM9 secure area encryption and decryption. - - HMAC validation and re-generation when keys are provided. - - Signature validation when keys are provided. -- DSi cartridge: - - Filesystem: read and write `arm9i` and `arm7i` programs. - - Header: read and write - - Animated banner icons - - Modcrypt encryption and decryption - - HMAC validation and re-generation (including digest) when keys are provided. - - Signature validation when keys are provided. +- :video_game: DS cartridge: + - :file_folder: Filesystem: read and write + - :information_source: Header: read and write, including extended header + - :framed_picture: Banner and icon: read and write. + - :closed_lock_with_key: ARM9 secure area encryption and decryption (KEY1). +- :video_game: DSi cartridge: + - :file_folder: Filesystem: read and write `arm9i` and `arm7i` programs. + - :information_source: Extended header: read and write + - :framed_picture: Animated banner icons + - :closed_lock_with_key: Modcrypt encryption and decryption + - :lock_with_ink_pen: HMAC validation and generation when keys are provided. + - :lock_with_ink_pen: Signature validation when keys are provided. + +## Getting started + +Check-out the +[getting started guide](https://scenegate.github.io/Ekona/dev/introduction.html) +to start using _Ekona_ in no time! Below you can find an example that shows how +to open a DS/DSi ROM file (cartridge dump). + +```csharp +// Create Yarhl node from a file (binary format). +Node game = NodeFactory.FromFile("game.nds", FileOpenMode.Read); + +// Use the `Binary2NitroRom` converter to convert the binary format +// into node containers (virtual file system tree with files and directories). +game.TransformWith(); + +// And it's done! +// Now we can access to every game file. For instance, we can export one file +Node items = Navigator.SearchNode(game, "data/Items.dat"); +items.Stream.WriteTo("dump/Items.dat"); +``` ## Documentation -Feel free to ask any question in the -[project Discussion site!](https://github.com/SceneGate/Ekona/discussions) +You can get full details about how to use library from the +[documentation](https://scenegate.github.io/Ekona/dev/features/cartridge.html) +website. -Check our on-line [API documentation](https://scenegate.github.io/Ekona/). +Don't miss the +[formats specifications](https://scenegate.github.io/Ekona/specs/cartridge/cartridge.html) +in case you need to do further research. + +And don't hesitate to ask questions in the +[project Discussion site!](https://github.com/SceneGate/Ekona/discussions) ## Build @@ -64,6 +89,9 @@ To run the performance test with memory and CPU traces: dotnet run --project src/Ekona.PerformanceTests/ -c Release -- -f "**" -m -p EP --maxWidth 60 ``` -## References +## Special thanks -- [GBATek](https://problemkaputt.de/gbatek.htm) +The DS / DSi cartridge format was based on the amazing reverse engineering work +of Martin Korth at [GBATek](https://problemkaputt.de/gbatek.htm). Its +specifications of the hardware of the video controller and I/O ports was also a +great help in additional reverse engineering. diff --git a/docs/dev/Changelog.md b/docs/dev/Changelog.md index 5540d05..5576e7e 100644 --- a/docs/dev/Changelog.md +++ b/docs/dev/Changelog.md @@ -1,3 +1,3 @@ # Changelog -To be filled on preview builds. +To be filled on preview and stable builds. diff --git a/docs/guides/Contributing.md b/docs/dev/Contributing.md similarity index 100% rename from docs/guides/Contributing.md rename to docs/dev/Contributing.md diff --git a/docs/dev/features/cartridge.md b/docs/dev/features/cartridge.md new file mode 100644 index 0000000..5ee5892 --- /dev/null +++ b/docs/dev/features/cartridge.md @@ -0,0 +1,193 @@ +# Cartridge / ROM converter and format + +Games and software in general that run in a DS or DSi device have the same +binary format. It is the same format for content dumped from a physical +cartridge, digital downloads (_DSiWare_) or digital content (firmware apps like +the menu launcher). + +The converters +[`Binary2NitroRom`](xref:SceneGate.Ekona.Containers.Rom.Binary2NitroRom) and +[`NitroRom2Binary`](xref:SceneGate.Ekona.Containers.Rom.NitroRom2Binary) support +DS and DSi software. + +You can find the technical details of the format specification +[here](../../specs/cartridge/cartridge.md). + +## Reading a ROM + +The format has information about the software and contains a list of files. This +format can be converted / unpacked into a tree _nodes_ with the converter +[`Binary2NitroRom`](xref:SceneGate.Ekona.Containers.Rom.Binary2NitroRom). + +> [!NOTE] +> +> - _Input format_: `IBinary` (like `BinaryFormat` from a file) +> - _Output format_: [`NitroRom`](xref:SceneGate.Ekona.Containers.Rom.NitroRom) +> (inherits from `NodeContainerFormat`) +> - _Parameters_: (_Optional_) +> [`DsiKeyStore`](xref:SceneGate.Ekona.Security.DsiKeyStore) + +[!code-csharp[OpenGame](../../../src/Ekona.Examples/QuickStart.cs?name=OpenGame)] + +The converter will use the following converters to transform some binary data +into the specific format: + +- Header using + [`Binary2RomHeader`](xref:SceneGate.Ekona.Containers.Rom.Binary2RomHeader) +- Banner using + [`Binary2Banner`](xref:SceneGate.Ekona.Containers.Rom.Binary2Banner) + +If the converter parameter is present and it contains the required keys, then +converter will also verify the hashes (HMAC) and signature of the binary +cartridge. If it is missing, it will skip the verification. + +> [!WARNING] +> The verification of the hashes may take some extra time. In DSi games there +> are hashes that verify the full content of the cartridge. The time it is +> proportional to the size of the binary ROM. + +Some DSi games may have +[modcrypt](../../specs/cartridge/security.md#modcrypt-aes-ctr) encryption. In +that case the converter will also decrypt the files. This operation does not +require keys. + +The data nodes contains two _tags_ that are used when +[writing back the ROM](#writing-a-rom): + +- `scenegate.ekona.id`: Index in the file system. Some times used by software to + access files. +- `scenegate.ekona.physical_id`: Index of the file in the data block. + +## Writing a ROM + +Any container node (`NodeContainerFormat`) that follows the +[structure defined by the cartridge](#nitrorom-tree) can be converter (packed) +into the ROM / cartridge binary format using the converter +[`NitroRom2Binary`](xref:SceneGate.Ekona.Containers.Rom.NitroRom2Binary). + +> [!NOTE] +> +> - _Input format_: `NodeContainerFormat` (like `NitroRom` or a directory) +> - _Output format_: `BinaryFormat` +> - _Parameters_: (_Optional_) +> [`NitroRom2BinaryParams`](xref:SceneGate.Ekona.Containers.Rom.NitroRom2BinaryParams) +> - `OutputStream`: `Stream` to write the output ROM data. If not provided, +> the converter will create a `Stream` in memory (caution). +> - `KeyStore`: keys to use to re-generate the HMACs from the header and +> digest hashes. If not provided, the hashes will be not be generated. They +> may be _zero bytes_ or the ones from the original ROM if already set. +> - `DecompressedProgram`: set it if the provided _arm9_ program file is +> decompressed with BLZ compression. In that case, the converter will update +> the compressed length in the _arm9_ program with zero. Otherwise it will +> set the current _arm9_ program length. + +[!code-csharp[WriteGame](../../../src/Ekona.Examples/QuickStart.cs?name=WriteGame)] + +The converter will use the following converters to transform the program and +banner information into binary data: + +- Header using + [`RomHeader2Binary`](xref:SceneGate.Ekona.Containers.Rom.RomHeader2Binary) +- Banner using + [`Banner2Binary`](xref:SceneGate.Ekona.Containers.Rom.Banner2Binary) + +> [!WARNING] +> If you are generating a big ROM, pass the parameter with an `OutputStream` +> from a disk file. Otherwise the output data may take a lot of RAM memory. + +The nodes from the container given as a input must have the formats and names +specified in [`NitroRom` tree](#nitrorom-tree). This means every game file must +be already in `IBinary` (`BinaryFormat`). + +> [!NOTE] +> When the `KeyStore` parameter is set, the hashes are re-generated. This +> operation may take some extra time and it is not needed. Even if the hashes +> won't be valid anymore, emulators and custom firmwares like _unlaunch_ skip +> the verification. In any case, the signature is not possible to generate +> (unknown private key) so the re-generated ROM won't be valid even if the keys +> are provided. + +The file IDs are assigned by following a depth-first approach. If every node +contains the tag `scenegate.ekona.id` then its value is used instead. + +The file data is written following the order of the file IDs. If every node +contains the tag `scenegate.ekona.physical_id`, then this order is used. + +> [!TIP] +> To reduce the size of patches, ensure that the nodes contains the +> `scenegate.ekona.physical_id` tags, so the data is written in the same order. +> You can do that by always applying changes to the file system read from a +> cartridge, instead of creating it from scratch. + +## `NitroRom` tree + +The [`NitroRom`](xref:SceneGate.Ekona.Containers.Rom.NitroRom) format inherits +from `NodeContainerFormat` and just provide helper properties to access to some +nodes easily. You can access to the data and files via its properties or +navigating the nodes. + +The structure of the tree nodes is defined and required by some converters. It +follows this structure from the root node (returned node by converter): + +- `system`: (container) ROM information and program files. + - `info`: ([`ProgramInfo`](xref:SceneGate.Ekona.Containers.Rom.ProgramInfo)) + Program information. + - `copyright_logo`: (`BinaryFormat`) Header copyright logo. + - `banner`: (container) Program banner + - `info`: ([`Banner`](xref:SceneGate.Ekona.Containers.Rom.Banner)) Program + banner content (titles and checksums). + - `icon`: (`IndexedPaletteImage`) Program icon. + - `animated`: (container) Animated icons on DSi software. + - `bitmap{0-7}`: (`IndexedImage`) Bitmaps for the animated icon frames. + - `palettes`: (`PaletteCollection`) Palettes for the animated icon frames. + - `animation`: + ([`IconAnimationSequence`](xref:SceneGate.Ekona.Containers.Rom.IconAnimationSequence)) + Animation information for the icon. + - `arm9`: (`BinaryFormat`) Executable for the ARM9 processor. + - `overlays9`: (container) Overlay directory for ARM9 processor. + - `overlay_{i}`: (`BinaryFormat`) Library overlay for ARM9 processor. + - `arm7`: (`BinaryFormat`) Executable for the ARM7 processor. + - `overlays7`: (container) Overlay directory for ARM7 processor. + - `overlay_{i}`: (`BinaryFormat`) Library overlay for ARM7 processor. +- `data`: (container) Root directory for program data files + - Every node under this container will be either another container or a + `BinaryFormat`. + +## Secure area encryption (KEY1) + +The converters does not decrypt or encrypt the secure area of the ARM9 program +automatically. They will present the ARM9 as it is inside the cartridge. But +they will do encrypt or decrypt the ARM9 with _modcrypt_ if the header specifies +so. + +You can decrypt or encrypt the file later by using the class +[`NitroKey1Encryption`](xref:SceneGate.Ekona.Security.NitroKey1Encryption) + +[!code-csharp[DecryptEncryptArm9](../../../src/Ekona.Examples/Cartridge.cs?name=DecryptEncryptArm9)] + +## Animated icon + +DSi software brings an animated icon. You can export this icon into standard GIF +format by also using the [Texim](https://github.com/SceneGate/Texim) library. +You can use the property +[`SupportAnimatedIcon`](xref:SceneGate.Ekona.Containers.Rom.Banner.SupportAnimatedIcon) +to know if the banner contains the animated icon. + +The converter +[`IconAnimation2AnimatedImage`](xref:SceneGate.Ekona.Containers.Rom.IconAnimation2AnimatedImage) +can convert the `animated` node from the banner into the `AnimatedFullImage` +type from _Texim_. You can use later its converters to convert to GIF format. + +> [!NOTE] +> +> - _Input format_: `NodeContainerFormat` +> - _Output format_: `AnimatedFullImage` +> - _Parameters_: none + +[!code-csharp[ExportIconGif](../../../src/Ekona.Examples/Cartridge.cs?name=ExportIconGif)] + +> [!NOTE] +> It is not possible to import the icon from a GIF file, only export. If you +> want to modify the animated icon, edit each frame and information. For +> instance, export each bitmap with a palette into PNG format and the animation +> information as a JSON/YAML file. diff --git a/docs/dev/introduction.md b/docs/dev/introduction.md new file mode 100644 index 0000000..a7ea921 --- /dev/null +++ b/docs/dev/introduction.md @@ -0,0 +1,60 @@ +# _Ekona: DS and DSi formats_ + +_Ekona_ is a library part of the _SceneGate_ framework that provides support for +DS and DSi file formats. + +## Usage + +The project provides the following .NET libraries (NuGet packages in nuget.org). +The libraries only support the latest .NET LTS version: **.NET 6.0**. + +- [![SceneGate.Ekona](https://img.shields.io/nuget/v/SceneGate.Ekona?label=SceneGate.Ekona&logo=nuget)](https://www.nuget.org/packages/SceneGate.Ekona) + - `SceneGate.Ekona.Containers.Rom`: DS and DSi cartridge (ROM) format. + - `SceneGate.Ekona.Security`: hash and encryption algorithms + +## Quick start + +### Cartridge file system + +Let's start by opening a game (ROM) and accessing to its files. We can virtually +_unpack_ the ROM by using the converter +[`Binar2NitroRom`](xref:SceneGate.Ekona.Containers.Rom.Binary2NitroRom). It will +create a tree of _nodes_ that we can use to access to the files. + +[!code-csharp[OpenGame](../../src/Ekona.Examples/QuickStart.cs?name=OpenGame)] + +> [!NOTE] +> The converter will not write any file to the disk. It will create a tree of +> nodes that points to different parts of the original game file. If it needs to +> decrypt a file (like `arm9i`), it will create a new _stream_ on memory. + +Now we can quickly modify our file even by code! + +[!code-csharp[ModifyFile](../../src/Ekona.Examples/QuickStart.cs?name=ModifyFile)] + +Finally, to generate a new game (ROM) file we just need one more line of code to +use the [`NitroRom2Binary`](xref:SceneGate.Ekona.Containers.Rom.NitroRom2Binary) +converter. + +[!code-csharp[WriteGame](../../src/Ekona.Examples/QuickStart.cs?name=WriteGame)] + +> [!TIP] +> Check-out the [cartridge](features/cartridge.md) section to learn about the +> optional parameters of these converters! + +### Cartridge information + +Once we have opened a game, we can access to all the information from its header +easily via the `system/info` node. + +[!code-csharp[HeaderInfo](../../src/Ekona.Examples/QuickStart.cs?name=HeaderInfo)] + +In a similar way, you can access to the information from the banner like the +game title in different languages: + +[!code-csharp[BannerTitle](../../src/Ekona.Examples/QuickStart.cs?name=BannerTitle)] + +You can also export the game icon. If it's a DSi game, it may even have an +animated icon that you can export as GIF! + +[!code-csharp[ExportIcon](../../src/Ekona.Examples/QuickStart.cs?name=ExportIcon)] diff --git a/docs/dev/toc.yml b/docs/dev/toc.yml index 56496ea..545c527 100644 --- a/docs/dev/toc.yml +++ b/docs/dev/toc.yml @@ -1,4 +1,18 @@ -- name: Changelog - href: Changelog.md +- name: ✨ Getting started + href: introduction.md + +- name: ♻️ Converters + items: + - name: Cartridge + href: features/cartridge.md + - name: API href: ../api/toc.yml + +- name: ⌨ Contribute + items: + - name: Guidelines + href: Contributing.md + +- name: Changelog + href: Changelog.md diff --git a/docs/docfx.json b/docs/docfx.json index d9c7260..e026194 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -15,41 +15,35 @@ "disableDefaultFilter": false } ], - "build": { - "content": [ - { - "files": [ "api/**.yml", "dev/**" ] - }, - { - "files": [ "toc.yml", "index.md" ] - }, - { - "files": [ "guides/**" ] - }, - { - "files": [ "README.md", "CONTRIBUTING.md" ], - "src": "../" - } - ], - "resource": [ - { - "files": [ "images/**" ] - } - ], - "dest": "_site", - "globalMetadataFiles": [ "global_metadata.json" ], - "fileMetadataFiles": [], - "template": [ - "default", - "statictoc", - "default-widescreen" - ], - "postProcessors": [], - "markdownEngineName": "markdig", - "noLangKeyword": false, - "keepFileLink": false, - "cleanupCacheHistory": false, - "disableGitFeatures": false, - "xrefService": [ "https://xref.docs.microsoft.com/query?uid={uid}" ] - } + "build": { + "content": [ + { "files": [ "api/**.yml", "dev/**" ] }, + { "files": [ "specs/**" ] }, + { "files": [ "toc.yml", "index.md" ] }, + { + "files": [ "README.md", "CONTRIBUTING.md" ], + "src": "../" + } + ], + "resource": [ + { "files": [ "images/**" ] }, + { "files": [ "Ekona.Examples/**.cs" ], "src": "../src" } + ], + "dest": "_site", + "globalMetadataFiles": [ "global_metadata.json" ], + "fileMetadataFiles": [], + "template": [ + "default", + "statictoc", + "templates/material/material", + "templates/widescreen" + ], + "postProcessors": [ "ExtractSearchIndex" ], + "markdownEngineName": "markdig", + "noLangKeyword": false, + "keepFileLink": false, + "cleanupCacheHistory": false, + "disableGitFeatures": false, + "xrefService": [ "https://xref.docs.microsoft.com/query?uid={uid}" ] + } } diff --git a/docs/global_metadata.json b/docs/global_metadata.json index ce3d74e..be9798b 100644 --- a/docs/global_metadata.json +++ b/docs/global_metadata.json @@ -1,11 +1,13 @@ { - "_appTitle": "Yarhl plugin for Nintendo DS common formats", - "_appFooter": "Copyright (c) 2021 SceneGate", - "_enableSearch": true, - "_enableNewTab": true, - "_gitContribute": { - "apiSpecFolder": "docs/apidoc", - "repo": "https://github.com/SceneGate/Ekona", - "branch": "main" - } + "_appTitle": "Ekona, SceneGate library for DS and DSi formats", + "_appFooter": "Copyright © 2021 SceneGate.
Generated by DocFX using Material (Oscar Vásquez) and Mathew (Mathew Sachin) templates.
", + "_appLogoPath": "images/logo_48.png", + "_appFaviconPath": "images/favicon.png", + "_enableSearch": true, + "_enableNewTab": true, + "_gitContribute": { + "apiSpecFolder": "docs/apidoc", + "repo": "https://github.com/SceneGate/Ekona", + "branch": "main" + } } \ No newline at end of file diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml deleted file mode 100644 index 5cc85eb..0000000 --- a/docs/guides/toc.yml +++ /dev/null @@ -1,4 +0,0 @@ -- name: Contributing - items: - - name: Guidelines - href: Contributing.md diff --git a/docs/images/favicon.png b/docs/images/favicon.png new file mode 100644 index 0000000..d2881ee Binary files /dev/null and b/docs/images/favicon.png differ diff --git a/docs/images/logo_128.png b/docs/images/logo_128.png new file mode 100644 index 0000000..f01e598 Binary files /dev/null and b/docs/images/logo_128.png differ diff --git a/docs/images/logo_48.png b/docs/images/logo_48.png new file mode 100644 index 0000000..8d0c411 Binary files /dev/null and b/docs/images/logo_48.png differ diff --git a/docs/specs/cartridge/banner.md b/docs/specs/cartridge/banner.md new file mode 100644 index 0000000..3124eed --- /dev/null +++ b/docs/specs/cartridge/banner.md @@ -0,0 +1,59 @@ +# Cartridge banner format + +The banner contains the program title in different languages and the icon. There +are different versions of the banner that support additional content. The +section start offset is defined in the [header](header.md) at `0x68`. Additional +in DSi-enhanced or exclusive games, the header also contains the banner length +at `0x208`. + +## Binary format + +| Offset | Format | Description | +| ------ | ----------- | ------------------------------------------------------ | +| 0x00 | ushort | Version (major.minor) | +| 0x02 | ushort | CRC-16 basic banner [0x20..0x840) | +| 0x04 | ushort | (Version >= 0.1) CRC-16 with Chinese [0x20..0x940) | +| 0x06 | ushort | (Version >= 0.2) CRC-16 with Korean [0x20..0xA40) | +| 0x08 | ushort | (Version >= 1.3) CRC-16 animated icon [0x1240..0x23C0) | +| 0x0A | byte[22] | Reserved | +| 0x20 | Pixel[1024] | 4 bpp indexed tiled pixels for 32x32 icon | +| 0x220 | Color[16] | 16 BGR-555 colors | +| 0x240 | char[256] | Japanese title | +| 0x340 | char[256] | English title | +| 0x440 | char[256] | French title | +| 0x540 | char[256] | German title | +| 0x640 | char[256] | Italian title | +| 0x740 | char[256] | Spanish title | +| 0x840 | char[256] | (Version >= 0.1) Chinese title | +| 0x940 | char[256] | (Version >= 0.2) Korean title | +| 0xA40 | byte[0x800] | Reserved | +| 0x1240 | Pixel[8192] | 4 bpp indexed tiled pixels animated bitmap 0..7 | +| 0x2240 | Color[128] | 16 x 8 BGR-555 colors | +| 0x2340 | byte[0x80] | [Animation sequence](#animated-icon) | + +The encoding for the title text is UTF-16. Titles may be empty, filled with +`0x00`. + +## Icon format + +The icon is an indexed bitmap of 32 x 32 pixels. The format is 4 bpp, each pixel +byte points to two pixel colors in the palette (4-bits for each pixel). The +pixels follow a tile order. This means they are not lineal, but each block of 64 +pixels defines a 8x8 pixels block of the image. + +The palette colors have the format `BGR555`. Each color is defined in an +unsigned 16-bits values. There are 5-bits per component (higher bit is unused). + +## Animated icon + +The animation sequence defines each frame of the animation. It points to one of +the 8 bitmaps and one of the 8 palettes. It also defines the duration of the +frame. The animation sequence are 16-bits values defined as: + +| Bits | Description | +| ----- | ----------------------------- | +| 0-7 | Frame duration in 60 Hz units | +| 8-10 | Bitmap index | +| 11-13 | Palette index | +| 14 | Horizontal flip | +| 15 | Vertical flip | diff --git a/docs/specs/cartridge/cartridge.md b/docs/specs/cartridge/cartridge.md new file mode 100644 index 0000000..8b1feb8 --- /dev/null +++ b/docs/specs/cartridge/cartridge.md @@ -0,0 +1,80 @@ +# Cartridge format + +The binary cartridge format (also known as `SRL` or _ROM_) is the way to pack +the information and files for a DS and DSi program. This is the format that +physical cartridge carts have, but also the format used by digital content like +DSiWare and utility programs from the DSi firmware. + +> [!NOTE] +> These documents refer to DS/DSi programs. This also includes games. + +## Cartridge memory map + +The raw binary format of the cartridge is based on four different regions. It +maps the different commands we need to send to the cartridge chip to retrieve +the data: + +| Offset | Length | Description | +| --------- | ------ | --------------------------------------------------- | +| 0x00 | 0x1000 | Header | +| 0x1000 | 0x3000 | Unknown, not readable | +| 0x4000 | 0x8000 | Secure area, first 2 KB with _KEY1_ encryption | +| 0x8000 | ... | Data area | +| 0xZZ00000 | 0x3000 | Unknown, not readable | +| 0xZZ03000 | 0x4000 | ARM9i secure area, usually with modcrypt encryption | +| 0xZZ07000 | ... | DSi data area | + +> [!NOTE] +> For details in _KEY1_ and modcrypt encryptions see section +> [security](security.md). + +## DS / DSi program sections + +Practically speaking, official DS / DSi program maps these memory map areas to +the following sections: + +- [Header](header.md) +- [Unknown, not readable](#unknown-regions), usually `0x00` +- [ARM9 processor program](program.md) (actual program code): + - Program code: starts at `0x4000` so first 2 KB are _KEY1_ encrypted. + - [Program extended parameters](program.md#program-parameters) (DS only) + - [Overlay table info](program.md#overlay-information-table) + - [Overlay files](program.md#overlays) +- [ARM7 processor program](program.md) (system code): + - Program code + - [Overlay table info](program.md#overlay-information-table) + - [Overlay files](program.md#overlays) (usually empty) +- [File name table](filesystem.md#file-name-table) +- [File access table](filesystem.md#file-access-table) +- [Banner](banner.md) +- Files data +- Additionally, for DSi programs: + - [Digest HMACs](security.md) + - [Unknown, not readable](#unknown-regions) + - [ARM9i program](program.md) + - [ARM7i program](program.md) + +### Padding + +Every section, including each file data is padded to blocks of 512 bytes with +the byte `0xFF`. + +On DS programs, the last file is not padded. + +A DS program region (nitro) ends after the _digest HMACs_. The additional DSi +program region (twilight) starts with the _unknown region_. Between these two +regions there is a special padding to fill the last block of 1 MB with the byte +`0xFF`. + +### Unknown regions + +The header contains `0x3000` unknown bytes. These bytes cannot be read as the +cartridge protocol does not support getting data from this address. On digital +programs like firmware utilities (like the launcher), this area seems to have +random bytes. + +This is similar to what happen at the DSi program region. It starts with +`0x3000` unknown bytes. These bytes cannot be dumped because the cartridge +protocol silently redirect any read command from these region to `0x8000`. These +means that if we try to dump these `0x3000` bytes, we will get from the physical +cart three times `0x1000` bytes of data from `0x8000` (arm9 after secure area). diff --git a/docs/specs/cartridge/filesystem.md b/docs/specs/cartridge/filesystem.md new file mode 100644 index 0000000..287397e --- /dev/null +++ b/docs/specs/cartridge/filesystem.md @@ -0,0 +1,108 @@ +# Cartridge file system format + +The cartridge format contains a file system with a files and directories to +define the boundaries of the data area. Files and directory have also a name and +ID that the game uses to access. + +The file system is defined in two sections: + +- _File Name Table_ (FNT): defines the hierarchy of directories and files and + their name. +- _File Access Table_ (FAT): defines the offset and length of each file. + +## File Access Table + +The start and length of this section is defined in the +[cartridge header](header.md) at `0x48` and `0x4C`. + +The table consists of a set of start and end offset for each file. This also +includes the overlays for ARM9 and ARM7 but it does not define the offsets for +the ARM9/ARM7 programs (defined in the header). + +The access of the table is by ID defined in the +[File Name Table](#file-name-table) + +For each file, there is: + +| Offset | Format | Description | +| ------ | ------ | -------------------------- | +| 0x00 | uint | Start position of the file | +| 0x04 | uint | End position of the file | + +Tips: + +- Dividing the section length by 8 we can get the total number of files. +- Subtracting the second value to the first one we can get the file length. +- File start offsets have a padding to the cartridge padding block: 512 bytes. + The padding byte in the file data content is `0xFF`. + +## File Name Table + +The start and length of this section is defined in the +[cartridge header](header.md) at `0x40` and `0x44`. + +The section actually contains two tables: + +- Directory definition table: defines the hierarchy between directories and its + files +- Name entry table: defines the names for files and directories. + +### Directory definition table + +There are 8 bytes per directory. The entries are sorted by directory ID. To get +the information for the directory ID `0xFnnn`, we would go to +`fnt_offset + ((dir_id & 0x0FFF) * 8)`. + +| Offset | Format | Description | +| ------ | ------ | ------------------------------------------------------- | +| 0x00 | uint | Name entry relative offset for directory and file names | +| 0x04 | ushort | First file ID for the directory, or 0 if empty. | +| 0x06 | ushort | Directory parent ID or number of directories for root | + +### Name entry table + +The section is a sequence of names with metadata with no random order access. +After finding the directory in the +[directory table](#directory-definition-table), jump to this section with the +relative offset and keep reading names until finding an empty token (`0x00`). + +The format of the entries is: + +| Offset | Format | Description | +| ------ | ------ | ----------------------------------------- | +| 0x00 | byte | Node type (see below) | +| 0x01 | string | Name | +| .. | ushort | Only for directories, ID of the directory | + +The _node type_ byte is defined as: + +- Bit 7: if set, the next bytes apply to a directory, otherwise it's a file. +- Bits 0-6: length of the name + +The name of the files are in order. This means that the first file name is for +the file ID defined in the second field of the +[directory table](#directory-definition-table). + +After every file and directory names of the current directory entry, the byte +`0x00` must be present. + +The text encoding for the name is Shift-JIS. As it's a Japanese encoding, names +may contain Japanese characters and cannot contain any European (Latin-1) or +other non-English language characters. + +## Files ID and order + +The files ID are assigned in order following a _depth-first_ recursion of the +file system hierarchy. The overlays for ARM9 (if any) must always be the first +ones, following the ID defined in the overlay table, but their file data is +written in a different section. + +The order of the file data in the data area seems to not follow any pattern. It +does not follow the order of the file IDs. Most likely the official SDK was able +to do incremental builds and new files during development were being placed at +the end each time. + +> [!TIP] +> In order to create small patches, tools should try to replicate the original +> order of the files. This is the case of _Ekona_, it adds the tag +> `scenegate.ekona.physical_id` to each file node. diff --git a/docs/specs/cartridge/header.md b/docs/specs/cartridge/header.md new file mode 100644 index 0000000..997327f --- /dev/null +++ b/docs/specs/cartridge/header.md @@ -0,0 +1,272 @@ +# Cartridge header + +A binary cartridge format starts with the program header. It contains +information about the program as well as the address and size of the rest of the +sections of the cartridge format. + +Formally it takes the first 0x4000 bytes of the cartridge file, but practically +it is only 0x1000. + +## Specification + +### Base header + +Every DS and DSi program has the following header fields consisting in the first +0x160 bytes of the cartridge. + +| Offset | Format | Description | +| ------ | --------- | ------------------------------------------------------ | +| 0x00 | char[12] | Program short title | +| 0x0C | char[4] | Program unique code | +| 0x10 | char[2] | Developer unique code | +| 0x12 | byte | Console, 0: DS, 2: DSi enhanced, 3: DSi exclusive | +| 0x13 | byte | KEY2 encryption seed select (0 to 7) | +| 0x14 | byte | Cartridge size: 128 KB \* 2^n | +| 0x15 | byte[8] | Reserved: zero | +| 0x1D | byte | Region, 0: base, 0x40: Korea, 0x80: China | +| 0x1E | byte | Program version | +| 0x1F | byte | Autostart flag, bit2: skip "Press button" after health | +| 0x20 | uint | ARM9 program offset | +| 0x24 | uint | ARM9 program entrypoint RAM address | +| 0x28 | uint | ARM9 program load RAM address | +| 0x2C | uint | ARM9 program size | +| 0x30 | uint | ARM7 program offset | +| 0x34 | uint | ARM7 program entrypoint RAM address | +| 0x38 | uint | ARM7 program load RAM address | +| 0x3C | uint | ARM7 program size | +| 0x40 | uint | File name table offset | +| 0x44 | uint | File name table size | +| 0x48 | uint | File address table offset | +| 0x4C | uint | File address table size | +| 0x50 | uint | Overlay table info offset for ARM9 | +| 0x54 | uint | Overlay table info size for ARM9 | +| 0x58 | uint | Overlay table info offset for ARM7 | +| 0x5C | uint | Overlay table info size for ARM7 | +| 0x60 | uint | IO port 0x40001A4 flags for normal commands | +| 0x64 | uint | IO port 0x40001A4h flags for KEY1 commands | +| 0x68 | uint | Banner offset | +| 0x6C | ushort | CRC-16 for encrypted secure area of ARM9 program | +| 0x6E | ushort | ARM9 program secure area delay (131kHz units) | +| 0x70 | uint | ARM9 program auto-load list hook RAM address | +| 0x74 | uint | ARM7 program auto-load list hook RAM address | +| 0x78 | byte[8] | Encrypted KEY1 value to disable secure area | +| 0x80 | uint | Program size (without cartridge final padding) | +| 0x84 | uint | Header size | +| 0x88 | byte[56] | Reserved | +| 0xC0 | byte[156] | [Copyright logo](#copyright-logo) | +| 0x15C | ushort | CRC-16 for copyright logo | +| 0x15E | ushort | CRC-16 for first 0x15E header bytes | +| 0x160 | uint | Debugging data offset | +| 0x164 | uint | Debugging data size | +| 0x168 | uint | Debugging data RAM load address | + +### DS extended header + +DS games released after the DSi contain an extended header used to verify its +authenticity and avoid piracy with flashcards. These fields are only load on DSi +devices. + +| Offset | Format | Description | +| ------ | --------- | ------------------------------------------------------- | +| 0x88 | uint | Offset to parameters offset in ARM9 program | +| 0x8C | uint | Offset to parameters offset in ARM7 program | +| 0x1BF | byte | [Extended program features](#extended-program-features) | +| 0x33C | byte[20] | HMAC-SHA1 of the banner (phase 3) | +| 0x378 | byte[20] | HMAC-SHA1 of the header, ARM9 and ARM7 (phase 1) | +| 0x38C | byte[20] | HMAC-SHA1 of the overlays for ARM9 (phase 2) | +| 0xF80 | byte[128] | RSA signature of the first 0xE00 bytes of the header | + +### DSi header + +Similar to the _DS extended header_, DSi enhanced or exclusive games extend the +header to 0xE00 bytes. They also overwrite some values from the standard DS +header. + +| Offset | Format | Description | +| ------ | --------- | ------------------------------------------------------- | +| 0x1C | byte | [DSi crypto mode](#dsi-crypto-mode) | +| 0x1D | byte | [Program start jump](#program-start-jump) | +| 0x90 | ushort | DS cartridge region end (512 KB units) | +| 0x92 | ushort | DSi cartridge region start (512 KB units) | +| 0x180 | uint[5] | Global MBK 1 to 5 settings | +| 0x194 | uint[3] | ARM9 local MBK 6 to 8 settings | +| 0x1A0 | uint[3] | ARM7 local MBK 6 to 8 settings | +| 0x1AC | int24 | Global MBK 9 settings | +| 0x1AF | byte | WRAM CNT settings | +| 0x1B0 | uint | [Region](#program-region) lock | +| 0x1B4 | uint | [Access control flags](#access-control-flags) | +| 0x1B8 | uint | SCFG extended features ARM7 | +| 0x1BC | byte[3] | Reserved | +| 0x1BF | byte | [Extended program features](#extended-program-features) | +| 0x1C0 | uint | ARM9 DSi program offset | +| 0x1C4 | byte[4] | Reserved | +| 0x1C8 | uint | ARM9 DSi program RAM load address | +| 0x1CC | uint | ARM9 DSi program size | +| 0x1D0 | uint | ARM7 DSi program offset | +| 0x1D4 | uint | SD / eMMC device list ARM7 RAM address | +| 0x1D8 | uint | ARM7 DSi program RAM load address | +| 0x1DC | uint | ARM7 DSi program size | +| 0x1E0 | uint | Digest DS region start offset | +| 0x1E4 | uint | Digest DS region length | +| 0x1E8 | uint | Digest DSi region start offset | +| 0x1EC | uint | Digest DSi region length | +| 0x1F0 | uint | Digest sector hashtable offset | +| 0x1F4 | uint | Digest sector hashtable length | +| 0x1F8 | uint | Digest block hashtable offset | +| 0x1FC | uint | Digest block hashtable length | +| 0x200 | uint | Digest sector size | +| 0x204 | uint | Digest block sector count | +| 0x208 | uint | Banner length | +| 0x20C | byte | SD / eMMC file `shared2/0000` length | +| 0x20D | byte | SD / eMMC file `shared2/0001` length | +| 0x20E | byte | EULA agreement version | +| 0x20F | byte | Use ratings | +| 0x210 | uint | Program size including DSi areas | +| 0x214 | byte | SD / eMMC file `shared/0002` length | +| 0x215 | byte | SD / eMMC file `shared/0003` length | +| 0x216 | byte | SD / eMMC file `shared/0004` length | +| 0x217 | byte | SD / eMMC file `shared/0005` length | +| 0x218 | uint | ARM9 DSi program parameters table relative offset | +| 0x21C | uint | ARM7 DSi program parameters table relative offset | +| 0x220 | uint | Modcrypt encrypted area 1 offset | +| 0x224 | uint | Modcrypt encrypted area 1 length | +| 0x228 | uint | Modcrypt encrypted area 2 offset | +| 0x22C | uint | Modcrypt encrypted area 2 length | +| 0x230 | ulong | Title ID (similar to Wii / 3DS) | +| 0x238 | uint | SD / eMMC `public.sav` length | +| 0x23C | uint | SD / eMMC `private.sav` length | +| 0x240 | byte[176] | Reserved | +| 0x2F0 | byte | [Age rating](#age-rating) CERO (Japan) | +| 0x2F1 | byte | [Age rating](#age-rating) ESRB (US / Canada) | +| 0x2F2 | byte | Reserved | +| 0x2F3 | byte | [Age rating](#age-rating) USK (Germany) | +| 0x2F4 | byte | [Age rating](#age-rating) PEGI | +| 0x2F5 | byte | Reserved | +| 0x2F6 | byte | [Age rating](#age-rating) PEGI Portugal | +| 0x2F7 | byte | [Age rating](#age-rating) PEGI UK and BBFC | +| 0x2F8 | byte | [Age rating](#age-rating) AGCB (Australia) | +| 0x2F9 | byte | [Age rating](#age-rating) GRB (South Korea) | +| 0x2FA | byte[6] | Reserved | +| 0x300 | byte[20] | HMAC-SHA1 of ARM9 program with encrypted secure area | +| 0x314 | byte[20] | HMAC-SHA1 of the ARM7 program | +| 0x328 | byte[20] | HMAC-SHA1 of the digest block area | +| 0x33C | byte[20] | HMAC-SHA1 of the banner | +| 0x350 | byte[20] | HMAC-SHA1 of the ARM9 DSi program | +| 0x364 | byte[20] | HMAC-SHA1 of the ARM7 DSi program | +| 0x378 | byte[20] | Not used (only extended DS header) | +| 0x38C | byte[20] | Not used (only extended DS header) | +| 0x3A0 | byte[20] | HMAC-SHA1 of ARM9 program without secure area | +| 0x3B4 | ... | Reserved | +| 0xE00 | ... | Debug arguments | +| 0xF80 | byte[128] | RSA signature of the first 0xE00 bytes of the header | + +## Detailed types + +### Copyright logo + +The copyright logo must be present in every game. The BIOS will verify only the +checksum value (it doesn't do the checksum). The firmware will verify the data. +This is the same logo as for GBA cartridges. + +The logo contains the _Nintendo_ image that can be see in the _Health and +Safety_ boot screen. It will show only if a cartridge is present (otherwise it +can't read the data). + +The image is compressed with Huffman and then encrypted with simple additions. +The header for the Huffman compression (with the codewords tree) is in the BIOS. +An implementation to decrypt and encrypt the logo can be found +[here](https://gist.github.com/pleonex/6265017). A blog post in Spanish +describing the assembly code is also available +[here](http://pleonet.blogspot.com/2013/08/logo-de-nintendo-en-gba-y-nds.html) + +### Extended program features + +8-bits flag enumeration: + +| Bit | Meaning if set | +| --- | ----------------------------------------------------------------- | +| 0 | Use touchscreen and sound controllers in DSi mode | +| 1 | Require to accept EULA agreement | +| 2 | Launcher uses icon from banner.sav instead of cartridge banner | +| 3 | Launcher shows Wi-Fi connection icon | +| 4 | Launcher shows DS wireless icon | +| 5 | Header contains HMAC of the banner | +| 6 | Header contains HMAC and RSA signature of the header and programs | +| 7 | Developer application | + +### DSi crypto mode + +8-bits flag enumeration: + +| Bit | Meaning if set | +| --- | -------------------------------------------------------- | +| 0 | Program contains DSi exclusive region area (arm9i/arm7i) | +| 1 | Cartridge contains areas with modcrypt encryption | +| 2 | Modcrypt encryption uses the debug key | +| 3 | Disable debug features | + +### Program start jump + +Regular enumeration: + +| Value | Description | +| ----- | --------------------------------------------------- | +| 0 | Normal jump copying header in the RAM | +| 1 | Special / temporary launch used for the system menu | + +### Program region + +32-bits flag enumeration: + +| Bit | Allowed region if set | +| --- | --------------------- | +| 0 | Japan | +| 1 | USA | +| 2 | Europe | +| 3 | Australia | +| 4 | China | +| 5 | Korea | +| All | Region free | + +### Access control flags + +32-bits flag enumeration: + +| Bit | Meaning if set | +| --- | ------------------------------------------------------ | +| 0 | Use common key | +| 1 | Use AES slot B | +| 2 | Use AES slot C | +| 3 | Allow access to SD card device | +| 4 | Allow access to NAND | +| 5 | Game card power on | +| 6 | Software uses `shared2` file from storage SD / eMMC | +| 7 | Sign camera photo JPEG files for launcher (AES slot B) | +| 8 | Game card DS mode | +| 9 | SSL client certificates (AES slot A) | +| 10 | Sign camera photo JPEG files for user (AES slot B) | +| 11 | Read access for photos | +| 12 | Write access for photos | +| 13 | Read access to SD card | +| 14 | Write access to SD card | +| 15 | Read access to cartridge save files | +| 16 | Write access to cartridge save files | +| 17 | Use debugger common client key | + +### Age rating + +| Bits | Description | +| ---- | ------------------------------ | +| 0-4 | Age | +| 5 | Reserved | +| 6 | Prohibited in country | +| 7 | Age rating enabled for country | + +## Unknown region + +According to the field `[0x84]`, the header size is always `0x4000` bytes. +However, only the first `0x1000` can be read by the DS / DSi device. The region +`0x1000 .. 0x3000` cannot be accessed outside the physical cart. + +On digital programs like firmware utilities (like the launcher), this area seems +to have random bytes. diff --git a/docs/specs/cartridge/program.md b/docs/specs/cartridge/program.md new file mode 100644 index 0000000..077be23 --- /dev/null +++ b/docs/specs/cartridge/program.md @@ -0,0 +1,164 @@ +# Cartridge programs + +The DS and DSi have two processors: ARM9 and ARM7. Each processor loads a +different program and run it in parallel. The hardware contains synchronization +mechanism between the two processors and there is an area of the RAM that it's +shared between the two processors. + +The developers usually only can write code for the ARM9 processor. The ARM7 +processor is reserved for the system code like controlling the hardware (sound, +touchscreen). The compiled object files with the assembly code are usually named +after the processors: `arm9.bin` and `arm7.bin`. + +DSi-enhanced and exclusive games contain two additional programs for the same +processors. These files contains DSi exclusive code that it's only load when the +game runs on a DSi console. These files are load into a different RAM address +and are called from the regular program file. It allows to run the program on +regular DS consoles with additional features when it runs in DSi consoles. These +files are usually named `arm9i.bin` and `arm7i.bin`. + +The header of the cartridge contains several fields for these files including: + +- File offset +- File length +- Load address into the RAM +- Entry point (first line of code to execute) +- [Program parameters](#program-parameters) + +## Overlays + +The RAM memory of the DS devices is very limited and it needs to hold the +program and its data. To save memory, large code program files are usually split +by features into different files (similar to the concept of libraries). These +files are named _overlays_ and can be load and unload from memory at any moment +from the main (_armX.bin_) program. For instance, some games put all its code +for battles on an separate overlay. This overlay is loaded into memory when the +battle starts and removed when it finishes. Different overlays may be load into +the same RAM address but at different times. + +Practically speaking, programs only contain overlays for the ARM9 processor. + +### Overlay information table + +Before the data of each overlay file, there is a table that contains a set of +fields for each overlay. For each overlay there are 32-bytes as follow: + +| Offset | Format | Description | +| ------ | ------ | ----------------------------------- | +| 0x00 | uint | ID | +| 0x04 | uint | Load RAM address | +| 0x08 | uint | File length to load into RAM | +| 0x0C | uint | BSS data section length | +| 0x10 | uint | Static initialization start address | +| 0x14 | uint | Static initialization end address | +| 0x18 | uint | Overlay file ID (same as [0x00]) | +| 0x1C | uint | Flags | + +Flags: + +| Bits | Description | +| ----- | ------------------------------------------- | +| 0-23 | Compressed file length (0 for uncompressed) | +| 24 | If set, the file is compressed | +| 25 | If set, the overlay is digitally signed | +| 26-31 | Reserved | + +## Secure area + +The first 16 KB of the ARM9 program code lands into the _secure area_ section of +the ROM. These bytes must be load using a special command of the cartridge +protocol. Additionally the first 2 KB of the _secure area_ (so of the file) are +_KEY1_ encrypted. + +The first 2 KB doesn't usually contain code. Instead its format is: + +| Offset | Format | Description | +| ------ | ----------- | --------------------------------------------- | +| 0x00 | char[8] | Encryption token `encryObj` | +| 0x08 | uint | Constant `FF DE FF E7 FF DE` | +| 0x0E | ushort | CRC-16 of the next 0x7F0 bytes. | +| 0x10 | byte[0x7F0] | Garbage / random bytes with small code blocks | + +The console decrypts first the initial 8 bytes. If the result does not match +`encryObj`, then it assumes the content is wrong and corrupts the rest of the +data with the same bytes as the token. If the data match, then overwrites these +characters with `FF DE FF E7 FF DE FF E7` (to hide the token). + +The BIOS nor the firmware do not verify the CRC-16. They only verify the first +8-bytes with the token. For this reason, this area is usually used for hooks and +patch code. + +### Disable secure area + +The secure area can be disabled by setting a special constant value in the +cartridge header at 0x78. The constant is encrypted with +[KEY1](security.md#blowfish-key1) encryption. It's decrypted value is +`NmMdOnly`. + +## Program parameters + +The ARM9 program contains a table inside the file that defines additional +parameters about the format like the location of _ITCM_ code or the +[_BSS_](https://en.wikipedia.org/wiki/.bss) area. These parameters are used at +runtime to initialize the program before calling the actual program entry-point. + +The location of this table is defined in the extended header for DSi games. In +the case of DS games, there are 12 bytes after the ARM9 program file (also known +as _arm9 tail_) that defines additional pointers. These bytes are not included +as part of the length of the ARM9 program file. The format of these bytes is: + +| Offset | Format | Description | +| ------ | ------ | ----------------------------------------------------- | +| 0x00 | uint | Constant: `0x2106C0DE` (_nitrocode_ marker) | +| 0x04 | uint | Offset to table parameters (see below) | +| 0x08 | uint | Offset to [HMAC-SHA1 table for overlays](security.md) | + +If the _nitrocode_ marker is present, then this structure exists. This constant +or marker is used across the _arm9_ program code to identify structures of data +after the compilation phase. + +> [!TIP] +> The constant number is a play of hexadecimal numbers to create the word: +> _nitro code_. _Nitro_ was the project name of the DS device. The meaning of +> the bytes is: +> +> - `2`: _ni_ japanese number +> - `10`: _to_ japanese number +> - `6`: _ro_ japanese number +> - `C0DE`: _code_ with hexadecimal numbers + +The format of the program parameter table is: + +| Offset | Format | Description | +| ------ | -------- | ----------------------------------------------------- | +| 0x00 | uint | First ITCM block info | +| 0x04 | uint | Last ITCM block info | +| 0x08 | uint | ITCM data offset | +| 0x0C | uint | BSS data offset | +| 0x10 | uint | BSS data end offset | +| 0x14 | uint | Compressed program size | +| 0x18 | uint | SDK version with format: major.minor.build | +| 0x1C | uint | _nitrocode_ marker | +| 0x20 | uint | _nitrocode_ marker in big endian | +| 0x24 | string[] | Frameworks used with format `[:]` | + +An _ITCM block info_ consists of two 32-bits integer values: target RAM address +and block size. The data should be copied consecutive starting from _ITCM data +offset_ field value. + +> [!NOTE] +> Programs regenerating a cartridge file should take into account the +> _compressed program size_ field value and update it if the _program file_ +> changes and it's compressed value is different. + +## Program entrypoint + +The entrypoint code for the _ARM9_ program is defined by the SDK. It runs a set +of initialization steps before calling the actual program _main_ function. + +These initializations includes: + +- Set the program stack address +- Moves program code and data to TCM areas (cache areas that run faster) +- Clean the _BSS_ area +- Decompress the rest of the program file (reverse LZSS compression or BLZ) diff --git a/docs/specs/cartridge/security.md b/docs/specs/cartridge/security.md new file mode 100644 index 0000000..b5a24e1 --- /dev/null +++ b/docs/specs/cartridge/security.md @@ -0,0 +1,348 @@ +# Cartridge security + +## Blowfish (KEY1) + +The cartridge data protocol data exchange as well as the cartridge secure area +are encrypted with _KEY1_ encryption. This is a variant of the the Blowfish +algorithm. + +The algorithm works with two 32-bits blocks of data. It uses a key buffer of +`0x12 + 0x400` 32-bits. + +### Initialization + +The initialization takes as a parameter: + +- _ID code_: for games it's the 4-chars game code. +- _Level_: depending on the data to encrypt/decrypt it's 1, 2 or 3. +- _Modulo_: for games it's `8`, for firmware it's `12`. +- _Key_: 0x1048 bytes from the DS ARM7 BIOS starting at 0x30. + +```csharp +public void Initialize(uint idCode, int level, int modulo, uint[] key) +{ + // Initializes key buffer by copying the key (same length) + Array.Copy(keyBuffer, key, 0); + + // Initializes key code from the ID code. + uint[] keyCode = new uint[3] { idCode, idCode >> 1, idCode << 1 }; + + ApplyKeyCode(modulo, keyCode); // level 1 (always) + if (level >= 2) { + ApplyKeyCode(modulo, keyCode); + } + + keyCode[1] <<= 1; + keyCode[2] >>= 1; + + if (level >= 3) { + ApplyKeyCode(modulo, keyCode); + } +} + +private void ApplyKeyCode(int modulo, uint[] keyCode) +{ + Encrypt(ref keyCode[1], ref keyCode[2]); + Encrypt(ref keyCode[0], ref keyCode[1]); + + modulo /= 4; + for (int i = 0; i <= 0x11; i++) { + uint xorValue = ReverseEndianness(keyCode[i % modulo]); + keyBuffer[i] = keyBuffer[i] ^ xorValue; + } + + uint scratch0 = 0; + uint scratch1 = 0; + for (int i = 0; i <= 0x410; i += 2) { + Encrypt(ref scratch0, ref scratch1); + keyBuffer[i] = scratch1; + keyBuffer[i + 1] = scratch0; + } +} +``` + +### Encryption or decryption + +To decrypt or encrypt the algorithm is very similar, it only changes the +direction to iterate the bottom part of the key buffer. + +```csharp +public void Encrypt(ref uint data0, ref uint data1) +{ + uint x = data1; + uint y = data0; + + for (int i = 0; i < 0x10; i++) { + uint z = keyBuffer[i] ^ x; + x = GetMixer(z) ^ y; + y = z; + } + + data0 = x ^ keyBuffer[0x10]; + data1 = y ^ keyBuffer[0x11]; +} + +public void Decrypt(ref uint data0, ref uint data1) +{ + uint x = data1; + uint y = data0; + + for (int i = 0x11; i >= 0x2; i--) { + uint z = keyBuffer[i] ^ x; + x = GetMixer(z) ^ y; + y = z; + } + + data0 = x ^ keyBuffer[0x01]; + data1 = y ^ keyBuffer[0x00]; +} + +private uint GetMixer(uint index) +{ + uint value = keyBuffer[0x12 + ((index >> 24) & 0xFF)]; + value += keyBuffer[0x112 + ((index >> 16) & 0xFF)]; + value ^= keyBuffer[0x212 + ((index >> 8) & 0xFF)]; + value += keyBuffer[0x312 + (index & 0xFF)]; + return value; +} +``` + +### Usage + +The cartridge protocol use this encryption with different parameters as follow: + +1. Decrypt "secure area disable" header value (0x78) with modulo 8 and level 1. +2. Initialize again with modulo 8 and level 2 to encrypt KEY1 commands and get + the secure area. +3. Decrypt the first 8 bytes of the secure area (secure area ID) +4. Initialize again with modulo 8 and level 3 and decrypt the first 2 KB of the + secure area +5. (DSi only) Initialize again with modulo 8 and level 1 to encrypt DSi KEY1 + commands. + +> [!NOTE] +> The _secure area ID_ is decrypted twice before getting its actual decrypted +> value. First it decrypts only its value with level 2 and then it's decrypted +> with the rest of the secure area with level 3. + +## Modcrypt (AES-CTR) + +DSi games may have some areas of the cartridge encrypted with an additional +encryption named _modcrypt_ which is a regular _AES-CTR (Counter)_. + +The encryption is present in a cartridge only if the flag _Modcrypted_ is +present in the [cartridge header](header.md) [0x1C]. + +### Implementation + +You can implement this mode by using repeatedly AES-ECB with no padding mode. +The _counter_ mode works with a _counter register_, an array of 16 bytes. + +![AES-CTR from Wikipedia](https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/CTR_encryption_2.svg/601px-CTR_encryption_2.svg.png) + +Use the user provided _IV_ (initialization value) to set the initial value of +the counter. Then initialize the AES-ECB engine with a zero bytes _IV_ +(initialization value) and the user provided key. + +For each block of data, repeat the following operation: + +1. Encrypt the current counter register into a new array of bytes, let's name it + `xorMask`. +2. Read a block of data from the input. +3. Apply the bitwise operation XOR to each byte with one of the bytes from the + `xorMask`. Iterate the `xorMask` from the end and the input from 0. +4. Write the result into the output +5. Increment the counter as the 16 bytes array were a big number in little + endian. + +### Key generation + +The key for the encryption is generated based on data of the cartridge. There +are two kinds of keys for developers / debugging and for retail software. + +In both cases the initialization value (IV) is the same and depend in the +_modcrypt area_ (1 or 2) to apply the encryption: + +- Area 1: First 0x10 bytes of the HMAC-SHA1 of the ARM9 program (with encrypted + secure area). In the header at 0x300. +- Area 2: First 0x10 bytes of the HMAC-SHA1 of the ARM7 program. In the header + at 0x314. + +The IV and the key must be **reversed** before applying to the engine. + +#### Debug software key + +Use this key of the [cartridge header](header.md) has at least one of the +following flags enabled: + +- DSi crypto flags ([0x1C]) with _ModcryptKeyDebug_. +- Program features ([0x1BF]) with _DeveloperApp_. + +Then the 16-bytes of the key are the first 16 bytes of the cartridge (header). +This would be the _software short title_ and its _unique code_. + +#### Retail key + +The key is the result of scrambling two keys (X and Y) of 16 bytes. + +- Key X: + - 0..7: ASCII bytes of the word `Nintendo`. + - 8..11: software unique code + - 12..15: backward software unique code +- Key Y: First 16 bytes of the HMAC-SHA1 of the ARM9 DSi program. + +Then use these two keys as a huge little-endian number so the final key is the +result of the first 16 bytes of the math expression: + +```plain +key = ((keyX ^ keyY) + 0xFFFEFB4E295902582A680F5F1A4F3E79h) ROL 42 +``` + +> [!NOTE] +> The bitwise shift operation is _circular_. It moves the _overflown_ bits into +> the least significant position. + +## Hashes + +The cartridge contains several HMAC-SHA1 hashes that ensure the content of the +cartridge is not modified. These hashes are later included in the +[signature](#signature). Because of the signature, although they can be +re-generated, they can't be modified either. + +### Cartridge header + +The header contains most of the hashes. They were introduced starting the +[extended header](header.md#ds-extended-header) of DS games released after the +DSi. These hashes are identical to the three phases we can find inside the allow +list of DS games the DSi device contains. The allow list is only applied for +software that do not contain the hashes in the header. + +All the cartridge header are standard HMAC-SHA1 hashes with different keys. + +DS software specific hashes, from their header positions: + +- 0x33C: Phase 3 + - Full banner content (including header, titles and icon). + - The key is the same used for phase 3-4 allow list. Found in the launcher. + - Only present if header field _extended program features_ has bit 6. +- 0x378: Phase 1. + - First 0x160 bytes of header + ARM9 program with encrypted secure area + + ARM7. + - The key is the same used for phase 1-2 allow list. Found in launcher. + - Only present if header field _extended program features_ has bit 6. +- 0x38C: Phase 2. + - Overlay 9 table info + [FAT entries](filesystem.md#file-access-table) of the + overlay files + partial content of the overlays + - The hash may include the cartridge padding of the overlay (`0xFF` to 512 + bytes) + - There is a maximum bytes to hash per overlay. The limit changes during the + iteration. + - The limit is `1 << 0xA` minus already overlay's blocks read divided by the + remaining overlays to hash and finally multiplied by the block size (512 + bytes). + - The key is the same used for phase 1-2 allow list. Found in launcher. + - Only present if header field _extended program features_ has bit 6. + +DSi software specific hashes, from their header positions: + +- 0x300: ARM9 program with encrypted secure area. +- 0x314: ARM7 program. +- 0x328: Digest block area (only block area, no sector area hashes) +- 0x33C: Banner but using the DSi key this time. +- 0x350: ARM9 DSi program. +- 0x365: ARM7 DSi program. +- 0x3A0: ARM9 program skipping the secure area (first 16 KB). + +All the DSi software hashes use the same key. It can be found in the ARM9 +program, a 64-bytes block of data that starts with the _nitrocode_. It's the +second occurrence of the _nitrocode_. + +> [!NOTE] +> DSi software does not contain phase 1 and 2 hashes as DS software does. The +> digest area should cover and the additional hashes should cover the same. + +### Overlays + +Some DS software contains additional HMAC-SHA1 inside the ARM9 program for its +overlays. It seems these hashes are only verified by the program when it boots +in _download play_ mode (transferring the software to other console over the +air). + +The location of these hashes inside the program is in the +[_ARM9 tail_](program.md#program-parameters). In contrast to the overlays hash +from the cartridge header, there is one hash per full content of the overlay. + +The overlay info table has a flag per overlay indicating if the overlay has a +hash. + +The 64-bytes key used to generate this hash is the one that starts with the +_nitrocode_. We can find this key inside the same ARM9 program at the second +occurrence of the _nitrocode_. + +### Digest area + +The digest are contains two subareas: sector hashes and block hashes. + +The first area hashes almost the full content of the cartridge data. It creates +a set of hashes, one per _sector_. The length of the sector is defined in the +header at 0x200. The data to hash is at the same time divided in two areas: data +belonging to DS-compatible regions and DSi-specific regions. The start offset +and length of both areas are defined in the header (0x1E0). These areas must be +padded to the sector size (usually same as cartridge padding, 512 bytes). + +The areas typically cover from the secure area (ARM9 program) to the last file +(just before the digest area) for the DS compatible region. The DSi-specific +region covers the ARM9 DSi and ARM7 DSi programs. + +This list of hashes of the cartridge is then hashed again in blocks, creating +another list of hashes. Each block contains a number of hashes from the sector +area. The number of hashes is defined in the header at 0x204. + +Finally, this last list of hashes is hashed one last time for the cartridge +header HMAC at 0x328. + +Every hash of these sections, including the ones from the cartridge data are +HMAC-SHA1. The key is the same used for the DSi header HMACs. + +> [!NOTE] +> The hashes of the data corresponding to the ARM9 are for its encrypted secure +> area (KEY1), as it would be as we get it from the cartridge protocol. +> +> The hashes for the four programs (ARM9, ARM7, ARM9i, ARM7i) happen without +> modcrypt encryption (decrypted). + +## CRC + +The cartridge header also contains several CRC-16 checksums. The variant is +_MODBUS_. + +## Signature + +At the end of the cartridge header of DSi and late released DS games there is a +digital signature of the this header. It ensures that there is no information +about the loading process of the software is changed. It also covers the +[hashes](#hashes) so the rest of the cartridge cannot be modified either. + +The signature is encrypted with the asymmetrical algorithm RSA. The private key +is unknown at this moment, so the signature cannot be re-generated. Custom +firmware like _unlaunch_ skip the verification of this signature, allowing +booting non-published games. + +The public key can be generated from its two components: + +- Exponent: constant `65537 (0x010001)` +- Public modulus: 128-bytes in the ARM9 BIOS at 0x8974 + +> [!NOTE] +> You may need to prepend a 0 to the public modulus to ensure the software does +> not think it's a negative number. + +The signature is the raw SHA-1 hash of the first 0xE00 bytes of the cartridge +(header) with PKCS #7 1.5 padding. **It is not encoded with ASN.1** as specified +in PKCS #1. + +You can verify the signature by decrypting with RSA the value using the public +key and comparing with a SHA-1 hash of the header. + +In DS cartridge is only present if header field _extended program features_ has +bit 6. diff --git a/docs/specs/cartridge/toc.yml b/docs/specs/cartridge/toc.yml new file mode 100644 index 0000000..12ab7db --- /dev/null +++ b/docs/specs/cartridge/toc.yml @@ -0,0 +1,17 @@ +- name: Cartridge + href: cartridge.md + +- name: Header + href: header.md + +- name: File system + href: filesystem.md + +- name: Banner + href: banner.md + +- name: Programs (armX) + href: program.md + +- name: Security + href: security.md diff --git a/docs/specs/toc.yml b/docs/specs/toc.yml new file mode 100644 index 0000000..eb09058 --- /dev/null +++ b/docs/specs/toc.yml @@ -0,0 +1,2 @@ +- name: 📁 Cartridge + href: cartridge/toc.yml diff --git a/docs/templates/material b/docs/templates/material new file mode 160000 index 0000000..2cc98b1 --- /dev/null +++ b/docs/templates/material @@ -0,0 +1 @@ +Subproject commit 2cc98b18b70bcb5f7639269c58ab1e148eb12415 diff --git a/docs/templates/widescreen/styles/main.js b/docs/templates/widescreen/styles/main.js new file mode 100644 index 0000000..d29bb2e --- /dev/null +++ b/docs/templates/widescreen/styles/main.js @@ -0,0 +1,4 @@ +// From https://github.com/MathewSachin/docfx-tmpl by Mathew Sachin +var containers = $(".container"); +containers.removeClass("container"); +containers.addClass("container-fluid"); diff --git a/docs/toc.yml b/docs/toc.yml index 3df60e1..515fa1c 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,11 +1,14 @@ - name: Home href: index.md -- name: Guides - href: guides/ +- name: Getting started + href: dev/introduction.md -- name: API - href: dev/ +- name: Dev docs + href: dev/features/cartridge.md + +- name: Format specifications + href: specs/ - name: GitHub href: https://github.com/SceneGate/Ekona diff --git a/src/.editorconfig b/src/.editorconfig index 2646695..1532274 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -182,7 +182,7 @@ dotnet_diagnostic.S1135.severity = suggestion # It's almost inevitable to have T dotnet_diagnostic.S1172.severity = silent # Buggy # Special rules for test projects -[*Tests/**] +[{*Tests/**,*Examples/**}] dotnet_diagnostic.CS1591.severity = none # Disable documentation dotnet_diagnostic.CA1001.severity = none # No need to implement IDisposable in test classes with cleanup. dotnet_diagnostic.CA1034.severity = none # Public types in test classes for testing implementations @@ -194,3 +194,9 @@ dotnet_diagnostic.SA0001.severity = none # Disable documentation dotnet_diagnostic.SA1600.severity = none # Disable documentation dotnet_diagnostic.S2699.severity = none # Assert may be in helper methods dotnet_diagnostic.S3966.severity = none # Dispose twice to test implementation + +# Special rules for example projects +[*Examples/**] +dotnet_diagnostic.SA1123.severity = none # We can have regions inside methods +dotnet_diagnostic.SA1515.severity = none # Allow comment lines after region +dotnet_diagnostic.S1481.severity = none # Unused variables diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 64d134b..d6b60fb 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -2,6 +2,7 @@ + diff --git a/src/Ekona.Examples/Cartridge.cs b/src/Ekona.Examples/Cartridge.cs new file mode 100644 index 0000000..9eba53d --- /dev/null +++ b/src/Ekona.Examples/Cartridge.cs @@ -0,0 +1,40 @@ +using SceneGate.Ekona.Containers.Rom; +using SceneGate.Ekona.Security; +using Texim.Animations; +using Texim.Formats; +using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; + +namespace SceneGate.Ekona.Examples; + +public class Cartridge +{ + public void DecryptEncryptArm9(Node rom, DsiKeyStore keyStore) + { + #region DecryptEncryptArm9 + var programInfo = Navigator.SearchNode(rom, "system/info").GetFormatAs(); + var key1Encryption = new NitroKey1Encryption(programInfo.GameCode, keyStore); + + DataStream arm9 = Navigator.SearchNode(rom, "system/arm9").Stream!; + bool isEncrypted = key1Encryption.HasEncryptedArm9(arm9); + if (isEncrypted) { + DataStream decryptedArm9 = key1Encryption.DecryptArm9(arm9); + } else { + DataStream encryptedArm9 = key1Encryption.EncryptArm9(arm9); + } + #endregion + } + + public void ExportGif(Node rom) + { + #region ExportIconGif + Node animated = Navigator.SearchNode(rom, "system/banner/animated"); + + AnimatedFullImage animatedImage = (AnimatedFullImage)ConvertFormat.With(animated.Format); + using var gifData = (BinaryFormat)ConvertFormat.With(animatedImage); + + gifData.Stream.WriteTo("icon.gif"); + #endregion + } +} diff --git a/src/Ekona.Examples/Ekona.Examples.csproj b/src/Ekona.Examples/Ekona.Examples.csproj new file mode 100644 index 0000000..bdc1dd9 --- /dev/null +++ b/src/Ekona.Examples/Ekona.Examples.csproj @@ -0,0 +1,20 @@ + + + + SceneGate.Ekona.Examples + SceneGate.Ekona.Examples + Examples for the documentation. + net6.0 + + enable + disable + + + + + + + + + + diff --git a/src/Ekona.Examples/QuickStart.cs b/src/Ekona.Examples/QuickStart.cs new file mode 100644 index 0000000..cfd4703 --- /dev/null +++ b/src/Ekona.Examples/QuickStart.cs @@ -0,0 +1,115 @@ +// Copyright (c) 2022 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +using SceneGate.Ekona.Containers.Rom; +using Texim.Animations; +using Texim.Formats; +using Texim.Images; +using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; +using Yarhl.Media.Text; + +namespace SceneGate.Ekona.Examples; + +public class QuickStart +{ + public void OpenWriteGame(string gameFilePath) + { + #region OpenGame + // Create node from file with binary format. + Node game = NodeFactory.FromFile(gameFilePath, FileOpenMode.Read); + + // Use the `Binary2NitroRom` converter to convert the binary format + // into node containers (virtual file system tree). + game.TransformWith(); + + // And it's done! + // Now we can access to every game file. For instance, we can export one file + Node gameFile = game.Children["data"].Children["Items.dat"]; + gameFile.Stream.WriteTo("dump/items.dat"); + #endregion + + #region ModifyFile + // Read the file by converting from binary format into PO (translation format) + Node itemsFile = Navigator.SearchNode(game, "data/Items.dat"); + Po items = itemsFile.TransformWith().GetFormatAs(); + + // Let's modify the first entry + items.Entries[0].Translated = "Hello world!"; + + // Convert back from PO format into binary (write into a new memory stream) + itemsFile.TransformWith(); + #endregion + + #region WriteGame + game.TransformWith(); + game.Stream.WriteTo("output/new_game.nds"); + #endregion + } + + public void AccessHeaderInfo(string gameFilePath) + { + #region HeaderInfo + Node game = NodeFactory.FromFile(gameFilePath, FileOpenMode.Read) + .TransformWith(); + + ProgramInfo info = game.Children["system"].Children["info"] + .GetFormatAs(); + + Console.WriteLine($"Game title: {info.GameTitle} [{info.GameCode}]"); + #endregion + + #region BannerTitle + Banner bannerInfo = Navigator.SearchNode(game, "system/banner/info").GetFormatAs(); + Console.WriteLine($"Japanese title: {bannerInfo.JapaneseTitle}"); + Console.WriteLine($"English title: {bannerInfo.EnglishTitle}"); + #endregion + + #region ExportIcon + IndexedPaletteImage icon = Navigator.SearchNode(game, "system/banner/icon") + .GetFormatAs(); + + // Using Texim converters, create a PNG image + IndexedImage2Bitmap bitmapConverter = new IndexedImage2Bitmap(); + bitmapConverter.Initialize(new IndexedImageBitmapParams { Palettes = icon }); + + using BinaryFormat binaryPng = bitmapConverter.Convert(icon); + binaryPng.Stream.WriteTo("dump/icon.png"); + + // For DSi-enhanced games we can export its animated icon as GIF + if (bannerInfo.SupportAnimatedIcon) { + Node animatedNode = Navigator.SearchNode(game, "system/banner/animated"); + var animatedImage = ConvertFormat.With(animatedNode.Format); + using var binaryGif = (BinaryFormat)ConvertFormat.With(animatedImage); + binaryGif.Stream.WriteTo("dump/icon.gif"); + } + #endregion + } + + private sealed class BinaryItems2Po : IConverter + { + public Po Convert(IBinary source) => new Po(); + } + + private sealed class Po2BinaryItems : IConverter + { + public BinaryFormat Convert(Po source) => new BinaryFormat(); + } +} diff --git a/src/Ekona.sln b/src/Ekona.sln index 4690a63..5e611f0 100644 --- a/src/Ekona.sln +++ b/src/Ekona.sln @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ekona.PerformanceTests", "Ekona.PerformanceTests\Ekona.PerformanceTests.csproj", "{5808F29C-C802-4728-99A5-E1F299178002}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ekona.Examples", "Ekona.Examples\Ekona.Examples.csproj", "{E1BC44DB-CAA3-4C74-8A5A-7D536A53E09F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,6 +37,10 @@ Global {5808F29C-C802-4728-99A5-E1F299178002}.Debug|Any CPU.Build.0 = Debug|Any CPU {5808F29C-C802-4728-99A5-E1F299178002}.Release|Any CPU.ActiveCfg = Release|Any CPU {5808F29C-C802-4728-99A5-E1F299178002}.Release|Any CPU.Build.0 = Release|Any CPU + {E1BC44DB-CAA3-4C74-8A5A-7D536A53E09F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1BC44DB-CAA3-4C74-8A5A-7D536A53E09F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1BC44DB-CAA3-4C74-8A5A-7D536A53E09F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1BC44DB-CAA3-4C74-8A5A-7D536A53E09F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Ekona/Containers/Rom/Binary2NitroRom.cs b/src/Ekona/Containers/Rom/Binary2NitroRom.cs index a8b08c9..31e26ba 100644 --- a/src/Ekona/Containers/Rom/Binary2NitroRom.cs +++ b/src/Ekona/Containers/Rom/Binary2NitroRom.cs @@ -217,7 +217,7 @@ private void ReadProgramCodeParameters() reader.Stream.Position = header.SectionInfo.Arm9Offset + header.SectionInfo.Arm9Size; if (reader.ReadUInt32() == NitroRom.NitroCode) { programParams.ProgramParameterOffset = reader.ReadUInt32(); - programParams.ExtraHashesOffset = reader.ReadUInt32(); // TODO: investigate hashes + programParams.NitroOverlayHMacOffset = reader.ReadUInt32(); paramsOffset = programParams.ProgramParameterOffset; } diff --git a/src/Ekona/Containers/Rom/Binary2RomHeader.cs b/src/Ekona/Containers/Rom/Binary2RomHeader.cs index ec53054..17a5fd4 100644 --- a/src/Ekona/Containers/Rom/Binary2RomHeader.cs +++ b/src/Ekona/Containers/Rom/Binary2RomHeader.cs @@ -343,8 +343,8 @@ private void ReadDsFields(DataReader reader, RomHeader header) header.ProgramInfo.Arm7ParametersTableOffset = reader.ReadUInt32(); // Pos: 0x90 - header.ProgramInfo.NitroRegionEnd = reader.ReadUInt16(); - header.ProgramInfo.TwilightRegionStart = reader.ReadUInt16(); + header.SectionInfo.NitroRegionEnd = reader.ReadUInt16(); + header.SectionInfo.TwilightRegionStart = reader.ReadUInt16(); // Pos: 0xC0 reader.Stream.Position = 0xC0; diff --git a/src/Ekona/Containers/Rom/NitroProgramCodeParameters.cs b/src/Ekona/Containers/Rom/NitroProgramCodeParameters.cs index 2e99512..96eaa5a 100644 --- a/src/Ekona/Containers/Rom/NitroProgramCodeParameters.cs +++ b/src/Ekona/Containers/Rom/NitroProgramCodeParameters.cs @@ -33,9 +33,13 @@ public class NitroProgramCodeParameters public uint ProgramParameterOffset { get; set; } /// - /// Gets or sets the offset to an optional area in the program with additional hashes. + /// Gets or sets the offset inside the decompressed ARM9 to the list of HMAC-SHA1 + /// hashes of each compressed overlay (only DS games). /// - public uint ExtraHashesOffset { get; set; } + /// + /// The key is inside the ARM9 as well (`HMacKeyDSiGames`). + /// + public uint NitroOverlayHMacOffset { get; set; } /// /// Gets or sets the ITCM first block info offset. diff --git a/src/Ekona/Containers/Rom/NitroRom.cs b/src/Ekona/Containers/Rom/NitroRom.cs index d424ae1..2a75d63 100644 --- a/src/Ekona/Containers/Rom/NitroRom.cs +++ b/src/Ekona/Containers/Rom/NitroRom.cs @@ -34,12 +34,16 @@ namespace SceneGate.Ekona.Containers.Rom /// /system/banner/Program banner. /// /system/banner/infoProgram banner content. /// /system/banner/iconProgram icon. - /// /system/arm9Program execuable for ARM9 CPU. + /// /system/banner/animatedAnimated program icon. + /// /system/banner/animated/bitmapXBitmap X (0-7) for the animated icon. + /// /system/banner/animated/palettesPalettes (0-7) for the animated icon. + /// /system/banner/animated/animationAnimation icon information. + /// /system/arm9Program executable for ARM9 CPU. /// /system/overlays9Overlay libraries for ARM9 CPU. - /// /system/overlays9/overlay_0Overlay 0 for ARM9 CPU. + /// /system/overlays9/overlay_XOverlay X for ARM9 CPU. /// /system/arm7Program executable for ARM7 CPU. /// /system/overlays7Overlay libraries for ARM7 CPU. - /// /system/overlays7/overlay7_0Overlay 0 for ARM7 CPU. + /// /system/overlays7/overlay7_XOverlay X for ARM7 CPU. /// /dataProgram data files. /// /// diff --git a/src/Ekona/Containers/Rom/NitroRom2Binary.cs b/src/Ekona/Containers/Rom/NitroRom2Binary.cs index 31f02b3..a56787a 100644 --- a/src/Ekona/Containers/Rom/NitroRom2Binary.cs +++ b/src/Ekona/Containers/Rom/NitroRom2Binary.cs @@ -137,6 +137,8 @@ public BinaryFormat Convert(NodeContainerFormat source) if (programInfo.UnitCode != DeviceUnitKind.DS) { // Padding to start twilight section writer.WritePadding(0xFF, 0x100000); + sectionInfo.NitroRegionEnd = (ushort)(writer.Stream.Length / (512 * 1024)); + sectionInfo.TwilightRegionStart = (ushort)(writer.Stream.Length / (512 * 1024)); WriteTwilightPrograms(); sectionInfo.DigestTwilightOffset = sectionInfo.Arm9iOffset; @@ -361,7 +363,7 @@ private void WriteProgramCodeParameters() writer.Stream.Position = sectionInfo.Arm9Offset + sectionInfo.Arm9Size; writer.Write(NitroRom.NitroCode); writer.Write(programParams.ProgramParameterOffset); - writer.Write(programParams.ExtraHashesOffset); + writer.Write(programParams.NitroOverlayHMacOffset); paramsOffset = programParams.ProgramParameterOffset; } @@ -564,7 +566,7 @@ private void PopulateNodeInfo() }; if (!node.IsContainer && node.Stream is null) { - throw new FormatException("Child is not binary"); + throw new FormatException($"Child is not binary '{node.Path}'"); } bool hasId = node.Tags.ContainsKey("scenegate.ekona.id"); diff --git a/src/Ekona/Containers/Rom/ProgramInfo.cs b/src/Ekona/Containers/Rom/ProgramInfo.cs index 146d662..49bad16 100644 --- a/src/Ekona/Containers/Rom/ProgramInfo.cs +++ b/src/Ekona/Containers/Rom/ProgramInfo.cs @@ -181,16 +181,6 @@ public class ProgramInfo : IFormat /// public uint Arm7ParametersTableOffset { get; set; } - /// - /// Gets or sets the DS region end. - /// - public ushort NitroRegionEnd { get; set; } - - /// - /// Gets or sets the DSi region start. - /// - public ushort TwilightRegionStart { get; set; } - /// /// Gets or sets flags related to ROM features. /// diff --git a/src/Ekona/Containers/Rom/RomHeader2Binary.cs b/src/Ekona/Containers/Rom/RomHeader2Binary.cs index 71ff02f..72ce10e 100644 --- a/src/Ekona/Containers/Rom/RomHeader2Binary.cs +++ b/src/Ekona/Containers/Rom/RomHeader2Binary.cs @@ -274,8 +274,8 @@ private void WriteDsFields(DataWriter writer, RomHeader source) writer.Write(source.SectionInfo.HeaderSize); writer.Write(source.ProgramInfo.Arm9ParametersTableOffset); writer.Write(source.ProgramInfo.Arm7ParametersTableOffset); - writer.Write(source.ProgramInfo.NitroRegionEnd); - writer.Write(source.ProgramInfo.TwilightRegionStart); + writer.Write(source.SectionInfo.NitroRegionEnd); + writer.Write(source.SectionInfo.TwilightRegionStart); writer.WriteTimes(0, 0x2C); writer.Write(source.CopyrightLogo); diff --git a/src/Ekona/Containers/Rom/RomSectionInfo.cs b/src/Ekona/Containers/Rom/RomSectionInfo.cs index 4a81034..a0a2b9b 100644 --- a/src/Ekona/Containers/Rom/RomSectionInfo.cs +++ b/src/Ekona/Containers/Rom/RomSectionInfo.cs @@ -99,6 +99,16 @@ public class RomSectionInfo /// public uint HeaderSize { get; set; } + /// + /// Gets or sets the DS region end. + /// + public ushort NitroRegionEnd { get; set; } + + /// + /// Gets or sets the DSi region start. + /// + public ushort TwilightRegionStart { get; set; } + /// /// Gets or sets the offset for the ARM-9 program for DSi devices. /// diff --git a/src/Ekona/Containers/Rom/ScfgExtendedFeaturesArm7.cs b/src/Ekona/Containers/Rom/ScfgExtendedFeaturesArm7.cs index 03b80b9..b7540ef 100644 --- a/src/Ekona/Containers/Rom/ScfgExtendedFeaturesArm7.cs +++ b/src/Ekona/Containers/Rom/ScfgExtendedFeaturesArm7.cs @@ -27,7 +27,7 @@ namespace SceneGate.Ekona.Containers.Rom; /// [Flags] [SuppressMessage("", "S4070", Justification = "False positive")] -public enum ScfgExtendedFeaturesArm7 +public enum ScfgExtendedFeaturesArm7 : uint { /// /// No setting set. @@ -62,5 +62,5 @@ public enum ScfgExtendedFeaturesArm7 /// /// Allow access to the SCFG/MBK registers. /// - AccessScfgMbkRegisters = 1 << 31, + AccessScfgMbkRegisters = 1u << 31, } diff --git a/src/Ekona/Containers/Rom/TwilightAccessControl.cs b/src/Ekona/Containers/Rom/TwilightAccessControl.cs index 9433321..af156e8 100644 --- a/src/Ekona/Containers/Rom/TwilightAccessControl.cs +++ b/src/Ekona/Containers/Rom/TwilightAccessControl.cs @@ -27,7 +27,7 @@ namespace SceneGate.Ekona.Containers.Rom; /// [Flags] [SuppressMessage("", "S4070", Justification = "False positive")] -public enum TwilightAccessControl +public enum TwilightAccessControl : uint { /// /// No access control set. @@ -122,5 +122,5 @@ public enum TwilightAccessControl /// /// Debugger common client key -> 0x0380F000 = 0x03FFC600 + 0x10. /// - DebuggerCommonClientKey = 1 << 31, + DebuggerCommonClientKey = 1u << 31, } diff --git a/src/Ekona/Security/DsiKeyStore.cs b/src/Ekona/Security/DsiKeyStore.cs index 4e734b4..10f524a 100644 --- a/src/Ekona/Security/DsiKeyStore.cs +++ b/src/Ekona/Security/DsiKeyStore.cs @@ -29,7 +29,7 @@ public class DsiKeyStore /// /// /// The key can be found in the DS ARM7 BIOS from 0x30 to 0x1077. - /// It starts with `99 D5 20 5F`. + /// It starts with `99 D5 20 5F` and has 0x1048 bytes. /// public byte[] BlowfishDsKey { get; set; } @@ -39,7 +39,7 @@ public class DsiKeyStore /// /// The key can be found in the DSi launcher application ARM9. /// For instance, at position 0270EC90h of the RAM. - /// It starts with `61 BD DD 72`. + /// It starts with `61 BD DD 72` and has 0x40 bytes. /// public byte[] HMacKeyWhitelist12 { get; set; } @@ -49,16 +49,18 @@ public class DsiKeyStore /// /// The key can be found in the DSi launcher application ARM9. /// For instance, at position 0270ECD0h of the RAM. - /// It starts with `85 29 48 F3`. + /// It starts with `85 29 48 F3` and has 0x40 bytes. /// public byte[] HMacKeyWhitelist34 { get; set; } /// - /// Gets or sets the HMAC key used in DSi games (like banner HMAC). + /// Gets or sets the HMAC key used in DSi games (like banner HMAC) but also + /// in some DS games to verify the (compressed) overlays in download play load mode. /// /// - /// The key can be found inside the ARM9 of most DSi games and in the launcher. - /// It starts with `21 06 C0 DE`. + /// The key can be found inside the ARM9 of most DS and DSi games and in the launcher. + /// It has 0x40 bytes and starts with `21 06 C0 DE BA 98`. + /// It seems to start with the "nitrocode" token and it would be the second one in the ARM9. /// public byte[] HMacKeyDSiGames { get; set; } @@ -67,7 +69,7 @@ public class DsiKeyStore /// /// /// The data can be found in the ARM9 BIOS of the DSi at position 0x8974. - /// It starts with `95 6F 79 0D`. + /// It starts with `95 6F 79 0D` and has 0x80 bytes. /// public byte[] PublicModulusRetailGames { get; set; } } diff --git a/src/Ekona/Security/Modcrypt.cs b/src/Ekona/Security/Modcrypt.cs index dfbf01c..4473412 100644 --- a/src/Ekona/Security/Modcrypt.cs +++ b/src/Ekona/Security/Modcrypt.cs @@ -117,7 +117,7 @@ public DataStream Transform(Stream input) private static Modcrypt CreateDebug(ProgramInfo programInfo, int area) { byte[] iv = (area == 1) - ? programInfo.DsiInfo.Arm9Mac.Hash[0..16] + ? programInfo.DsiInfo.Arm9SecureMac.Hash[0..16] : programInfo.DsiInfo.Arm7Mac.Hash[0..16]; // Debug KEY[0..F]: First 16 bytes of the header diff --git a/src/Ekona/Security/TwilightHMacGenerator.cs b/src/Ekona/Security/TwilightHMacGenerator.cs index 8aa94a9..d33dc31 100644 --- a/src/Ekona/Security/TwilightHMacGenerator.cs +++ b/src/Ekona/Security/TwilightHMacGenerator.cs @@ -112,7 +112,7 @@ public byte[] GeneratePhase2Hmac(Stream romStream, RomSectionInfo sectionInfo) _ = generator.TransformBlock(overlaysFat, 0, overlaysFat.Length, null, 0); // Finally hash each overlay in an fun, unnecessary and complex way. - // Hash each overlay including its hash, without exceeding a maximum size per file. + // Hash each overlay including its padding, without exceeding a maximum size per file. // The maximum file size to hash is changing depending how many bytes are hashed already. int blocksRead = 0; for (int i = 0; i < numOverlays; i++) { diff --git a/src/Ekona/Security/TwilightSigner.cs b/src/Ekona/Security/TwilightSigner.cs index 06bf3b9..5c94375 100644 --- a/src/Ekona/Security/TwilightSigner.cs +++ b/src/Ekona/Security/TwilightSigner.cs @@ -75,7 +75,7 @@ public HashStatus VerifySignature(byte[] signature, Stream romStream) // is the raw hash (with PKCS #7 1.5 padding). It is NOT encoded with ASN.1 // as specified in PKCS #1. // That's why we do manually comparison: - // 1. Decript signature with RSA public key + // 1. Decrypt signature with RSA public key var rsaCipher = CipherUtilities.GetCipher("RSA/NONE/PKCS1Padding"); rsaCipher.Init(false, pubKey); byte[] expectedHash = rsaCipher.DoFinal(signature);