Add support for loading in backup TAD files

Kind of silly usecase, but it's not too bad to implement anyways
This commit is contained in:
CasualPokePlayer 2025-08-06 03:58:50 -07:00
parent c2b1e0110b
commit 4424a7103c
2 changed files with 267 additions and 36 deletions

View File

@ -1,10 +1,12 @@
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@ -81,6 +83,9 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
return new(x, y);
}
private static readonly BigInteger GENERATOR_CONSTANT = BigInteger.Parse("0FFFEFB4E295902582A680F5F1A4F3E79", NumberStyles.HexNumber, CultureInfo.InvariantCulture);
private static readonly BigInteger U128_MAX = BigInteger.Parse("0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", NumberStyles.HexNumber, CultureInfo.InvariantCulture);
[CoreConstructor(VSystemID.Raw.NDS)]
public NDS(CoreLoadParameters<NDSSettings, NDSSyncSettings> lp)
: base(lp.Comm, new()
@ -106,7 +111,190 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
var roms = lp.Roms.Select(r => r.FileData).ToList();
DSiTitleId = GetDSiTitleId(roms[0]);
// make sure we have a valid header before doing any parsing
if (roms[0].Length < 0x1000)
{
throw new InvalidOperationException("ROM is too small to be a valid NDS ROM!");
}
ReadOnlySpan<byte> romHeader = roms[0].AsSpan(0, 0x1000);
ReadOnlySpan<byte> dsiWare = [ ], bios7i = [ ];
if (!IsRomValid(roms[0]))
{
// if the ROM isn't valid, this could potentially be a backup TAD the user is attempting to load
// backup TADs are DSiWare exported to the SD Card
// https://problemkaputt.de/gbatek.htm#dsisdmmcdsiwarefilesonexternalsdcardbinakatadfiles
bios7i = CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios7i"));
var fixTadKeyX = bios7i.Slice(0xB588, 0x10);
var fixTadKeyY = bios7i.Slice(0x8328, 0x10);
var varTadKeyY = bios7i.Slice(0x8318, 0x10);
// CCM is used to encrypt backup TAD files
// For purposes of decryption, this is just CTR really, as we can more or less ignore authentication
// CTR isn't implemented by C# AES of course... so have to reimplement it
// Note: Not a copy paste of N3DSHasher! DSi is weird and transforms each block in little endian rather than big endian
static void AesCtrTransform(Aes aes, byte[] iv, Span<byte> inputOutput)
{
// ECB encryptor is used for both CTR encryption and decryption
using var encryptor = aes.CreateEncryptor();
var blockSize = aes.BlockSize / 8;
var outputBlockBuffer = new byte[blockSize];
// mostly copied from tiny-AES-c (public domain)
for (int i = 0, bi = blockSize; i < inputOutput.Length; ++i, ++bi)
{
if (bi == blockSize)
{
encryptor.TransformBlock(iv, 0, iv.Length, outputBlockBuffer, 0);
for (bi = blockSize - 1; bi >= 0; --bi)
{
if (iv[bi] == 0xFF)
{
iv[bi] = 0;
continue;
}
++iv[bi];
break;
}
bi = 0;
}
inputOutput[i] ^= outputBlockBuffer[blockSize - 1 - bi];
}
}
static byte[] DeriveNormalKey(ReadOnlySpan<byte> keyX, ReadOnlySpan<byte> keyY)
{
static BigInteger LeftRot128(BigInteger v, int rot)
{
var l = (v << rot) & U128_MAX;
var r = v >> (128 - rot);
return l | r;
}
static BigInteger Add128(BigInteger v1, BigInteger v2)
=> (v1 + v2) & U128_MAX;
var keyBytes = new byte[17];
keyX.CopyTo(keyBytes);
var keyXBigInteger = new BigInteger(keyBytes);
keyY.CopyTo(keyBytes);
var keyYBigInteger = new BigInteger(keyBytes);
var normalKey = LeftRot128(Add128(keyXBigInteger ^ keyYBigInteger, GENERATOR_CONSTANT), 42);
var normalKeyBytes = normalKey.ToByteArray();
if (normalKeyBytes.Length > 17)
{
// this shoudn't ever happen
throw new InvalidOperationException();
}
Array.Resize(ref normalKeyBytes, 16);
Array.Reverse(normalKeyBytes);
return normalKeyBytes;
}
static void InitIv(Span<byte> iv, ReadOnlySpan<byte> nonce)
{
iv[0] = 0x02;
// ES block nonce
for (var i = 0; i < 12; i++)
{
iv[1 + i] = nonce[11 - i];
}
iv[13] = 0x00;
iv[14] = 0x00;
iv[15] = 0x01;
}
using var aes = Aes.Create();
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
aes.BlockSize = 128;
aes.KeySize = 128;
var iv = new byte[16];
// first decrypt the header (mainly to verify this is in fact a backup TAD)
aes.Key = DeriveNormalKey(fixTadKeyX, fixTadKeyY);
InitIv(iv, roms[0].AsSpan(0x4020 + 0xB4 + 0x11, 12));
AesCtrTransform(aes, iv, roms[0].AsSpan(0x4020, 0x30));
if (!roms[0].AsSpan(0x4020, 4).SequenceEqual("4ANT"u8))
{
// not a backup TAD, this is a garbage NDS anyways
throw new InvalidOperationException("Invalid ROM");
}
// these include the ES block footer
var tmdSize = BinaryPrimitives.ReadUInt32LittleEndian(roms[0].AsSpan(0x4020 + 0x28, 4));
var appSize = BinaryPrimitives.ReadUInt32LittleEndian(roms[0].AsSpan(0x4020 + 0x2C, 4));
if (appSize % 0x20020 < 0x20)
{
throw new InvalidOperationException("Invalid ROM");
}
// decrypt cert area now (to fetch the console unique id, which Nintendo mistakenly includes in here)
InitIv(iv, roms[0].AsSpan(0x40F4 + 0x440 + 0x11, 12));
AesCtrTransform(aes, iv, roms[0].AsSpan(0x40F4, 0x440));
var consoleIdStr = Encoding.ASCII.GetString(roms[0].AsSpan(0x40F4 + 0x38F, 16));
if (!ulong.TryParse(consoleIdStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var consoleId))
{
throw new InvalidOperationException("Failed to parse console ID in TAD TWL cert!");
}
Span<byte> varTadKeyX = stackalloc byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(varTadKeyX, 0x4E00004A);
BinaryPrimitives.WriteUInt32LittleEndian(varTadKeyX[4..], 0x4A00004E);
BinaryPrimitives.WriteUInt32LittleEndian(varTadKeyX[8..], (uint)(consoleId >> 32) ^ 0xC80C4B72);
BinaryPrimitives.WriteUInt32LittleEndian(varTadKeyX[12..], (uint)(consoleId & 0xFFFFFFFF));
// decrypt app area now
aes.Key = DeriveNormalKey(varTadKeyX, varTadKeyY);
var appStart = (int)(0x4554 + tmdSize);
var appEnd = appStart + (int)appSize;
var appOffset = 0;
var appDataOffset = 0;
while (appStart + appOffset < appEnd)
{
var esBlockFooterOffset = (int)Math.Min(0x20020, appSize - appOffset) - 0x20;
InitIv(iv, roms[0].AsSpan(appStart + appOffset + esBlockFooterOffset + 0x11, 12));
AesCtrTransform(aes, iv, roms[0].AsSpan(appStart + appOffset, esBlockFooterOffset));
appOffset += esBlockFooterOffset + 0x20;
appDataOffset += esBlockFooterOffset;
}
var appData = new byte[appDataOffset];
appOffset = 0;
appDataOffset = 0;
while (appStart + appOffset < appEnd)
{
var esBlockFooterOffset = (int)Math.Min(0x20020, appSize - appOffset) - 0x20;
roms[0].AsSpan(appStart + appOffset, esBlockFooterOffset).CopyTo(appData.AsSpan(appDataOffset));
appOffset += esBlockFooterOffset + 0x20;
appDataOffset += esBlockFooterOffset;
}
dsiWare = appData;
if (dsiWare.Length < 0x1000 || !IsRomValid(dsiWare))
{
throw new InvalidOperationException("Invalid ROM in TAD");
}
romHeader = dsiWare[..0x1000];
DSiTitleId = GetDSiTitleId(romHeader);
if (!IsDSiWare)
{
throw new InvalidOperationException("Backup TAD did not have DSiWare");
}
}
DSiTitleId = GetDSiTitleId(romHeader);
IsDSi |= IsDSiWare;
if (roms.Count > (IsDSi ? 1 : 2))
@ -203,17 +391,17 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
if (!_activeSyncSettings.UseRealBIOS)
{
var arm9RomOffset = BinaryPrimitives.ReadInt32LittleEndian(roms[0].AsSpan(0x20, 4));
var arm9RomOffset = BinaryPrimitives.ReadInt32LittleEndian(romHeader.Slice(0x20, 4));
if (arm9RomOffset is >= 0x4000 and < 0x8000)
{
// check if the user is using an encrypted rom
// if they are, they need to be using real bios files
var secureAreaId = BinaryPrimitives.ReadUInt64LittleEndian(roms[0].AsSpan(arm9RomOffset, 8));
var secureAreaId = BinaryPrimitives.ReadUInt64LittleEndian(romHeader.Slice(arm9RomOffset, 8));
_activeSyncSettings.UseRealBIOS = secureAreaId != 0xE7FFDEFF_E7FFDEFF;
}
}
byte[] bios9 = null, bios7 = null, firmware = null;
ReadOnlySpan<byte> bios9 = [ ], bios7 = [ ], firmware = [ ];
if (_activeSyncSettings.UseRealBIOS)
{
bios9 = CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios9"));
@ -228,19 +416,19 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
NDSFirmware.MaybeWarnIfBadFw(firmware, CoreComm.ShowMessage);
}
byte[] bios9i = null, bios7i = null, nand = null;
ReadOnlySpan<byte> bios9i = [ ], nand = [ ];
if (IsDSi)
{
bios9i = CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios9i"));
bios7i = CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios7i"));
nand = DecideNAND(CoreComm.CoreFileProvider, (DSiTitleId.Upper & ~0xFF) == 0x00030000, roms[0][0x1B0]);
bios7i = bios7i.IsEmpty ? CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios7i")) : bios7i;
nand = DecideNAND(CoreComm.CoreFileProvider, (DSiTitleId.Upper & ~0xFF) == 0x00030000, romHeader[0x1B0]);
}
byte[] ndsRom = null, gbaRom = null, dsiWare = null, tmd = null;
ReadOnlySpan<byte> ndsRom = [ ], gbaRom = [ ], tmd = [ ];
if (IsDSiWare)
{
tmd = GetTMDData(DSiTitleId.Full);
dsiWare = roms[0];
dsiWare = dsiWare.IsEmpty ? roms[0] : dsiWare;
}
else
{
@ -264,16 +452,16 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
LibMelonDS.ConsoleCreationArgs consoleCreationArgs;
consoleCreationArgs.NdsRomLength = ndsRom?.Length ?? 0;
consoleCreationArgs.GbaRomLength = gbaRom?.Length ?? 0;
consoleCreationArgs.Arm9BiosLength = bios9?.Length ?? 0;
consoleCreationArgs.Arm7BiosLength = bios7?.Length ?? 0;
consoleCreationArgs.FirmwareLength = firmware?.Length ?? 0;
consoleCreationArgs.Arm9iBiosLength = bios9i?.Length ?? 0;
consoleCreationArgs.Arm7iBiosLength = bios7i?.Length ?? 0;
consoleCreationArgs.NandLength = nand?.Length ?? 0;
consoleCreationArgs.DsiWareLength = dsiWare?.Length ?? 0;
consoleCreationArgs.TmdLength = tmd?.Length ?? 0;
consoleCreationArgs.NdsRomLength = ndsRom.Length;
consoleCreationArgs.GbaRomLength = gbaRom.Length;
consoleCreationArgs.Arm9BiosLength = bios9.Length;
consoleCreationArgs.Arm7BiosLength = bios7.Length;
consoleCreationArgs.FirmwareLength = firmware.Length;
consoleCreationArgs.Arm9iBiosLength = bios9i.Length;
consoleCreationArgs.Arm7iBiosLength = bios7i.Length;
consoleCreationArgs.NandLength = nand.Length;
consoleCreationArgs.DsiWareLength = dsiWare.Length;
consoleCreationArgs.TmdLength = tmd.Length;
consoleCreationArgs.DSi = IsDSi;
consoleCreationArgs.ClearNAND = _activeSyncSettings.ClearNAND || lp.DeterministicEmulationRequested;
@ -377,14 +565,47 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
}
}
private static (ulong Full, uint Upper, uint Lower) GetDSiTitleId(ReadOnlySpan<byte> file)
private static bool IsRomValid(ReadOnlySpan<byte> rom)
{
ulong titleId = 0;
for (var i = 0; i < 8; i++)
// check ARM7/ARM9 (and maybe ARM7i/ARM9i) binary offsets/sizes to see if they're sane
// if these are wildly wrong, the ROM may crash the core
var unitCode = rom[0x12];
var isDsiExclusive = (unitCode & 0x03) == 3;
var arm9RomOffset = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x20, 4));
var arm9Size = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x2C, 4));
var arm7RomOffset = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x30, 4));
var arm7Size = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x3C, 4));
if (arm9RomOffset > rom.Length ||
(arm9Size > 0x3BFE00 && !isDsiExclusive) ||
arm9RomOffset + arm9Size > rom.Length ||
arm7RomOffset > rom.Length ||
(arm7Size > 0x3BFE00 && !isDsiExclusive) ||
arm7RomOffset + arm7Size > rom.Length)
{
titleId <<= 8;
titleId |= file[0x237 - i];
return false;
}
if ((unitCode & 0x02) != 0)
{
var arm9iRomOffset = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x1C0, 4));
var arm9iSize = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x1CC, 4));
var arm7iRomOffset = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x1D0, 4));
var arm7iSize = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x1DC, 4));
if (arm9iRomOffset > rom.Length ||
arm9iRomOffset + arm9iSize > rom.Length ||
arm7iRomOffset > rom.Length ||
arm7iRomOffset + arm7iSize > rom.Length)
{
return false;
}
}
return true;
}
private static (ulong Full, uint Upper, uint Lower) GetDSiTitleId(ReadOnlySpan<byte> romHeader)
{
var titleId = BinaryPrimitives.ReadUInt64LittleEndian(romHeader.Slice(0x230, 8));
return (titleId, (uint)(titleId >> 32), (uint)(titleId & 0xFFFFFFFFU));
}

View File

@ -9,7 +9,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
// TODO: a lot of this has been removed as it's not really needed anymore (our c++ code forces correctness everywhere)
internal static class NDSFirmware
{
public static void MaybeWarnIfBadFw(byte[] fw, Action<string> warningCallback)
public static void MaybeWarnIfBadFw(ReadOnlySpan<byte> fw, Action<string> warningCallback)
{
if (fw[0x17C] != 0xFF)
{
@ -25,7 +25,8 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
}
[DllImport("libfwunpack", CallingConvention = CallingConvention.Cdecl)]
private static extern bool GetDecryptedFirmware(byte[] fw, int fwlen, out IntPtr decryptedFw, out int decryptedlen);
[return: MarshalAs(UnmanagedType.U1)]
private static extern bool GetDecryptedFirmware(IntPtr fw, int fwLen, out IntPtr decryptedFw, out int decryptedLen);
[DllImport("libfwunpack", CallingConvention = CallingConvention.Cdecl)]
private static extern void FreeDecryptedFirmware(IntPtr decryptedFw);
@ -46,21 +47,30 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
"BFBC33D996AA73A050F1951529327D5844461A00", // MACi DS Lite (Korean v5, 2006-11-09)
};
private static void CheckDecryptedCodeChecksum(byte[] fw, Action<string> warningCallback)
private static unsafe void CheckDecryptedCodeChecksum(ReadOnlySpan<byte> fw, Action<string> warningCallback)
{
if (!GetDecryptedFirmware(fw, fw.Length, out var decryptedfw, out var decrypedfwlen))
IntPtr decryptedFw;
int decrypedFwLen;
fixed (byte* fwPtr = fw)
{
warningCallback("Firmware could not be decryped for verification! This firmware might be not work!");
return;
if (!GetDecryptedFirmware((IntPtr)fwPtr, fw.Length, out decryptedFw, out decrypedFwLen))
{
warningCallback("Firmware could not be decryped for verification! This firmware might be not work!");
return;
}
}
var decryptedFirmware = new byte[decrypedfwlen];
Marshal.Copy(decryptedfw, decryptedFirmware, 0, decrypedfwlen);
FreeDecryptedFirmware(decryptedfw);
var hash = SHA1Checksum.ComputeDigestHex(decryptedFirmware);
if (!_goodHashes.Contains(hash))
try
{
warningCallback("Potentially bad firmware dump! Decrypted hash " + hash + " does not match known good dumps.");
var hash = SHA1Checksum.ComputeDigestHex(Util.UnsafeSpanFromPointer(decryptedFw, decrypedFwLen));
if (!_goodHashes.Contains(hash))
{
warningCallback($"Potentially bad firmware dump! Decrypted hash {hash} does not match known good dumps.");
}
}
finally
{
FreeDecryptedFirmware(decryptedFw);
}
}
}