NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsSecureArea.cs
1using System.Buffers.Binary;
2using System.Text;
3
4namespace NdsForge;
5
10public static class NdsSecureArea
11{
13 public const int Offset = 0x4000;
15 public const int ByteLength = 0x4000;
16
21 public static NdsSecureAreaInspection Inspect(NdsImage image, NdsKey1KeyTable? keyTable = null)
22 {
23 ArgumentNullException.ThrowIfNull(image);
24 if (image.Header.Arm9.Data.Offset < Offset)
25 {
26 return new(NdsSecureAreaState.Absent, default, image.Header.SecureAreaCrc, null);
27 }
28
29 var region = new NdsRegion(Offset, ByteLength);
30 if (image.Length < region.End)
31 {
32 return new(NdsSecureAreaState.Malformed, region, image.Header.SecureAreaCrc, null);
33 }
34
35 var data = new byte[ByteLength];
36 using Stream stream = image.OpenRead(region);
37 stream.ReadExactly(data);
38 return Inspect(data, image.Header.GameCode, image.Header.SecureAreaCrc, keyTable);
39 }
40
48 ReadOnlySpan<byte> area,
49 string gameCode,
50 ushort storedCrc,
51 NdsKey1KeyTable? keyTable = null)
52 {
53 ValidateArea(area);
54 uint first = BinaryPrimitives.ReadUInt32LittleEndian(area);
55 uint second = BinaryPrimitives.ReadUInt32LittleEndian(area[4..]);
56 var region = new NdsRegion(Offset, ByteLength);
57 if (first == 0 && second == 0)
58 {
59 return new(NdsSecureAreaState.Multiboot, region, storedCrc, NdsChecksums.ComputeCrc16(area));
60 }
61
62 if (first == NdsKey1Cipher.DestroyedId && second == NdsKey1Cipher.DestroyedId)
63 {
64 ushort? calculated = keyTable is null
65 ? null
66 : NdsChecksums.ComputeCrc16(NdsKey1Cipher.Encrypt(area, ParseGameCode(gameCode), keyTable));
67 return new(NdsSecureAreaState.Decrypted, region, storedCrc, calculated);
68 }
69
70 if (keyTable is not null)
71 {
72 try
73 {
74 _ = NdsKey1Cipher.Decrypt(area, ParseGameCode(gameCode), keyTable);
75 return new(
76 NdsSecureAreaState.Encrypted,
77 region,
78 storedCrc,
80 }
81 catch (InvalidDataException)
82 {
83 // Failed identifier recovery is an inspection result rather than an exceptional control path.
84 }
85 }
86
87 return new(
88 NdsSecureAreaState.Unrecognized,
89 region,
90 storedCrc,
91 keyTable is null ? null : NdsChecksums.ComputeCrc16(area));
92 }
93
99 public static byte[] Encrypt(ReadOnlySpan<byte> area, string gameCode, NdsKey1KeyTable keyTable)
100 {
101 ArgumentNullException.ThrowIfNull(keyTable);
102 return NdsKey1Cipher.Encrypt(area, ParseGameCode(gameCode), keyTable);
103 }
104
110 public static byte[] Decrypt(ReadOnlySpan<byte> area, string gameCode, NdsKey1KeyTable keyTable)
111 {
112 ArgumentNullException.ThrowIfNull(keyTable);
113 return NdsKey1Cipher.Decrypt(area, ParseGameCode(gameCode), keyTable);
114 }
115
127 public static ValueTask EncryptAsync(
128 Stream source,
129 Stream destination,
130 string gameCode,
131 NdsKey1KeyTable keyTable,
132 CancellationToken cancellationToken = default) =>
133 TransformAsync(source, destination, gameCode, keyTable, encrypt: true, cancellationToken);
134
145 public static ValueTask DecryptAsync(
146 Stream source,
147 Stream destination,
148 string gameCode,
149 NdsKey1KeyTable keyTable,
150 CancellationToken cancellationToken = default) =>
151 TransformAsync(source, destination, gameCode, keyTable, encrypt: false, cancellationToken);
152
160 private static async ValueTask TransformAsync(
161 Stream source,
162 Stream destination,
163 string gameCode,
164 NdsKey1KeyTable keyTable,
165 bool encrypt,
166 CancellationToken cancellationToken)
167 {
168 ArgumentNullException.ThrowIfNull(source);
169 ArgumentNullException.ThrowIfNull(destination);
170 if (!source.CanRead)
171 {
172 throw new ArgumentException("The secure-area source must be readable.", nameof(source));
173 }
174
175 if (!destination.CanWrite)
176 {
177 throw new ArgumentException("The secure-area destination must be writable.", nameof(destination));
178 }
179
180 var input = new byte[ByteLength];
181 await source.ReadExactlyAsync(input, cancellationToken).ConfigureAwait(false);
182 byte[] output = encrypt
183 ? Encrypt(input, gameCode, keyTable)
184 : Decrypt(input, gameCode, keyTable);
185 await destination.WriteAsync(output, cancellationToken).ConfigureAwait(false);
186 }
187
191 private static uint ParseGameCode(string gameCode)
192 {
193 ArgumentNullException.ThrowIfNull(gameCode);
194 if (gameCode.Length != 4 || gameCode.Any(static value => value is < ' ' or > '~'))
195 {
196 throw new ArgumentException("A KEY1 game code must contain exactly four printable ASCII characters.", nameof(gameCode));
197 }
198
199 Span<byte> bytes = stackalloc byte[4];
200 Encoding.ASCII.GetBytes(gameCode, bytes);
201 return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
202 }
203
205 private static void ValidateArea(ReadOnlySpan<byte> area)
206 {
207 if (area.Length != ByteLength)
208 {
209 throw new ArgumentException($"A secure area must contain exactly 0x{ByteLength:X} bytes.", nameof(area));
210 }
211 }
212}
Calculates checksums used by Nintendo DS image structures.
static ushort ComputeCrc16(ReadOnlySpan< byte > data, ushort seed=ushort.MaxValue)
Calculates the CRC-16 used by Nintendo DS headers and banners.
NdsProgram Arm9
Locates the primary processor payload and its entry/load addresses from header offsets 0x20-0x2F.
Definition NdsHeader.cs:86
string GameCode
Decodes the four-byte product code used for title identity, region suffixes, and encryption derivatio...
Definition NdsHeader.cs:56
ushort SecureAreaCrc
Contains the header's CRC16 for the secure area; interpretation requires secure-area encryption state...
Definition NdsHeader.cs:122
Provides structured, random-access inspection of a Nintendo DS-family image.
Definition NdsImage.cs:5
long Length
Reports physical source bytes, which may exceed the header's used-ROM size because cartridges are cap...
Definition NdsImage.cs:50
NdsHeader Header
Preserves both typed DS/DSi fields and the raw bytes required for checksums and lossless edits.
Definition NdsImage.cs:35
Stream OpenRead(NdsRegion region)
Opens a read-only stream over a validated image region.
Definition NdsImage.cs:98
Holds the 18 round words and four 256-word substitution boxes consumed by the DS KEY1 algorithm....
Identifies a bounded range of bytes in an image.
Definition NdsRegion.cs:11
Reports secure-area presence, encryption state, and checksum evidence without modifying image bytes.
Provides pure inspection and KEY1 transformations for the conventional 0x4000-0x7FFF cartridge interv...
static byte[] Encrypt(ReadOnlySpan< byte > area, string gameCode, NdsKey1KeyTable keyTable)
Encrypts a decrypted interval for the supplied product identity and returns an independent copy.
static ValueTask DecryptAsync(Stream source, Stream destination, string gameCode, NdsKey1KeyTable keyTable, CancellationToken cancellationToken=default)
Reads one encrypted interval and emits a verified decrypted copy without closing caller-owned streams...
static byte[] Decrypt(ReadOnlySpan< byte > area, string gameCode, NdsKey1KeyTable keyTable)
Decrypts a key-verifiable interval and replaces its secure identifier with conventional destroyed mar...
const int ByteLength
CRC-covered interval length; KEY1 itself transforms only the first 0x800 bytes.
static NdsSecureAreaInspection Inspect(NdsImage image, NdsKey1KeyTable? keyTable=null)
Inspects a loaded image and verifies encrypted state or CRC only when enough explicit key material ex...
static NdsSecureAreaInspection Inspect(ReadOnlySpan< byte > area, string gameCode, ushort storedCrc, NdsKey1KeyTable? keyTable=null)
Classifies one isolated interval using caller-supplied identity and stored checksum context.
const int Offset
Absolute image offset at which the cartridge security interval begins.
static ValueTask EncryptAsync(Stream source, Stream destination, string gameCode, NdsKey1KeyTable keyTable, CancellationToken cancellationToken=default)
Reads one interval at the source's current position, encrypts it, and writes exactly 16 KiB without c...
NdsSecureAreaState
Classifies the cartridge security interval without conflating encrypted bytes with malformed metadata...