NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsDsiDigestBuilder.cs
1using System.Security.Cryptography;
2
3namespace NdsForge;
4
6internal static class NdsDsiDigestBuilder
7{
18 public static async ValueTask<NdsDsiDigestBuildResult> BuildAsync(
19 Stream image,
20 NdsImageBuildLayout layout,
21 NdsDsiDigestOptions options,
22 ReadOnlyMemory<byte> key,
23 CancellationToken cancellationToken)
24 {
25 if (layout.SectorHashTable.Length > Array.MaxLength || layout.BlockHashTable.Length > Array.MaxLength)
26 {
27 throw new IOException("DSi digest tables are too large to materialize safely.");
28 }
29
30 byte[] sectorHashes = new byte[layout.SectorHashTable.Length];
31 int hashOffset = 0;
32 hashOffset = await AppendRegionSectorsAsync(
33 image,
34 layout.NtrDigest,
35 options.SectorSize,
36 key,
37 sectorHashes,
38 hashOffset,
39 cancellationToken).ConfigureAwait(false);
40 hashOffset = await AppendRegionSectorsAsync(
41 image,
42 layout.TwlDigest,
43 options.SectorSize,
44 key,
45 sectorHashes,
46 hashOffset,
47 cancellationToken).ConfigureAwait(false);
48 if (hashOffset != sectorHashes.Length)
49 {
50 throw new InvalidDataException("DSi sector hash count disagrees with the planned table length.");
51 }
52
53 byte[] blockHashes = BuildBlockHashes(sectorHashes, options.BlockSectorCount, key.Span);
54#pragma warning disable CA5350 // DSi digest tables mandate HMAC-SHA1; the key policy is explicit in the build recipe.
55 byte[] masterHmac = HMACSHA1.HashData(key.Span, blockHashes);
56#pragma warning restore CA5350
57 return new(sectorHashes, blockHashes, masterHmac);
58 }
59
69 private static async ValueTask<int> AppendRegionSectorsAsync(
70 Stream image,
71 NdsRegion region,
72 int sectorSize,
73 ReadOnlyMemory<byte> key,
74 byte[] destination,
75 int hashOffset,
76 CancellationToken cancellationToken)
77 {
78 byte[] buffer = new byte[Math.Min(sectorSize, 64 * 1024)];
79 long regionOffset = 0;
80 while (regionOffset < region.Length)
81 {
82#pragma warning disable CA5350 // DSi content sectors are specified as HMAC-SHA1 rather than a modern configurable algorithm.
83 using IncrementalHash hash = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA1, key.Span);
84#pragma warning restore CA5350
85 long sectorRemaining = Math.Min(sectorSize, region.Length - regionOffset);
86 image.Position = checked(region.Offset + regionOffset);
87 while (sectorRemaining > 0)
88 {
89 int count = (int)Math.Min(buffer.Length, sectorRemaining);
90 await image.ReadExactlyAsync(buffer.AsMemory(0, count), cancellationToken).ConfigureAwait(false);
91 hash.AppendData(buffer, 0, count);
92 sectorRemaining -= count;
93 regionOffset += count;
94 }
95
96 if (!hash.TryGetHashAndReset(destination.AsSpan(hashOffset, 20), out int written) || written != 20)
97 {
98 throw new CryptographicException("The platform HMAC provider did not produce a 20-byte SHA-1 result.");
99 }
100
101 hashOffset += 20;
102 }
103
104 return hashOffset;
105 }
106
112 private static byte[] BuildBlockHashes(
113 ReadOnlySpan<byte> sectorHashes,
114 int blockSectorCount,
115 ReadOnlySpan<byte> key)
116 {
117 int blockInputSize = checked(blockSectorCount * 20);
118 int blockCount = checked((sectorHashes.Length + blockInputSize - 1) / blockInputSize);
119 byte[] blockHashes = new byte[checked(blockCount * 20)];
120 for (int index = 0; index < blockCount; index++)
121 {
122 int offset = checked(index * blockInputSize);
123 ReadOnlySpan<byte> input = sectorHashes.Slice(offset, Math.Min(blockInputSize, sectorHashes.Length - offset));
124#pragma warning disable CA5350 // DSi block-table entries are specified as HMAC-SHA1.
125 HMACSHA1.HashData(key, input, blockHashes.AsSpan(index * 20, 20));
126#pragma warning restore CA5350
127 }
128
129 return blockHashes;
130 }
131}