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:
parent
c2b1e0110b
commit
4424a7103c
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue