mirror of
https://git.ryujinx.app/kenji-nx/ryujinx.git
synced 2025-12-17 16:37:04 +00:00
Adds the ability to read and write to amiibo bin files
This introduces the ability to read and write game data and model information from an Amiibo dump file (BIN format). Note that this functionality requires the presence of a key_retail.bin file. For the option to appear and function in the UI, ensure that the key_retail.bin file is located in the <RyujinxData>/system folder.
This commit is contained in:
parent
8547285aba
commit
78400132c0
29 changed files with 958 additions and 19 deletions
|
|
@ -16,6 +16,8 @@ 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.AmiiboDecryption;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
||||
using Ryujinx.HLE.HOS.Services.Nv;
|
||||
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
|
||||
|
|
@ -337,6 +339,11 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
public void ScanAmiibo(int nfpDeviceId, string amiiboId, bool useRandomUuid)
|
||||
{
|
||||
if (VirtualAmiibo.ApplicationBytes.Length > 0)
|
||||
{
|
||||
VirtualAmiibo.ApplicationBytes = new byte[0];
|
||||
VirtualAmiibo.InputBin = string.Empty;
|
||||
}
|
||||
if (NfpDevices[nfpDeviceId].State == NfpDeviceState.SearchingForTag)
|
||||
{
|
||||
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
|
||||
|
|
@ -344,6 +351,22 @@ namespace Ryujinx.HLE.HOS
|
|||
NfpDevices[nfpDeviceId].UseRandomUuid = useRandomUuid;
|
||||
}
|
||||
}
|
||||
public void ScanAmiiboFromBin(string path)
|
||||
{
|
||||
VirtualAmiibo.InputBin = path;
|
||||
if (VirtualAmiibo.ApplicationBytes.Length > 0)
|
||||
{
|
||||
VirtualAmiibo.ApplicationBytes = new byte[0];
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,340 @@
|
|||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
||||
{
|
||||
public class AmiiboBinReader
|
||||
{
|
||||
private static byte CalculateBCC0(byte[] uid)
|
||||
{
|
||||
return (byte)(uid[0] ^ uid[1] ^ uid[2] ^ 0x88);
|
||||
}
|
||||
|
||||
private static byte CalculateBCC1(byte[] uid)
|
||||
{
|
||||
return (byte)(uid[3] ^ uid[4] ^ uid[5] ^ uid[6]);
|
||||
}
|
||||
|
||||
public static VirtualAmiiboFile ReadBinFile(byte[] fileBytes)
|
||||
{
|
||||
string keyRetailBinPath = GetKeyRetailBinPath();
|
||||
if (string.IsNullOrEmpty(keyRetailBinPath))
|
||||
{
|
||||
return new VirtualAmiiboFile();
|
||||
}
|
||||
|
||||
byte[] initialCounter = new byte[16];
|
||||
|
||||
const int totalPages = 135;
|
||||
const int pageSize = 4;
|
||||
const int totalBytes = totalPages * pageSize;
|
||||
|
||||
if (fileBytes.Length < totalBytes)
|
||||
{
|
||||
return new VirtualAmiiboFile();
|
||||
}
|
||||
|
||||
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
|
||||
AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(fileBytes);
|
||||
|
||||
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[] appId = new byte[8];
|
||||
byte[] settingsBytes = new byte[2];
|
||||
byte formData = 0;
|
||||
byte[] applicationAreas = new byte[216];
|
||||
byte[] dataFull = amiiboDump.GetData();
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Data Full Length: {dataFull.Length}");
|
||||
byte[] uid = new byte[7];
|
||||
Array.Copy(dataFull, 0, uid, 0, 7);
|
||||
|
||||
byte bcc0 = CalculateBCC0(uid);
|
||||
byte bcc1 = CalculateBCC1(uid);
|
||||
LogDebugData(uid, bcc0, bcc1);
|
||||
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];
|
||||
byte[] sourceBytes = dataFull;
|
||||
Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
|
||||
// Special handling for specific pages
|
||||
switch (page)
|
||||
{
|
||||
case 0: // Page 0 (UID + BCC0)
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, "Page 0: UID and BCC0.");
|
||||
break;
|
||||
case 2: // Page 2 (BCC1 + Internal Value)
|
||||
byte internalValue = pageData[1];
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"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 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;
|
||||
}
|
||||
}
|
||||
|
||||
string usedCharacterStr = BitConverter.ToString(usedCharacter).Replace("-", "");
|
||||
string variationStr = BitConverter.ToString(variation).Replace("-", "");
|
||||
string amiiboIDStr = BitConverter.ToString(amiiboID).Replace("-", "");
|
||||
string setIDStr = BitConverter.ToString(setID).Replace("-", "");
|
||||
string head = usedCharacterStr + variationStr;
|
||||
string tail = amiiboIDStr + setIDStr + "02";
|
||||
string finalID = head + tail;
|
||||
|
||||
ushort settingsValue = BitConverter.ToUInt16(settingsBytes, 0);
|
||||
ushort initDateValue = BitConverter.ToUInt16(initDate, 0);
|
||||
ushort writeDateValue = BitConverter.ToUInt16(writeDate, 0);
|
||||
DateTime initDateTime = DateTimeFromTag(initDateValue);
|
||||
DateTime writeDateTime = DateTimeFromTag(writeDateValue);
|
||||
ushort writeCounterValue = BitConverter.ToUInt16(writeCounter, 0);
|
||||
string nickName = amiiboDump.AmiiboNickname;
|
||||
LogFinalData(titleId, appId, head, tail, finalID, nickName, initDateTime, writeDateTime, settingsValue, writeCounterValue, applicationAreas);
|
||||
|
||||
VirtualAmiiboFile virtualAmiiboFile = new VirtualAmiiboFile
|
||||
{
|
||||
FileVersion = 1,
|
||||
TagUuid = uid,
|
||||
AmiiboId = finalID,
|
||||
NickName = nickName,
|
||||
FirstWriteDate = initDateTime,
|
||||
LastWriteDate = writeDateTime,
|
||||
WriteCounter = writeCounterValue,
|
||||
};
|
||||
if (writeCounterValue > 0)
|
||||
{
|
||||
VirtualAmiibo.ApplicationBytes = applicationAreas;
|
||||
}
|
||||
VirtualAmiibo.NickName = nickName;
|
||||
return virtualAmiiboFile;
|
||||
}
|
||||
public static bool SaveBinFile(string inputFile, byte[] appData)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.ServiceNfp, "Saving bin file.");
|
||||
byte[] readBytes;
|
||||
try
|
||||
{
|
||||
readBytes = File.ReadAllBytes(inputFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, $"Error reading file: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
string keyRetailBinPath = GetKeyRetailBinPath();
|
||||
if (string.IsNullOrEmpty(keyRetailBinPath))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, "Key retail path is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (appData.Length != 216) // Ensure application area size is valid
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, "Invalid application data length. Expected 216 bytes.");
|
||||
return false;
|
||||
}
|
||||
|
||||
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
|
||||
AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(readBytes);
|
||||
|
||||
byte[] oldData = amiiboDump.GetData();
|
||||
if (oldData.Length != 540) // Verify the expected length for NTAG215 tags
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, "Invalid tag data length. Expected 540 bytes.");
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] newData = new byte[oldData.Length];
|
||||
Array.Copy(oldData, newData, oldData.Length);
|
||||
|
||||
// Replace application area with appData
|
||||
int appAreaOffset = 76 * 4; // Starting page (76) times 4 bytes per page
|
||||
Array.Copy(appData, 0, newData, appAreaOffset, appData.Length);
|
||||
|
||||
AmiiboDump encryptedDump = amiiboDecryptor.EncryptAmiiboDump(newData);
|
||||
byte[] encryptedData = encryptedDump.GetData();
|
||||
|
||||
if (encryptedData == null || encryptedData.Length != readBytes.Length)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, "Failed to encrypt data correctly.");
|
||||
return false;
|
||||
}
|
||||
inputFile = inputFile.Replace("_modified", string.Empty);
|
||||
// Save the encrypted data to file or return it for saving externally
|
||||
string outputFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_modified.bin");
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(outputFilePath, encryptedData);
|
||||
Logger.Info?.Print(LogClass.ServiceNfp, $"Modified Amiibo data saved to {outputFilePath}.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, $"Error saving file: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public static bool SaveBinFile(string inputFile, string newNickName)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.ServiceNfp, "Saving bin file.");
|
||||
byte[] readBytes;
|
||||
try
|
||||
{
|
||||
readBytes = File.ReadAllBytes(inputFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, $"Error reading file: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
string keyRetailBinPath = GetKeyRetailBinPath();
|
||||
if (string.IsNullOrEmpty(keyRetailBinPath))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, "Key retail path is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
|
||||
AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(readBytes);
|
||||
amiiboDump.AmiiboNickname = newNickName;
|
||||
byte[] oldData = amiiboDump.GetData();
|
||||
if (oldData.Length != 540) // Verify the expected length for NTAG215 tags
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, "Invalid tag data length. Expected 540 bytes.");
|
||||
return false;
|
||||
}
|
||||
byte[] encryptedData = amiiboDecryptor.EncryptAmiiboDump(oldData).GetData();
|
||||
|
||||
if (encryptedData == null || encryptedData.Length != readBytes.Length)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, "Failed to encrypt data correctly.");
|
||||
return false;
|
||||
}
|
||||
inputFile = inputFile.Replace("_modified", string.Empty);
|
||||
// Save the encrypted data to file or return it for saving externally
|
||||
string outputFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_modified.bin");
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(outputFilePath, encryptedData);
|
||||
Logger.Info?.Print(LogClass.ServiceNfp, $"Modified Amiibo data saved to {outputFilePath}.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceNfp, $"Error saving file: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
private static void LogDebugData(byte[] uid, byte bcc0, byte bcc1)
|
||||
{
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"UID: {BitConverter.ToString(uid)}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"BCC0: 0x{bcc0:X2}, BCC1: 0x{bcc1:X2}");
|
||||
}
|
||||
|
||||
private static void LogFinalData(byte[] titleId, byte[] appId, string head, string tail, string finalID, string nickName, DateTime initDateTime, DateTime writeDateTime, ushort settingsValue, ushort writeCounterValue, byte[] applicationAreas)
|
||||
{
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Title ID: 0x{BitConverter.ToString(titleId).Replace("-", "")}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Application Program ID: 0x{BitConverter.ToString(appId).Replace("-", "")}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Head: {head}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Tail: {tail}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Final ID: {finalID}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Nickname: {nickName}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Init Date: {initDateTime}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Write Date: {writeDateTime}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Settings: 0x{settingsValue:X4}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, $"Write Counter: {writeCounterValue}");
|
||||
Logger.Debug?.Print(LogClass.ServiceNfp, "Length of Application Areas: " + applicationAreas.Length);
|
||||
}
|
||||
|
||||
private static uint CalculateCRC32(byte[] input)
|
||||
{
|
||||
uint[] table = new uint[256];
|
||||
uint polynomial = 0xEDB88320;
|
||||
for (uint i = 0; i < table.Length; ++i)
|
||||
{
|
||||
uint crc = i;
|
||||
for (int j = 0; j < 8; ++j)
|
||||
{
|
||||
if ((crc & 1) != 0)
|
||||
crc = (crc >> 1) ^ polynomial;
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
table[i] = crc;
|
||||
}
|
||||
|
||||
uint result = 0xFFFFFFFF;
|
||||
foreach (byte b in input)
|
||||
{
|
||||
byte index = (byte)((result & 0xFF) ^ b);
|
||||
result = (result >> 8) ^ table[index];
|
||||
}
|
||||
return ~result;
|
||||
}
|
||||
|
||||
private static string GetKeyRetailBinPath()
|
||||
{
|
||||
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
|
||||
}
|
||||
|
||||
public static bool HasKeyRetailBinPath()
|
||||
{
|
||||
return File.Exists(GetKeyRetailBinPath());
|
||||
}
|
||||
public static DateTime DateTimeFromTag(ushort value)
|
||||
{
|
||||
try
|
||||
{
|
||||
int day = value & 0x1F;
|
||||
int month = (value >> 5) & 0x0F;
|
||||
int year = (value >> 9) & 0x7F;
|
||||
|
||||
if (day == 0 || month == 0 || month > 12 || day > DateTime.DaysInMonth(2000 + year, month))
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
return new DateTime(2000 + year, month, day);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
using System.IO;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
||||
{
|
||||
public class AmiiboDecrypter
|
||||
{
|
||||
public AmiiboMasterKey DataKey { get; private set; }
|
||||
public AmiiboMasterKey TagKey { get; private set; }
|
||||
|
||||
public AmiiboDecrypter(string keyRetailBinPath)
|
||||
{
|
||||
var combinedKeys = File.ReadAllBytes(keyRetailBinPath);
|
||||
var keys = AmiiboMasterKey.FromCombinedBin(combinedKeys);
|
||||
DataKey = keys.DataKey;
|
||||
TagKey = keys.TagKey;
|
||||
}
|
||||
|
||||
public AmiiboDump DecryptAmiiboDump(byte[] encryptedDumpData)
|
||||
{
|
||||
// Initialize AmiiboDump with encrypted data
|
||||
AmiiboDump amiiboDump = new AmiiboDump(encryptedDumpData, DataKey, TagKey, isLocked: true);
|
||||
|
||||
// Unlock (decrypt) the dump
|
||||
amiiboDump.Unlock();
|
||||
|
||||
// Optional: Verify HMACs
|
||||
amiiboDump.VerifyHMACs();
|
||||
|
||||
return amiiboDump;
|
||||
}
|
||||
|
||||
public AmiiboDump EncryptAmiiboDump(byte[] decryptedDumpData)
|
||||
{
|
||||
// Initialize AmiiboDump with decrypted data
|
||||
AmiiboDump amiiboDump = new AmiiboDump(decryptedDumpData, DataKey, TagKey, isLocked: false);
|
||||
|
||||
// Lock (encrypt) the dump
|
||||
amiiboDump.Lock();
|
||||
|
||||
return amiiboDump;
|
||||
}
|
||||
}
|
||||
}
|
||||
387
src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDump.cs
Normal file
387
src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDump.cs
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
||||
{
|
||||
public class AmiiboDump
|
||||
{
|
||||
private AmiiboMasterKey dataMasterKey;
|
||||
private AmiiboMasterKey tagMasterKey;
|
||||
|
||||
private bool isLocked;
|
||||
private byte[] data;
|
||||
private byte[] hmacTagKey;
|
||||
private byte[] hmacDataKey;
|
||||
private byte[] aesKey;
|
||||
private byte[] aesIv;
|
||||
|
||||
public AmiiboDump(byte[] dumpData, AmiiboMasterKey dataKey, AmiiboMasterKey tagKey, bool isLocked = true)
|
||||
{
|
||||
if (dumpData.Length < 540)
|
||||
throw new ArgumentException("Incomplete dump. Amiibo data is at least 540 bytes.");
|
||||
|
||||
this.data = new byte[540];
|
||||
Array.Copy(dumpData, this.data, dumpData.Length);
|
||||
this.dataMasterKey = dataKey;
|
||||
this.tagMasterKey = tagKey;
|
||||
this.isLocked = isLocked;
|
||||
|
||||
if (!isLocked)
|
||||
{
|
||||
DeriveKeysAndCipher();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] DeriveKey(AmiiboMasterKey key, bool deriveAes, out byte[] derivedAesKey, out byte[] derivedAesIv)
|
||||
{
|
||||
List<byte> seed = new List<byte>();
|
||||
|
||||
// Start with the type string (14 bytes)
|
||||
seed.AddRange(key.TypeString);
|
||||
|
||||
// Append data based on magic size
|
||||
int append = 16 - key.MagicSize;
|
||||
byte[] extract = new byte[16];
|
||||
Array.Copy(this.data, 0x011, extract, 0, 2); // Extract two bytes from user data section
|
||||
for (int i = 2; i < 16; i++)
|
||||
{
|
||||
extract[i] = 0x00;
|
||||
}
|
||||
seed.AddRange(extract.Take(append));
|
||||
|
||||
// Add the magic bytes
|
||||
seed.AddRange(key.MagicBytes.Take(key.MagicSize));
|
||||
|
||||
// Extract the UID (UID is 8 bytes)
|
||||
byte[] uid = new byte[8];
|
||||
Array.Copy(this.data, 0x000, uid, 0, 8);
|
||||
seed.AddRange(uid);
|
||||
seed.AddRange(uid);
|
||||
|
||||
// Extract some tag data (pages 0x20 - 0x28)
|
||||
byte[] user = new byte[32];
|
||||
Array.Copy(this.data, 0x060, user, 0, 32);
|
||||
|
||||
// XOR it with the key padding (XorPad)
|
||||
byte[] paddedUser = new byte[32];
|
||||
for (int i = 0; i < user.Length; i++)
|
||||
{
|
||||
paddedUser[i] = (byte)(user[i] ^ key.XorPad[i]);
|
||||
}
|
||||
seed.AddRange(paddedUser);
|
||||
|
||||
byte[] seedBytes = seed.ToArray();
|
||||
if (seedBytes.Length != 78)
|
||||
{
|
||||
throw new Exception("Size check for key derived seed failed");
|
||||
}
|
||||
|
||||
byte[] hmacKey;
|
||||
derivedAesKey = null;
|
||||
derivedAesIv = null;
|
||||
|
||||
if (deriveAes)
|
||||
{
|
||||
// Derive AES Key and IV
|
||||
var dataForAes = new byte[2 + seedBytes.Length];
|
||||
dataForAes[0] = 0x00;
|
||||
dataForAes[1] = 0x00; // Counter (0)
|
||||
Array.Copy(seedBytes, 0, dataForAes, 2, seedBytes.Length);
|
||||
|
||||
byte[] derivedBytes;
|
||||
using (var hmac = new HMACSHA256(key.HmacKey))
|
||||
{
|
||||
derivedBytes = hmac.ComputeHash(dataForAes);
|
||||
}
|
||||
|
||||
derivedAesKey = derivedBytes.Take(16).ToArray();
|
||||
derivedAesIv = derivedBytes.Skip(16).Take(16).ToArray();
|
||||
|
||||
// Derive HMAC Key
|
||||
var dataForHmacKey = new byte[2 + seedBytes.Length];
|
||||
dataForHmacKey[0] = 0x00;
|
||||
dataForHmacKey[1] = 0x01; // Counter (1)
|
||||
Array.Copy(seedBytes, 0, dataForHmacKey, 2, seedBytes.Length);
|
||||
|
||||
using (var hmac = new HMACSHA256(key.HmacKey))
|
||||
{
|
||||
derivedBytes = hmac.ComputeHash(dataForHmacKey);
|
||||
}
|
||||
|
||||
hmacKey = derivedBytes.Take(16).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Derive HMAC Key only
|
||||
var dataForHmacKey = new byte[2 + seedBytes.Length];
|
||||
dataForHmacKey[0] = 0x00;
|
||||
dataForHmacKey[1] = 0x01; // Counter (1)
|
||||
Array.Copy(seedBytes, 0, dataForHmacKey, 2, seedBytes.Length);
|
||||
|
||||
byte[] derivedBytes;
|
||||
using (var hmac = new HMACSHA256(key.HmacKey))
|
||||
{
|
||||
derivedBytes = hmac.ComputeHash(dataForHmacKey);
|
||||
}
|
||||
|
||||
hmacKey = derivedBytes.Take(16).ToArray();
|
||||
}
|
||||
|
||||
return hmacKey;
|
||||
}
|
||||
|
||||
private void DeriveKeysAndCipher()
|
||||
{
|
||||
byte[] discard;
|
||||
// Derive HMAC Tag Key
|
||||
this.hmacTagKey = DeriveKey(this.tagMasterKey, false, out discard, out discard);
|
||||
|
||||
// Derive HMAC Data Key and AES Key/IV
|
||||
this.hmacDataKey = DeriveKey(this.dataMasterKey, true, out aesKey, out aesIv);
|
||||
}
|
||||
|
||||
private void DecryptData()
|
||||
{
|
||||
byte[] encryptedBlock = new byte[0x020 + 0x168];
|
||||
Array.Copy(data, 0x014, encryptedBlock, 0, 0x020); // data[0x014:0x034]
|
||||
Array.Copy(data, 0x0A0, encryptedBlock, 0x020, 0x168); // data[0x0A0:0x208]
|
||||
|
||||
byte[] decryptedBlock = AES_CTR_Transform(encryptedBlock, aesKey, aesIv);
|
||||
|
||||
// Copy decrypted data back
|
||||
Array.Copy(decryptedBlock, 0, data, 0x014, 0x020);
|
||||
Array.Copy(decryptedBlock, 0x020, data, 0x0A0, 0x168);
|
||||
}
|
||||
|
||||
private void EncryptData()
|
||||
{
|
||||
byte[] plainBlock = new byte[0x020 + 0x168];
|
||||
Array.Copy(data, 0x014, plainBlock, 0, 0x020); // data[0x014:0x034]
|
||||
Array.Copy(data, 0x0A0, plainBlock, 0x020, 0x168); // data[0x0A0:0x208]
|
||||
|
||||
byte[] encryptedBlock = AES_CTR_Transform(plainBlock, aesKey, aesIv);
|
||||
|
||||
// Copy encrypted data back
|
||||
Array.Copy(encryptedBlock, 0, data, 0x014, 0x020);
|
||||
Array.Copy(encryptedBlock, 0x020, data, 0x0A0, 0x168);
|
||||
}
|
||||
|
||||
private byte[] AES_CTR_Transform(byte[] data, byte[] key, byte[] iv)
|
||||
{
|
||||
byte[] output = new byte[data.Length];
|
||||
|
||||
using (Aes aes = Aes.Create())
|
||||
{
|
||||
aes.Key = key;
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Padding = PaddingMode.None;
|
||||
|
||||
int blockSize = aes.BlockSize / 8; // in bytes, should be 16
|
||||
byte[] counter = new byte[blockSize];
|
||||
Array.Copy(iv, counter, blockSize);
|
||||
|
||||
using (ICryptoTransform encryptor = aes.CreateEncryptor())
|
||||
{
|
||||
byte[] encryptedCounter = new byte[blockSize];
|
||||
|
||||
for (int i = 0; i < data.Length; i += blockSize)
|
||||
{
|
||||
// Encrypt the counter
|
||||
encryptor.TransformBlock(counter, 0, blockSize, encryptedCounter, 0);
|
||||
|
||||
// Determine the number of bytes to process in this block
|
||||
int blockLength = Math.Min(blockSize, data.Length - i);
|
||||
|
||||
// XOR the encrypted counter with the plaintext/ciphertext block
|
||||
for (int j = 0; j < blockLength; j++)
|
||||
{
|
||||
output[i + j] = (byte)(data[i + j] ^ encryptedCounter[j]);
|
||||
}
|
||||
|
||||
// Increment the counter
|
||||
IncrementCounter(counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private void IncrementCounter(byte[] counter)
|
||||
{
|
||||
for (int i = counter.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (++counter[i] != 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeriveHMACs()
|
||||
{
|
||||
if (isLocked)
|
||||
throw new InvalidOperationException("Cannot derive HMACs when data is locked.");
|
||||
|
||||
// Calculate tag HMAC
|
||||
byte[] tagHmacData = new byte[8 + 44];
|
||||
Array.Copy(data, 0x000, tagHmacData, 0, 8);
|
||||
Array.Copy(data, 0x054, tagHmacData, 8, 44);
|
||||
|
||||
byte[] tagHmac;
|
||||
using (var hmac = new HMACSHA256(hmacTagKey))
|
||||
{
|
||||
tagHmac = hmac.ComputeHash(tagHmacData);
|
||||
}
|
||||
|
||||
// Overwrite the stored tag HMAC
|
||||
Array.Copy(tagHmac, 0, data, 0x034, 32);
|
||||
|
||||
// Prepare data for data HMAC
|
||||
int len1 = 0x023; // 0x011 to 0x034 (0x034 - 0x011)
|
||||
int len2 = 0x168; // 0x0A0 to 0x208 (0x208 - 0x0A0)
|
||||
int len3 = tagHmac.Length; // 32 bytes
|
||||
int len4 = 0x008; // 0x000 to 0x008 (0x008 - 0x000)
|
||||
int len5 = 0x02C; // 0x054 to 0x080 (0x080 - 0x054)
|
||||
int totalLength = len1 + len2 + len3 + len4 + len5;
|
||||
byte[] dataHmacData = new byte[totalLength];
|
||||
|
||||
int offset = 0;
|
||||
Array.Copy(data, 0x011, dataHmacData, offset, len1);
|
||||
offset += len1;
|
||||
Array.Copy(data, 0x0A0, dataHmacData, offset, len2);
|
||||
offset += len2;
|
||||
Array.Copy(tagHmac, 0, dataHmacData, offset, len3);
|
||||
offset += len3;
|
||||
Array.Copy(data, 0x000, dataHmacData, offset, len4);
|
||||
offset += len4;
|
||||
Array.Copy(data, 0x054, dataHmacData, offset, len5);
|
||||
|
||||
byte[] dataHmac;
|
||||
using (var hmac = new HMACSHA256(hmacDataKey))
|
||||
{
|
||||
dataHmac = hmac.ComputeHash(dataHmacData);
|
||||
}
|
||||
|
||||
// Overwrite the stored data HMAC
|
||||
Array.Copy(dataHmac, 0, data, 0x080, 32);
|
||||
}
|
||||
|
||||
public void VerifyHMACs()
|
||||
{
|
||||
if (isLocked)
|
||||
throw new InvalidOperationException("Cannot verify HMACs when data is locked.");
|
||||
|
||||
// Calculate tag HMAC
|
||||
byte[] tagHmacData = new byte[8 + 44];
|
||||
Array.Copy(data, 0x000, tagHmacData, 0, 8);
|
||||
Array.Copy(data, 0x054, tagHmacData, 8, 44);
|
||||
|
||||
byte[] calculatedTagHmac;
|
||||
using (var hmac = new HMACSHA256(hmacTagKey))
|
||||
{
|
||||
calculatedTagHmac = hmac.ComputeHash(tagHmacData);
|
||||
}
|
||||
|
||||
byte[] storedTagHmac = new byte[32];
|
||||
Array.Copy(data, 0x034, storedTagHmac, 0, 32);
|
||||
|
||||
if (!calculatedTagHmac.SequenceEqual(storedTagHmac))
|
||||
{
|
||||
throw new Exception("Tag HMAC verification failed.");
|
||||
}
|
||||
|
||||
// Prepare data for data HMAC
|
||||
int len1 = 0x023; // 0x011 to 0x034
|
||||
int len2 = 0x168; // 0x0A0 to 0x208
|
||||
int len3 = calculatedTagHmac.Length; // 32 bytes
|
||||
int len4 = 0x008; // 0x000 to 0x008
|
||||
int len5 = 0x02C; // 0x054 to 0x080
|
||||
int totalLength = len1 + len2 + len3 + len4 + len5;
|
||||
byte[] dataHmacData = new byte[totalLength];
|
||||
|
||||
int offset = 0;
|
||||
Array.Copy(data, 0x011, dataHmacData, offset, len1);
|
||||
offset += len1;
|
||||
Array.Copy(data, 0x0A0, dataHmacData, offset, len2);
|
||||
offset += len2;
|
||||
Array.Copy(calculatedTagHmac, 0, dataHmacData, offset, len3);
|
||||
offset += len3;
|
||||
Array.Copy(data, 0x000, dataHmacData, offset, len4);
|
||||
offset += len4;
|
||||
Array.Copy(data, 0x054, dataHmacData, offset, len5);
|
||||
|
||||
byte[] calculatedDataHmac;
|
||||
using (var hmac = new HMACSHA256(hmacDataKey))
|
||||
{
|
||||
calculatedDataHmac = hmac.ComputeHash(dataHmacData);
|
||||
}
|
||||
|
||||
byte[] storedDataHmac = new byte[32];
|
||||
Array.Copy(data, 0x080, storedDataHmac, 0, 32);
|
||||
|
||||
if (!calculatedDataHmac.SequenceEqual(storedDataHmac))
|
||||
{
|
||||
throw new Exception("Data HMAC verification failed.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Unlock()
|
||||
{
|
||||
if (!isLocked)
|
||||
throw new InvalidOperationException("Data is already unlocked.");
|
||||
|
||||
// Derive keys and cipher
|
||||
DeriveKeysAndCipher();
|
||||
|
||||
// Decrypt the encrypted data
|
||||
DecryptData();
|
||||
|
||||
isLocked = false;
|
||||
}
|
||||
|
||||
public void Lock()
|
||||
{
|
||||
if (isLocked)
|
||||
throw new InvalidOperationException("Data is already locked.");
|
||||
|
||||
// Recalculate HMACs
|
||||
DeriveHMACs();
|
||||
|
||||
// Encrypt the data
|
||||
EncryptData();
|
||||
|
||||
isLocked = true;
|
||||
}
|
||||
|
||||
public byte[] GetData()
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
// Property to get or set Amiibo nickname
|
||||
public string AmiiboNickname
|
||||
{
|
||||
get
|
||||
{
|
||||
// data[0x020:0x034], big endian UTF-16
|
||||
byte[] nicknameBytes = new byte[0x014];
|
||||
Array.Copy(data, 0x020, nicknameBytes, 0, 0x014);
|
||||
string nickname = System.Text.Encoding.BigEndianUnicode.GetString(nicknameBytes).TrimEnd('\0');
|
||||
return nickname;
|
||||
}
|
||||
set
|
||||
{
|
||||
byte[] nicknameBytes = System.Text.Encoding.BigEndianUnicode.GetBytes(value.PadRight(10, '\0'));
|
||||
if (nicknameBytes.Length > 20)
|
||||
throw new ArgumentException("Nickname too long.");
|
||||
Array.Copy(nicknameBytes, 0, data, 0x020, nicknameBytes.Length);
|
||||
// Pad remaining bytes with zeros
|
||||
for (int i = 0x020 + nicknameBytes.Length; i < 0x034; i++)
|
||||
{
|
||||
data[i] = 0x00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
||||
{
|
||||
public class AmiiboMasterKey
|
||||
{
|
||||
public byte[] HmacKey { get; private set; } // 16 bytes
|
||||
public byte[] TypeString { get; private set; } // 14 bytes
|
||||
public byte Rfu { get; private set; } // 1 byte
|
||||
public byte MagicSize { get; private set; } // 1 byte
|
||||
public byte[] MagicBytes { get; private set; } // 16 bytes
|
||||
public byte[] XorPad { get; private set; } // 32 bytes
|
||||
|
||||
public AmiiboMasterKey(byte[] data)
|
||||
{
|
||||
if (data.Length != 80)
|
||||
throw new ArgumentException("Master key data must be 80 bytes.");
|
||||
|
||||
HmacKey = data.Take(16).ToArray();
|
||||
TypeString = data.Skip(16).Take(14).ToArray();
|
||||
Rfu = data[30];
|
||||
MagicSize = data[31];
|
||||
MagicBytes = data.Skip(32).Take(16).ToArray();
|
||||
XorPad = data.Skip(48).Take(32).ToArray();
|
||||
}
|
||||
|
||||
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromCombinedBin(byte[] combinedBin)
|
||||
{
|
||||
if (combinedBin.Length != 160)
|
||||
throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be 160).");
|
||||
|
||||
byte[] dataBin = combinedBin.Take(80).ToArray();
|
||||
byte[] tagBin = combinedBin.Skip(80).Take(80).ToArray();
|
||||
|
||||
AmiiboMasterKey dataKey = new AmiiboMasterKey(dataBin);
|
||||
AmiiboMasterKey tagKey = new AmiiboMasterKey(tagBin);
|
||||
|
||||
return (dataKey, tagKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +78,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
if (_state == State.Initialized)
|
||||
{
|
||||
_cancelTokenSource?.Cancel();
|
||||
|
||||
// NOTE: All events are destroyed here.
|
||||
context.Device.System.NfpDevices.Clear();
|
||||
|
||||
|
|
@ -146,9 +145,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
while (true)
|
||||
|
|
@ -199,7 +196,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +225,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
}
|
||||
|
||||
// TODO: Found how the MountTarget is handled.
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
|
||||
{
|
||||
if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
|
||||
|
|
@ -488,14 +483,12 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
#pragma warning disable IDE0059 // Remove unnecessary value assignment
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
#pragma warning restore IDE0059
|
||||
|
||||
if (context.Device.System.NfpDevices.Count == 0)
|
||||
{
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
// NOTE: Since we handle amiibo through VirtualAmiibo, we don't have to flush anything in our case.
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
|
|
@ -884,7 +877,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Ryujinx.Common.Utilities;
|
|||
using Ryujinx.Cpu;
|
||||
using Ryujinx.HLE.HOS.Services.Mii;
|
||||
using Ryujinx.HLE.HOS.Services.Mii.Types;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -14,10 +15,11 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
{
|
||||
static class VirtualAmiibo
|
||||
{
|
||||
private static uint _openedApplicationAreaId;
|
||||
|
||||
public static uint OpenedApplicationAreaId;
|
||||
public static byte[] ApplicationBytes = new byte[0];
|
||||
public static string InputBin = string.Empty;
|
||||
public static string NickName = string.Empty;
|
||||
private static readonly AmiiboJsonSerializerContext _serializerContext = AmiiboJsonSerializerContext.Default;
|
||||
|
||||
public static byte[] GenerateUuid(string amiiboId, bool useRandomUuid)
|
||||
{
|
||||
if (useRandomUuid)
|
||||
|
|
@ -69,6 +71,11 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
{
|
||||
VirtualAmiiboFile amiiboFile = LoadAmiiboFile(amiiboId);
|
||||
string nickname = amiiboFile.NickName ?? "Ryujinx";
|
||||
if (NickName != string.Empty)
|
||||
{
|
||||
nickname = NickName;
|
||||
NickName = string.Empty;
|
||||
}
|
||||
UtilityImpl utilityImpl = new(tickSource);
|
||||
CharInfo charInfo = new();
|
||||
|
||||
|
|
@ -98,16 +105,26 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
{
|
||||
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
|
||||
virtualAmiiboFile.NickName = newNickName;
|
||||
if (InputBin != string.Empty)
|
||||
{
|
||||
AmiiboBinReader.SaveBinFile(InputBin, virtualAmiiboFile.NickName);
|
||||
return;
|
||||
}
|
||||
SaveAmiiboFile(virtualAmiiboFile);
|
||||
}
|
||||
|
||||
public static bool OpenApplicationArea(string amiiboId, uint applicationAreaId)
|
||||
{
|
||||
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
|
||||
if (ApplicationBytes.Length > 0)
|
||||
{
|
||||
OpenedApplicationAreaId = applicationAreaId;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (virtualAmiiboFile.ApplicationAreas.Any(item => item.ApplicationAreaId == applicationAreaId))
|
||||
{
|
||||
_openedApplicationAreaId = applicationAreaId;
|
||||
OpenedApplicationAreaId = applicationAreaId;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -117,11 +134,17 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
|
||||
public static byte[] GetApplicationArea(string amiiboId)
|
||||
{
|
||||
if (ApplicationBytes.Length > 0)
|
||||
{
|
||||
byte[] bytes = ApplicationBytes;
|
||||
ApplicationBytes = new byte[0];
|
||||
return bytes;
|
||||
}
|
||||
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
|
||||
|
||||
foreach (VirtualAmiiboApplicationArea applicationArea in virtualAmiiboFile.ApplicationAreas)
|
||||
{
|
||||
if (applicationArea.ApplicationAreaId == _openedApplicationAreaId)
|
||||
if (applicationArea.ApplicationAreaId == OpenedApplicationAreaId)
|
||||
{
|
||||
return applicationArea.ApplicationArea;
|
||||
}
|
||||
|
|
@ -152,17 +175,22 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||
|
||||
public static void SetApplicationArea(string amiiboId, byte[] applicationAreaData)
|
||||
{
|
||||
if (InputBin != string.Empty)
|
||||
{
|
||||
AmiiboBinReader.SaveBinFile(InputBin, applicationAreaData);
|
||||
return;
|
||||
}
|
||||
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
|
||||
|
||||
if (virtualAmiiboFile.ApplicationAreas.Any(item => item.ApplicationAreaId == _openedApplicationAreaId))
|
||||
if (virtualAmiiboFile.ApplicationAreas.Any(item => item.ApplicationAreaId == OpenedApplicationAreaId))
|
||||
{
|
||||
for (int i = 0; i < virtualAmiiboFile.ApplicationAreas.Count; i++)
|
||||
{
|
||||
if (virtualAmiiboFile.ApplicationAreas[i].ApplicationAreaId == _openedApplicationAreaId)
|
||||
if (virtualAmiiboFile.ApplicationAreas[i].ApplicationAreaId == OpenedApplicationAreaId)
|
||||
{
|
||||
virtualAmiiboFile.ApplicationAreas[i] = new VirtualAmiiboApplicationArea()
|
||||
{
|
||||
ApplicationAreaId = _openedApplicationAreaId,
|
||||
ApplicationAreaId = OpenedApplicationAreaId,
|
||||
ApplicationArea = applicationAreaData,
|
||||
};
|
||||
|
||||
|
|
@ -205,10 +233,21 @@ 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);
|
||||
}
|
||||
|
||||
public static bool SaveFileExists(VirtualAmiiboFile virtualAmiiboFile)
|
||||
{
|
||||
if (InputBin != string.Empty)
|
||||
{
|
||||
SaveAmiiboFile(virtualAmiiboFile);
|
||||
return true;
|
||||
|
||||
}
|
||||
return File.Exists(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_الإجراءات",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "محاكاة رسالة الاستيقاظ",
|
||||
"MenuBarActionsScanAmiibo": "فحص Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_الأدوات",
|
||||
"MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Aktionen",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Aufwachnachricht simulieren",
|
||||
"MenuBarActionsScanAmiibo": "Amiibo scannen",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Tools",
|
||||
"MenuBarToolsInstallFirmware": "Firmware installieren",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Firmware von einer XCI- oder einer ZIP-Datei installieren",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Δράσεις",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Προσομοίωση Μηνύματος Αφύπνισης",
|
||||
"MenuBarActionsScanAmiibo": "Σάρωση Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Εργαλεία",
|
||||
"MenuBarToolsInstallFirmware": "Εγκατάσταση Firmware",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Εγκατάσταση Firmware από XCI ή ZIP",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,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",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Acciones",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Simular mensaje de reactivación",
|
||||
"MenuBarActionsScanAmiibo": "Escanear Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Herramientas",
|
||||
"MenuBarToolsInstallFirmware": "Instalar firmware",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware desde un archivo XCI o ZIP",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Actions",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Simuler un message de réveil",
|
||||
"MenuBarActionsScanAmiibo": "Scanner un Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Outils",
|
||||
"MenuBarToolsInstallFirmware": "Installer un firmware",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Installer un firmware depuis un fichier XCI ou ZIP",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_פעולות",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "דמה הודעת השכמה",
|
||||
"MenuBarActionsScanAmiibo": "סרוק אמיבו",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_כלים",
|
||||
"MenuBarToolsInstallFirmware": "התקן קושחה",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "התקן קושחה מקובץ- ZIP/XCI",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Azioni",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Simula messaggio Wake-up",
|
||||
"MenuBarActionsScanAmiibo": "Scansiona un Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Strumenti",
|
||||
"MenuBarToolsInstallFirmware": "Installa firmware",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Installa un firmware da file XCI o ZIP",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "アクション(_A)",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "スリープ復帰メッセージをシミュレート",
|
||||
"MenuBarActionsScanAmiibo": "Amiibo をスキャン",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "ツール(_T)",
|
||||
"MenuBarToolsInstallFirmware": "ファームウェアをインストール",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "XCI または ZIP からファームウェアをインストール",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_동작",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "깨우기 메시지 시뮬레이션",
|
||||
"MenuBarActionsScanAmiibo": "Amiibo 스캔",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_도구",
|
||||
"MenuBarToolsInstallFirmware": "펌웨어 설치",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "XCI 또는 ZIP에서 펌웨어 설치",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Akcje",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Symuluj wiadomość wybudzania",
|
||||
"MenuBarActionsScanAmiibo": "Skanuj Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Narzędzia",
|
||||
"MenuBarToolsInstallFirmware": "Zainstaluj oprogramowanie",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Zainstaluj oprogramowanie z XCI lub ZIP",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Ações",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "_Simular mensagem de acordar console",
|
||||
"MenuBarActionsScanAmiibo": "Escanear um Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Ferramentas",
|
||||
"MenuBarToolsInstallFirmware": "_Instalar firmware",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware a partir de um arquivo ZIP/XCI",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Действия",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Имитировать сообщение пробуждения",
|
||||
"MenuBarActionsScanAmiibo": "Сканировать Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Инструменты",
|
||||
"MenuBarToolsInstallFirmware": "Установка прошивки",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Установить прошивку из XCI или ZIP",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "การดำเนินการ",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "จำลองข้อความปลุก",
|
||||
"MenuBarActionsScanAmiibo": "สแกนหา Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_เครื่องมือ",
|
||||
"MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Eylemler",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Uyandırma Mesajı Simüle Et",
|
||||
"MenuBarActionsScanAmiibo": "Bir Amiibo Tara",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Araçlar",
|
||||
"MenuBarToolsInstallFirmware": "Yazılım Yükle",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "XCI veya ZIP'ten Yazılım Yükle",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "_Дії",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "Симулювати повідомлення про пробудження",
|
||||
"MenuBarActionsScanAmiibo": "Сканувати Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "_Інструменти",
|
||||
"MenuBarToolsInstallFirmware": "Установити прошивку",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "Установити прошивку з XCI або ZIP",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "操作(_A)",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息",
|
||||
"MenuBarActionsScanAmiibo": "扫描 Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "工具(_T)",
|
||||
"MenuBarToolsInstallFirmware": "安装系统固件",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"MenuBarActions": "動作(_A)",
|
||||
"MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息",
|
||||
"MenuBarActionsScanAmiibo": "掃描 Amiibo",
|
||||
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||
"MenuBarTools": "工具(_T)",
|
||||
"MenuBarToolsInstallFirmware": "安裝韌體",
|
||||
"MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ using Ryujinx.HLE;
|
|||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
|
||||
using Ryujinx.HLE.UI;
|
||||
using Ryujinx.Input.HLE;
|
||||
using Ryujinx.Modules;
|
||||
|
|
@ -35,6 +36,7 @@ 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;
|
||||
|
|
@ -69,6 +71,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
private string _volumeStatusText;
|
||||
private string _gpuStatusText;
|
||||
private bool _isAmiiboRequested;
|
||||
private bool _isAmiiboBinRequested;
|
||||
private bool _isGameRunning;
|
||||
private bool _isFullScreen;
|
||||
private int _progressMaximum;
|
||||
|
|
@ -294,7 +297,16 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public bool IsAmiiboBinRequested
|
||||
{
|
||||
get => _isAmiiboBinRequested && _isGameRunning;
|
||||
set
|
||||
{
|
||||
_isAmiiboBinRequested = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public bool ShowLoadProgress
|
||||
{
|
||||
get => _showLoadProgress;
|
||||
|
|
@ -2001,6 +2013,33 @@ 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()
|
||||
{
|
||||
if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs)
|
||||
|
|
|
|||
|
|
@ -171,6 +171,12 @@
|
|||
Click="OpenAmiiboWindow"
|
||||
Header="{locale:Locale MenuBarActionsScanAmiibo}"
|
||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||
<MenuItem
|
||||
Name="ScanAmiiboMenuItemFromBin"
|
||||
AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree"
|
||||
Click="OpenBinFile"
|
||||
Header="{locale:Locale MenuBarActionsScanAmiiboBin}"
|
||||
IsEnabled="{Binding IsAmiiboBinRequested}" />
|
||||
<MenuItem
|
||||
Command="{Binding TakeScreenshot}"
|
||||
Header="{locale:Locale MenuBarFileToolsTakeScreenshot}"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.ViewModels;
|
|||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.UI.App.Common;
|
||||
using Ryujinx.UI.Common;
|
||||
|
|
@ -170,6 +171,9 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||
}
|
||||
}
|
||||
|
||||
public async void OpenBinFile(object sender, RoutedEventArgs e)
|
||||
=> await ViewModel.OpenBinFile();
|
||||
|
||||
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!ViewModel.IsGameRunning)
|
||||
|
|
@ -196,6 +200,12 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||
}
|
||||
}
|
||||
|
||||
private void ScanBinAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem)
|
||||
ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasKeyRetailBinPath();
|
||||
}
|
||||
|
||||
private async void InstallFileTypes_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (FileAssociationHelper.Install())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue