First attempt

Currently DateTime is incorrect, Still trying to figure out application data.
This commit is contained in:
Jacobwasbeast 2024-12-03 21:50:33 -06:00
parent 08b7257be5
commit 6b155f5fbe
10 changed files with 469 additions and 3 deletions

View file

@ -16,6 +16,7 @@ using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemA
using Ryujinx.HLE.HOS.Services.Apm;
using Ryujinx.HLE.HOS.Services.Caps;
using Ryujinx.HLE.HOS.Services.Mii;
using Ryujinx.HLE.HOS.Services.Nfc.Bin;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using Ryujinx.HLE.HOS.Services.Nv;
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
@ -344,6 +345,17 @@ namespace Ryujinx.HLE.HOS
NfpDevices[nfpDeviceId].UseRandomUuid = useRandomUuid;
}
}
public void ScanAmiiboFromBin(string path)
{
byte[] encryptedData = File.ReadAllBytes(path);
VirtualAmiiboFile newFile = AmiiboBinReader.ReadBinFile(encryptedData);
if (SearchingForAmiibo(out int nfpDeviceId))
{
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
NfpDevices[nfpDeviceId].AmiiboId = newFile.AmiiboId;
NfpDevices[nfpDeviceId].UseRandomUuid = false;
}
}
public bool SearchingForAmiibo(out int nfpDeviceId)
{

View file

@ -0,0 +1,233 @@
using Ryujinx.Common.Configuration;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using Ryujinx.HLE.HOS.Tamper;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
{
public class AmiiboBinReader
{
// Method to calculate BCC (XOR checksum) from UID bytes
private static byte CalculateBCC(byte[] uid, int startIdx)
{
return (byte)(uid[startIdx] ^ uid[startIdx + 1] ^ uid[startIdx + 2] ^ 0x88);
}
// Method to read and process a .bin file
public static VirtualAmiiboFile ReadBinFile(byte[] fileBytes)
{
string keyRetailBinPath = GetKeyRetailBinPath();
if (string.IsNullOrEmpty(keyRetailBinPath))
{
Console.WriteLine("Key retail bin path not found.");
return new VirtualAmiiboFile();
}
byte[] initialCounter = new byte[16];
// Ensure the file is long enough
if (fileBytes.Length < 128 * 4) // Each page is 4 bytes, total 512 bytes
{
Console.WriteLine("File is too short to process.");
return new VirtualAmiiboFile();
}
// Decrypt the Amiibo data
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
byte[] decryptedFileBytes = amiiboDecryptor.DecryptAmiiboData(fileBytes, initialCounter);
// Assuming the UID is stored in the first 7 bytes (NTAG215 UID length)
byte[] uid = new byte[7];
Array.Copy(fileBytes, 0, uid, 0, 7);
// Calculate BCC values
byte bcc0 = CalculateBCC(uid, 0); // BCC0 = UID0 ^ UID1 ^ UID2 ^ 0x88
byte bcc1 = CalculateBCC(uid, 3); // BCC1 = UID3 ^ UID4 ^ UID5 ^ 0x00
Console.WriteLine($"UID: {BitConverter.ToString(uid)}");
Console.WriteLine($"BCC0: 0x{bcc0:X2}, BCC1: 0x{bcc1:X2}");
// Initialize byte arrays for data extraction
byte[] nickNameBytes = new byte[20]; // Amiibo nickname is 20 bytes
byte[] titleId = new byte[8];
byte[] usedCharacter = new byte[2];
byte[] variation = new byte[2];
byte[] amiiboID = new byte[2];
byte[] setID = new byte[1];
byte[] initDate = new byte[2];
byte[] writeDate = new byte[2];
byte[] writeCounter = new byte[2];
byte formData = 0;
byte[] applicationAreas = new byte[212];
// Reading specific pages and parsing bytes
for (int page = 0; page < 128; page++) // NTAG215 has 128 pages
{
int pageStartIdx = page * 4; // Each page is 4 bytes
byte[] pageData = new byte[4];
bool isEncrypted = IsPageEncrypted(page);
byte[] sourceBytes = isEncrypted ? decryptedFileBytes : fileBytes;
Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
// Special handling for specific pages
switch (page)
{
case 0: // Page 0 (UID + BCC0)
Console.WriteLine("Page 0: UID and BCC0.");
break;
case 2: // Page 2 (BCC1 + Internal Value)
byte internalValue = pageData[1];
Console.WriteLine($"Page 2: BCC1 + Internal Value 0x{internalValue:X2} (Expected 0x48).");
break;
case 6:
// Bytes 0 and 1 are init date, bytes 2 and 3 are write date
Array.Copy(pageData, 0, initDate, 0, 2);
Array.Copy(pageData, 2, writeDate, 0, 2);
break;
case 8:
case 9:
case 10:
case 11:
case 12:
// Extract nickname bytes
int nickNameOffset = (page - 8) * 4;
Array.Copy(pageData, 0, nickNameBytes, nickNameOffset, 4);
break;
case 21:
// Bytes 0 and 1 are used character, bytes 2 and 3 are variation
Array.Copy(pageData, 0, usedCharacter, 0, 2);
Array.Copy(pageData, 2, variation, 0, 2);
break;
case 22:
// Bytes 0 and 1 are amiibo ID, byte 2 is set ID, byte 3 is form data
Array.Copy(pageData, 0, amiiboID, 0, 2);
setID[0] = pageData[2];
formData = pageData[3];
break;
case 64:
case 65:
// Extract title ID
int titleIdOffset = (page - 64) * 4;
Array.Copy(pageData, 0, titleId, titleIdOffset, 4);
break;
case 66:
// Bytes 0 and 1 are write counter
Array.Copy(pageData, 0, writeCounter, 0, 2);
break;
// Pages 76 to 127 are application areas
case >= 76 and <= 127:
int appAreaOffset = (page - 76) * 4;
Array.Copy(pageData, 0, applicationAreas, appAreaOffset, 4);
break;
}
}
// Debugging
string titleIdStr = BitConverter.ToString(titleId).Replace("-", "");
string usedCharacterStr = BitConverter.ToString(usedCharacter).Replace("-", "");
string variationStr = BitConverter.ToString(variation).Replace("-", "");
string amiiboIDStr = BitConverter.ToString(amiiboID).Replace("-", "");
string formDataStr = formData.ToString("X2");
string setIDStr = BitConverter.ToString(setID).Replace("-", "");
string nickName = Encoding.BigEndianUnicode.GetString(nickNameBytes).TrimEnd('\0');
string head = usedCharacterStr + variationStr;
string tail = amiiboIDStr + setIDStr + "02";
string finalID = head + tail;
string initDateStr = BitConverter.ToString(initDate).Replace("-", "");
string writeDateStr = BitConverter.ToString(writeDate).Replace("-", "");
Console.WriteLine($"Title ID: {titleIdStr}");
Console.WriteLine($"Head: {head}");
Console.WriteLine($"Tail: {tail}");
Console.WriteLine($"Used Character: {usedCharacterStr}");
Console.WriteLine($"Form Data: {formDataStr}");
Console.WriteLine($"Variation: {variationStr}");
Console.WriteLine($"Amiibo ID: {amiiboIDStr}");
Console.WriteLine($"Set ID: {setIDStr}");
Console.WriteLine($"Final ID: {finalID}");
Console.WriteLine($"Nickname: {nickName}");
Console.WriteLine($"Init Date: {initDateStr}");
Console.WriteLine($"Write Date: {writeDateStr}");
VirtualAmiiboFile virtualAmiiboFile = new VirtualAmiiboFile
{
FileVersion = 1,
TagUuid = uid,
AmiiboId = finalID
};
DateTime initDateTime = DateTimeFromBytes(initDate);
DateTime writeDateTime = DateTimeFromBytes(writeDate);
Console.WriteLine($"Parsed Init Date: {initDateTime}");
Console.WriteLine($"Parsed Write Date: {writeDateTime}");
virtualAmiiboFile.FirstWriteDate = initDateTime;
virtualAmiiboFile.LastWriteDate = writeDateTime;
virtualAmiiboFile.WriteCounter = BitConverter.ToUInt16(writeCounter, 0);
// Parse application areas
//List<VirtualAmiiboApplicationArea> applicationAreasList = ParseAmiiboData(applicationAreas);
List<VirtualAmiiboApplicationArea> applicationAreasList = new List<VirtualAmiiboApplicationArea>();
virtualAmiiboFile.ApplicationAreas = applicationAreasList;
// Save the virtual Amiibo file
VirtualAmiibo.SaveAmiiboFile(virtualAmiiboFile);
return virtualAmiiboFile;
}
private static string GetKeyRetailBinPath()
{
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
}
public static bool IsPageEncrypted(int page)
{
return (page >= 6 && page <= 9) || (page >= 43 && page <= 84);
}
public static DateTime DateTimeFromBytes(byte[] date)
{
if (date == null || date.Length != 2)
{
Console.WriteLine("Invalid date bytes.");
return DateTime.MinValue;
}
ushort value = BitConverter.ToUInt16(date, 0);
int day = value & 0x1F;
int month = (value >> 5) & 0x0F;
int year = (value >> 9) & 0x7F;
try
{
return new DateTime(2000 + year, month, day);
}
catch (ArgumentOutOfRangeException)
{
Console.WriteLine("Invalid date values extracted.");
return DateTime.MinValue;
}
}
public static List<VirtualAmiiboApplicationArea> ParseAmiiboData(byte[] decryptedData)
{
return JsonSerializer.Deserialize<List<VirtualAmiiboApplicationArea>>(decryptedData);
}
}
}

View file

@ -0,0 +1,113 @@
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.HLE.HOS.Services.Mii.Types;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
{
public class AmiiboDecrypter
{
public readonly byte[] _hmacKey; // HMAC key
public readonly byte[] _aesKey; // AES key
public AmiiboDecrypter(string keyRetailBinPath)
{
var keys = AmiiboMasterKey.FromCombinedBin(File.ReadAllBytes(keyRetailBinPath));
_hmacKey = keys.DataKey.HmacKey;
_aesKey = keys.DataKey.XorPad;
}
public byte[] DecryptAmiiboData(byte[] encryptedData, byte[] counter)
{
// Ensure the counter length matches the block size
if (counter.Length != 16)
{
throw new ArgumentException("Counter must be 16 bytes long for AES block size.");
}
byte[] decryptedData = new byte[encryptedData.Length];
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = _aesKey;
aesAlg.Mode = CipherMode.ECB; // Use ECB mode to handle the counter encryption
aesAlg.Padding = PaddingMode.None;
using (var encryptor = aesAlg.CreateEncryptor())
{
int blockSize = 16;
byte[] encryptedCounter = new byte[blockSize];
byte[] currentCounter = (byte[])counter.Clone();
for (int i = 0; i < encryptedData.Length; i += blockSize)
{
// Encrypt the current counter block
encryptor.TransformBlock(currentCounter, 0, blockSize, encryptedCounter, 0);
// XOR the encrypted counter with the ciphertext to get the decrypted data
for (int j = 0; j < blockSize && i + j < encryptedData.Length; j++)
{
decryptedData[i + j] = (byte)(encryptedData[i + j] ^ encryptedCounter[j]);
}
// Increment the counter for the next block
IncrementCounter(currentCounter);
}
}
}
return decryptedData;
}
public byte[] CalculateHMAC(byte[] data)
{
using (var hmac = new HMACSHA256(_hmacKey))
{
return hmac.ComputeHash(data);
}
}
public void IncrementCounter(byte[] counter)
{
for (int i = counter.Length - 1; i >= 0; i--)
{
if (++counter[i] != 0)
break; // Stop if no overflow
}
}
public DateTime ParseDate(byte[] data, int offset)
{
ushort year = BitConverter.ToUInt16(data, offset);
byte month = data[offset + 2];
byte day = data[offset + 3];
byte hour = data[offset + 4];
byte minute = data[offset + 5];
byte second = data[offset + 6];
return new DateTime(year, month, day, hour, minute, second);
}
public List<object> ParseApplicationAreas(byte[] data, int startOffset, int areaSize)
{
var areas = new List<object>();
for (int i = 0; i < 8; i++) // Assuming 8 areas
{
int offset = startOffset + (i * areaSize);
string applicationId = BitConverter.ToString(data[offset..(offset + 4)]).Replace("-", "");
byte[] areaData = data[(offset + 4)..(offset + areaSize)];
areas.Add(new VirtualAmiiboApplicationArea
{
ApplicationAreaId = uint.Parse(applicationId),
ApplicationArea = areaData
});
}
return areas;
}
}
}

View file

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
{
public class AmiiboMasterKey
{
private const int DataLength = 80;
private const int CombinedLength = 160;
public byte[] HmacKey { get; private set; } // 16 bytes
public byte[] TypeString { get; private set; } // 14 bytes
public byte Rfu { get; private set; } // 1 byte reserved
public byte MagicSize { get; private set; } // 1 byte
public byte[] MagicBytes { get; private set; } // 16 bytes
public byte[] XorPad { get; private set; } // 32 bytes
private AmiiboMasterKey(byte[] data)
{
if (data.Length != DataLength)
throw new ArgumentException($"Data is {data.Length} bytes (should be {DataLength}).");
// Unpack the data
HmacKey = data[..16];
TypeString = data[16..30];
Rfu = data[30];
MagicSize = data[31];
MagicBytes = data[32..48];
XorPad = data[48..];
}
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromSeparateBin(byte[] dataBin, byte[] tagBin)
{
var dataKey = new AmiiboMasterKey(dataBin);
var tagKey = new AmiiboMasterKey(tagBin);
return (dataKey, tagKey);
}
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromSeparateHex(string dataHex, string tagHex)
{
return FromSeparateBin(HexToBytes(dataHex), HexToBytes(tagHex));
}
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromCombinedBin(byte[] combinedBin)
{
if (combinedBin.Length != CombinedLength)
throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be {CombinedLength}).");
byte[] dataBin = combinedBin[..DataLength];
byte[] tagBin = combinedBin[DataLength..];
return FromSeparateBin(dataBin, tagBin);
}
private static byte[] HexToBytes(string hex)
{
int length = hex.Length / 2;
byte[] bytes = new byte[length];
for (int i = 0; i < length; i++)
{
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return bytes;
}
}
}

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager
{
struct VirtualAmiiboFile
public struct VirtualAmiiboFile
{
public uint FileVersion { get; set; }
public byte[] TagUuid { get; set; }
@ -15,7 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager
public List<VirtualAmiiboApplicationArea> ApplicationAreas { get; set; }
}
struct VirtualAmiiboApplicationArea
public struct VirtualAmiiboApplicationArea
{
public uint ApplicationAreaId { get; set; }
public byte[] ApplicationArea { get; set; }

View file

@ -204,7 +204,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
return virtualAmiiboFile;
}
private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile)
public static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile)
{
string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json");
JsonHelper.SerializeToFile(filePath, virtualAmiiboFile, _serializerContext.VirtualAmiiboFile);

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Actions",
"MenuBarOptionsSimulateWakeUpMessage": "Simulate Wake-up message",
"MenuBarActionsScanAmiibo": "Scan An Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Tools",
"MenuBarToolsInstallFirmware": "Install Firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP",

View file

@ -28,12 +28,14 @@ using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Nfc.Bin;
using Ryujinx.HLE.UI;
using Ryujinx.Input.HLE;
using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using Silk.NET.Vulkan;
using SkiaSharp;
using System;
using System.Collections.Generic;
@ -2059,6 +2061,32 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
}
public async Task OpenBinFile()
{
if (!IsAmiiboRequested)
return;
if (AppHost.Device.System.SearchingForAmiibo(out int deviceId))
{
var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
AllowMultiple = false,
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
{
Patterns = new[] { "*.bin" },
}
}
});
if (result.Count > 0)
{
AppHost.Device.System.ScanAmiiboFromBin(result[0].Path.LocalPath);
}
}
}
public void ToggleFullscreen()
{

View file

@ -241,6 +241,13 @@
Icon="{ext:Icon mdi-cube-scan}"
InputGesture="Ctrl + A"
IsEnabled="{Binding IsAmiiboRequested}" />
<MenuItem
Name="ScanAmiiboMenuItemFromBin"
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
Click="OpenBinFile"
Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
Icon="{ext:Icon mdi-cube-scan}"
IsEnabled="{Binding IsAmiiboRequested}" />
<MenuItem
Command="{Binding TakeScreenshot}"
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"

View file

@ -141,6 +141,9 @@ namespace Ryujinx.Ava.UI.Views.Main
public async void OpenAmiiboWindow(object sender, RoutedEventArgs e)
=> await ViewModel.OpenAmiiboWindow();
public async void OpenBinFile(object sender, RoutedEventArgs e)
=> await ViewModel.OpenBinFile();
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
{
if (!ViewModel.IsGameRunning)