NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsDsiIntegrityValidator.cs
1using System.Security.Cryptography;
2
3namespace NdsForge;
4
6internal static class NdsDsiIntegrityValidator
7{
12 public static void Validate(
13 NdsImage image,
14 List<NdsDiagnostic> diagnostics,
15 NdsValidationOptions options)
16 {
17 NdsDsiHeader? dsi = image.Header.Dsi;
18 if (dsi is null)
19 {
20 return;
21 }
22
23 if (dsi.TotalImageSize != 0 &&
24 (dsi.TotalImageSize > image.Length || dsi.TotalImageSize < image.Header.UsedImageSize))
25 {
26 diagnostics.Add(new(
27 "NDS1301",
29 $"The DSi total image size 0x{dsi.TotalImageSize:X} is inconsistent with the physical or common content size."));
30 }
31
32 ValidateOptionalRegion(image, diagnostics, "NDS1302", "first modcrypt area", dsi.ModcryptArea1);
33 ValidateOptionalRegion(image, diagnostics, "NDS1303", "second modcrypt area", dsi.ModcryptArea2);
34 bool digestMetadataValid = ValidateDigestMetadata(image, diagnostics, dsi, options);
35 if (!options.DsiHmacKey.IsEmpty)
36 {
37 ValidateHmacs(image, diagnostics, dsi, options.DsiHmacKey.Span);
38 if (digestMetadataValid && !dsi.SectorHashTable.IsEmpty)
39 {
40 ValidateDigestHierarchy(image, diagnostics, dsi, options, options.DsiHmacKey.Span);
41 }
42 }
43
44 if (options.ValidateDsiDevelopmentSignature)
45 {
46 ValidateDevelopmentSignature(image, diagnostics, dsi);
47 }
48
49 if (options.DsiRsaPublicKey is not null && !dsi.VerifyRsaSignature(options.DsiRsaPublicKey))
50 {
51 diagnostics.Add(new(
52 "NDS1321",
54 "The DSi header RSA-SHA1 signature does not match the caller-trusted public key.",
55 new(0xF80, 128)));
56 }
57 }
58
65 private static void ValidateOptionalRegion(
66 NdsImage image,
67 List<NdsDiagnostic> diagnostics,
68 string code,
69 string name,
70 NdsRegion region)
71 {
72 if (!region.IsEmpty && (region.Offset < 0 || region.Length < 0 || region.Offset > image.Length - region.Length))
73 {
74 diagnostics.Add(new(
75 code,
77 $"The DSi {name} at 0x{region.Offset:X}+0x{region.Length:X} is outside the image.",
78 region));
79 }
80 }
81
88 private static bool ValidateDigestMetadata(
89 NdsImage image,
90 List<NdsDiagnostic> diagnostics,
91 NdsDsiHeader dsi,
92 NdsValidationOptions options)
93 {
94 bool hasDigestMetadata = !dsi.NtrDigest.IsEmpty || !dsi.TwlDigest.IsEmpty ||
95 !dsi.SectorHashTable.IsEmpty || !dsi.BlockHashTable.IsEmpty;
96 if (!hasDigestMetadata)
97 {
98 return true;
99 }
100
101 bool valid = true;
102 if (dsi.DigestSectorSize is < 0x200 or > 16 * 1024 * 1024 ||
103 (dsi.DigestSectorSize & (dsi.DigestSectorSize - 1)) != 0 ||
104 dsi.DigestBlockSectorCount is 0 or > 65_536 ||
105 dsi.SectorHashTable.IsEmpty ||
106 dsi.BlockHashTable.IsEmpty)
107 {
108 diagnostics.Add(new(
109 "NDS1304",
111 "The DSi digest hierarchy requires power-of-two sectors, a nonzero block sector count, and both hash tables."));
112 valid = false;
113 }
114
115 ValidateOptionalRegion(image, diagnostics, "NDS1305", "sector hash table", dsi.SectorHashTable);
116 ValidateOptionalRegion(image, diagnostics, "NDS1306", "block hash table", dsi.BlockHashTable);
117 ValidateOptionalRegion(image, diagnostics, "NDS1307", "NTR digest content", dsi.NtrDigest);
118 ValidateOptionalRegion(image, diagnostics, "NDS1308", "TWL digest content", dsi.TwlDigest);
119 if (dsi.SectorHashTable.Length > options.MaxDsiDigestTableBytes ||
120 dsi.BlockHashTable.Length > options.MaxDsiDigestTableBytes)
121 {
122 diagnostics.Add(new(
123 "NDS1309",
125 "A DSi digest table exceeds the configured validation allocation limit."));
126 valid = false;
127 }
128
129 if (!valid || !IsWithin(image, dsi.NtrDigest) || !IsWithin(image, dsi.TwlDigest) ||
130 !IsWithin(image, dsi.SectorHashTable) || !IsWithin(image, dsi.BlockHashTable))
131 {
132 return false;
133 }
134
135 long sectorCount;
136 long expectedSectorBytes;
137 long blockCount;
138 long expectedBlockBytes;
139 try
140 {
141 sectorCount = checked(
142 DivideRoundUp(dsi.NtrDigest.Length, dsi.DigestSectorSize) +
143 DivideRoundUp(dsi.TwlDigest.Length, dsi.DigestSectorSize));
144 expectedSectorBytes = checked(sectorCount * 20);
145 blockCount = DivideRoundUp(sectorCount, dsi.DigestBlockSectorCount);
146 expectedBlockBytes = checked(blockCount * 20);
147 }
148 catch (OverflowException)
149 {
150 diagnostics.Add(new(
151 "NDS1316",
153 "The DSi digest hierarchy overflows supported sector or table counts."));
154 return false;
155 }
156
157 if (dsi.SectorHashTable.Length != expectedSectorBytes || dsi.BlockHashTable.Length != expectedBlockBytes)
158 {
159 diagnostics.Add(new(
160 "NDS1316",
162 $"DSi digest table lengths do not match {sectorCount} content sectors and {blockCount} hash blocks."));
163 return false;
164 }
165
166 return true;
167 }
168
175 private static void ValidateDigestHierarchy(
176 NdsImage image,
177 List<NdsDiagnostic> diagnostics,
178 NdsDsiHeader dsi,
179 NdsValidationOptions options,
180 ReadOnlySpan<byte> key)
181 {
182 byte[] sectorHashes = ReadRegion(image, dsi.SectorHashTable);
183 byte[] blockHashes = ReadRegion(image, dsi.BlockHashTable);
184 int failureCount = 0;
185 int sectorIndex = 0;
186 ValidateRegionSectors(
187 image,
188 diagnostics,
189 dsi.NtrDigest,
190 dsi.DigestSectorSize,
191 sectorHashes,
192 ref sectorIndex,
193 ref failureCount,
194 options.MaxDsiDigestFailures,
195 key);
196 ValidateRegionSectors(
197 image,
198 diagnostics,
199 dsi.TwlDigest,
200 dsi.DigestSectorSize,
201 sectorHashes,
202 ref sectorIndex,
203 ref failureCount,
204 options.MaxDsiDigestFailures,
205 key);
206
207 int blockInputSize = checked((int)dsi.DigestBlockSectorCount * 20);
208 for (int blockIndex = 0; blockIndex * blockInputSize < sectorHashes.Length; blockIndex++)
209 {
210 int inputOffset = checked(blockIndex * blockInputSize);
211 ReadOnlySpan<byte> input = sectorHashes.AsSpan(
212 inputOffset,
213 Math.Min(blockInputSize, sectorHashes.Length - inputOffset));
214#pragma warning disable CA5350 // DSi block-table verification is defined as HMAC-SHA1.
215 byte[] calculated = HMACSHA1.HashData(key, input);
216#pragma warning restore CA5350
217 if (!CryptographicOperations.FixedTimeEquals(blockHashes.AsSpan(blockIndex * 20, 20), calculated) &&
218 failureCount++ < options.MaxDsiDigestFailures)
219 {
220 diagnostics.Add(new(
221 "NDS1318",
223 $"DSi digest block {blockIndex} does not authenticate its sector-hash group.",
224 new(dsi.BlockHashTable.Offset + (blockIndex * 20L), 20)));
225 }
226 }
227
228#pragma warning disable CA5350 // The DSi digest master field is defined as HMAC-SHA1 over the block table.
229 byte[] master = HMACSHA1.HashData(key, blockHashes);
230#pragma warning restore CA5350
231 if (!CryptographicOperations.FixedTimeEquals(dsi.DigestMasterHmac.Span, master))
232 {
233 diagnostics.Add(new(
234 "NDS1319",
236 "The DSi digest master HMAC does not authenticate the block hash table.",
237 new(0x328, 20)));
238 }
239
240 if (failureCount > options.MaxDsiDigestFailures)
241 {
242 diagnostics.Add(new(
243 "NDS1320",
244 NdsDiagnosticSeverity.Warning,
245 $"Additional DSi digest mismatches were suppressed after {options.MaxDsiDigestFailures} findings."));
246 }
247 }
248
259 private static void ValidateRegionSectors(
260 NdsImage image,
261 List<NdsDiagnostic> diagnostics,
262 NdsRegion region,
263 uint sectorSize,
264 byte[] storedHashes,
265 ref int sectorIndex,
266 ref int failureCount,
267 int failureLimit,
268 ReadOnlySpan<byte> key)
269 {
270 long offset = 0;
271 while (offset < region.Length)
272 {
273 long length = Math.Min(sectorSize, region.Length - offset);
274 var sector = new NdsRegion(region.Offset + offset, length);
275 byte[] calculated = CalculateRegionHmac(image, sector, key);
276 if (!CryptographicOperations.FixedTimeEquals(storedHashes.AsSpan(sectorIndex * 20, 20), calculated) &&
277 failureCount++ < failureLimit)
278 {
279 diagnostics.Add(new(
280 "NDS1317",
282 $"DSi digest sector {sectorIndex} does not match its covered image bytes.",
283 sector));
284 }
285
286 sectorIndex++;
287 offset += length;
288 }
289 }
290
296 private static void ValidateHmacs(
297 NdsImage image,
298 List<NdsDiagnostic> diagnostics,
299 NdsDsiHeader dsi,
300 ReadOnlySpan<byte> key)
301 {
302 ValidateHmac(image, diagnostics, "NDS1310", "ARM9", image.Header.Arm9.Data, dsi.Arm9Hmac.Span, key);
303 ValidateHmac(image, diagnostics, "NDS1311", "ARM7", image.Header.Arm7.Data, dsi.Arm7Hmac.Span, key);
304 var banner = new NdsRegion(image.Header.BannerOffset, dsi.BannerSize);
305 ValidateHmac(image, diagnostics, "NDS1312", "Banner", banner, dsi.BannerHmac.Span, key);
306 ValidateHmac(image, diagnostics, "NDS1313", "ARM9i", image.Header.Arm9i!.Data, dsi.Arm9iHmac.Span, key);
307 ValidateHmac(image, diagnostics, "NDS1314", "ARM7i", image.Header.Arm7i!.Data, dsi.Arm7iHmac.Span, key);
308 }
309
318 private static void ValidateHmac(
319 NdsImage image,
320 List<NdsDiagnostic> diagnostics,
321 string code,
322 string name,
323 NdsRegion region,
324 ReadOnlySpan<byte> stored,
325 ReadOnlySpan<byte> key)
326 {
327 if (region.Offset < 0 || region.Length < 0 || region.Offset > image.Length - region.Length)
328 {
329 return;
330 }
331
332 byte[] calculated = CalculateRegionHmac(image, region, key);
333 if (!CryptographicOperations.FixedTimeEquals(stored, calculated))
334 {
335 diagnostics.Add(new(
336 code,
338 $"The DSi {name} HMAC-SHA1 does not match the supplied key.",
339 region));
340 }
341 }
342
348 private static byte[] CalculateRegionHmac(NdsImage image, NdsRegion region, ReadOnlySpan<byte> key)
349 {
350#pragma warning disable CA5350 // DSi authentication fields are specified as HMAC-SHA1; callers explicitly opt into key validation.
351 using IncrementalHash hash = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA1, key);
352#pragma warning restore CA5350
353 using Stream stream = image.OpenRead(region);
354 byte[] buffer = new byte[64 * 1024];
355 int read;
356 while ((read = stream.Read(buffer)) > 0)
357 {
358 hash.AppendData(buffer, 0, read);
359 }
360
361 return hash.GetHashAndReset();
362 }
363
368 private static byte[] ReadRegion(NdsImage image, NdsRegion region)
369 {
370 byte[] data = new byte[checked((int)region.Length)];
371 using Stream stream = image.OpenRead(region);
372 stream.ReadExactly(data);
373 return data;
374 }
375
380 private static bool IsWithin(NdsImage image, NdsRegion region) =>
381 region.Offset >= 0 && region.Length >= 0 && region.Offset <= image.Length - region.Length;
382
387 private static long DivideRoundUp(long value, uint divisor) => checked((value + divisor - 1) / divisor);
388
393 private static void ValidateDevelopmentSignature(
394 NdsImage image,
395 List<NdsDiagnostic> diagnostics,
396 NdsDsiHeader dsi)
397 {
398 ReadOnlySpan<byte> signature = dsi.RsaSignature.Span;
399 if (signature[0] != 0 || signature[1] != 1 || signature[0x6B] != 0)
400 {
401 return;
402 }
403
404#pragma warning disable CA5350 // The recognized development marker is defined as SHA-1 and is not treated as a secure signature.
405 byte[] calculated = SHA1.HashData(image.Header.RawData.Span[..0xE00]);
406#pragma warning restore CA5350
407 if (!CryptographicOperations.FixedTimeEquals(signature[0x6C..0x80], calculated))
408 {
409 diagnostics.Add(new(
410 "NDS1315",
412 "The no$gba DSi development marker does not match the finalized extended header.",
413 new(0xFEC, 20)));
414 }
415 }
416}
NdsDiagnosticSeverity
Indicates the impact of a validation finding.