Actually got it working
Nickname changing and saving does not work, if you try to save with a bin amiibo it will just save over your previous virtual amiibo.
This commit is contained in:
parent
334e71528e
commit
e6b11ad76d
@ -16,7 +16,8 @@ using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemA
|
|||||||
using Ryujinx.HLE.HOS.Services.Apm;
|
using Ryujinx.HLE.HOS.Services.Apm;
|
||||||
using Ryujinx.HLE.HOS.Services.Caps;
|
using Ryujinx.HLE.HOS.Services.Caps;
|
||||||
using Ryujinx.HLE.HOS.Services.Mii;
|
using Ryujinx.HLE.HOS.Services.Mii;
|
||||||
using Ryujinx.HLE.HOS.Services.Nfc.Bin;
|
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.Nfc.Nfp.NfpManager;
|
||||||
using Ryujinx.HLE.HOS.Services.Nv;
|
using Ryujinx.HLE.HOS.Services.Nv;
|
||||||
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
|
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
|
||||||
@ -338,6 +339,10 @@ namespace Ryujinx.HLE.HOS
|
|||||||
|
|
||||||
public void ScanAmiibo(int nfpDeviceId, string amiiboId, bool useRandomUuid)
|
public void ScanAmiibo(int nfpDeviceId, string amiiboId, bool useRandomUuid)
|
||||||
{
|
{
|
||||||
|
if (VirtualAmiibo.applicationBytes.Length > 0)
|
||||||
|
{
|
||||||
|
VirtualAmiibo.applicationBytes = new byte[0];
|
||||||
|
}
|
||||||
if (NfpDevices[nfpDeviceId].State == NfpDeviceState.SearchingForTag)
|
if (NfpDevices[nfpDeviceId].State == NfpDeviceState.SearchingForTag)
|
||||||
{
|
{
|
||||||
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
|
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
|
||||||
@ -347,6 +352,10 @@ namespace Ryujinx.HLE.HOS
|
|||||||
}
|
}
|
||||||
public void ScanAmiiboFromBin(string path)
|
public void ScanAmiiboFromBin(string path)
|
||||||
{
|
{
|
||||||
|
if (VirtualAmiibo.applicationBytes.Length > 0)
|
||||||
|
{
|
||||||
|
VirtualAmiibo.applicationBytes = new byte[0];
|
||||||
|
}
|
||||||
byte[] encryptedData = File.ReadAllBytes(path);
|
byte[] encryptedData = File.ReadAllBytes(path);
|
||||||
VirtualAmiiboFile newFile = AmiiboBinReader.ReadBinFile(encryptedData);
|
VirtualAmiiboFile newFile = AmiiboBinReader.ReadBinFile(encryptedData);
|
||||||
if (SearchingForAmiibo(out int nfpDeviceId))
|
if (SearchingForAmiibo(out int nfpDeviceId))
|
||||||
|
@ -5,8 +5,9 @@ using Ryujinx.HLE.HOS.Tamper;
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using static LibHac.FsSystem.AesCtrCounterExtendedStorage;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
||||||
{
|
{
|
||||||
public class AmiiboBinReader
|
public class AmiiboBinReader
|
||||||
{
|
{
|
||||||
@ -40,22 +41,9 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
|||||||
}
|
}
|
||||||
|
|
||||||
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
|
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
|
||||||
byte[] decryptedFileBytes = amiiboDecryptor.DecryptAmiiboData(fileBytes, initialCounter);
|
AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(fileBytes);
|
||||||
|
|
||||||
if (decryptedFileBytes.Length != totalBytes)
|
|
||||||
{
|
|
||||||
Array.Resize(ref decryptedFileBytes, totalBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] uid = new byte[7];
|
|
||||||
Array.Copy(fileBytes, 0, uid, 0, 7);
|
|
||||||
|
|
||||||
byte bcc0 = CalculateBCC0(uid);
|
|
||||||
byte bcc1 = CalculateBCC1(uid);
|
|
||||||
|
|
||||||
LogDebugData(uid, bcc0, bcc1);
|
|
||||||
|
|
||||||
byte[] nickNameBytes = new byte[20];
|
|
||||||
byte[] titleId = new byte[8];
|
byte[] titleId = new byte[8];
|
||||||
byte[] usedCharacter = new byte[2];
|
byte[] usedCharacter = new byte[2];
|
||||||
byte[] variation = new byte[2];
|
byte[] variation = new byte[2];
|
||||||
@ -68,75 +56,60 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
|||||||
byte[] settingsBytes = new byte[2];
|
byte[] settingsBytes = new byte[2];
|
||||||
byte formData = 0;
|
byte formData = 0;
|
||||||
byte[] applicationAreas = new byte[216];
|
byte[] applicationAreas = new byte[216];
|
||||||
|
byte[] dataFull = amiiboDump.GetData();
|
||||||
|
Console.WriteLine("Data Full Length: " + dataFull.Length);
|
||||||
|
byte[] uid = new byte[7];
|
||||||
|
Array.Copy(dataFull, 0, uid, 0, 7);
|
||||||
|
|
||||||
for (int page = 0; page < totalPages; page++)
|
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 * pageSize;
|
int pageStartIdx = page * 4; // Each page is 4 bytes
|
||||||
byte[] pageData = new byte[4];
|
byte[] pageData = new byte[4];
|
||||||
bool isEncrypted = IsPageEncrypted(page);
|
byte[] sourceBytes = dataFull;
|
||||||
byte[] sourceBytes = isEncrypted ? decryptedFileBytes : fileBytes;
|
|
||||||
if (pageStartIdx + pageSize > sourceBytes.Length)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
|
Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
|
||||||
|
// Special handling for specific pages
|
||||||
switch (page)
|
switch (page)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0: // Page 0 (UID + BCC0)
|
||||||
|
Console.WriteLine("Page 0: UID and BCC0.");
|
||||||
break;
|
break;
|
||||||
|
case 2: // Page 2 (BCC1 + Internal Value)
|
||||||
case 2:
|
|
||||||
byte internalValue = pageData[1];
|
byte internalValue = pageData[1];
|
||||||
|
Console.WriteLine($"Page 2: BCC1 + Internal Value 0x{internalValue:X2} (Expected 0x48).");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 5:
|
|
||||||
Array.Copy(pageData, 0, settingsBytes, 0, 2);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 6:
|
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, 0, initDate, 0, 2);
|
||||||
Array.Copy(pageData, 2, writeDate, 0, 2);
|
Array.Copy(pageData, 2, writeDate, 0, 2);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case >= 8 and <= 12:
|
|
||||||
int nickNameOffset = (page - 8) * 4;
|
|
||||||
Array.Copy(pageData, 0, nickNameBytes, nickNameOffset, 4);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 21:
|
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, 0, usedCharacter, 0, 2);
|
||||||
Array.Copy(pageData, 2, variation, 0, 2);
|
Array.Copy(pageData, 2, variation, 0, 2);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 22:
|
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);
|
Array.Copy(pageData, 0, amiiboID, 0, 2);
|
||||||
setID[0] = pageData[2];
|
setID[0] = pageData[2];
|
||||||
formData = pageData[3];
|
formData = pageData[3];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 40:
|
|
||||||
case 41:
|
|
||||||
int appIdOffset = (page - 40) * 4;
|
|
||||||
Array.Copy(decryptedFileBytes, pageStartIdx, appId, appIdOffset, 4);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 64:
|
case 64:
|
||||||
case 65:
|
case 65:
|
||||||
|
// Extract title ID
|
||||||
int titleIdOffset = (page - 64) * 4;
|
int titleIdOffset = (page - 64) * 4;
|
||||||
Array.Copy(sourceBytes, pageStartIdx, titleId, titleIdOffset, 4);
|
Array.Copy(pageData, 0, titleId, titleIdOffset, 4);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 66:
|
case 66:
|
||||||
|
// Bytes 0 and 1 are write counter
|
||||||
Array.Copy(pageData, 0, writeCounter, 0, 2);
|
Array.Copy(pageData, 0, writeCounter, 0, 2);
|
||||||
break;
|
break;
|
||||||
|
// Pages 76 to 127 are application areas
|
||||||
case >= 76 and <= 129:
|
case >= 76 and <= 127:
|
||||||
int appAreaOffset = (page - 76) * 4;
|
int appAreaOffset = (page - 76) * 4;
|
||||||
if (appAreaOffset + 4 <= applicationAreas.Length)
|
Array.Copy(pageData, 0, applicationAreas, appAreaOffset, 4);
|
||||||
{
|
|
||||||
Array.Copy(pageData, 0, applicationAreas, appAreaOffset, 4);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,13 +123,12 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
|||||||
string finalID = head + tail;
|
string finalID = head + tail;
|
||||||
|
|
||||||
ushort settingsValue = BitConverter.ToUInt16(settingsBytes, 0);
|
ushort settingsValue = BitConverter.ToUInt16(settingsBytes, 0);
|
||||||
string nickName = Encoding.BigEndianUnicode.GetString(nickNameBytes).TrimEnd('\0');
|
|
||||||
ushort initDateValue = BitConverter.ToUInt16(initDate, 0);
|
ushort initDateValue = BitConverter.ToUInt16(initDate, 0);
|
||||||
ushort writeDateValue = BitConverter.ToUInt16(writeDate, 0);
|
ushort writeDateValue = BitConverter.ToUInt16(writeDate, 0);
|
||||||
DateTime initDateTime = DateTimeFromTag(initDateValue);
|
DateTime initDateTime = DateTimeFromTag(initDateValue);
|
||||||
DateTime writeDateTime = DateTimeFromTag(writeDateValue);
|
DateTime writeDateTime = DateTimeFromTag(writeDateValue);
|
||||||
ushort writeCounterValue = BitConverter.ToUInt16(writeCounter, 0);
|
ushort writeCounterValue = BitConverter.ToUInt16(writeCounter, 0);
|
||||||
|
string nickName = amiiboDump.AmiiboNickname;
|
||||||
LogFinalData(titleId, appId, head, tail, finalID, nickName, initDateTime, writeDateTime, settingsValue, writeCounterValue, applicationAreas);
|
LogFinalData(titleId, appId, head, tail, finalID, nickName, initDateTime, writeDateTime, settingsValue, writeCounterValue, applicationAreas);
|
||||||
|
|
||||||
VirtualAmiiboFile virtualAmiiboFile = new VirtualAmiiboFile
|
VirtualAmiiboFile virtualAmiiboFile = new VirtualAmiiboFile
|
||||||
@ -164,11 +136,15 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
|||||||
FileVersion = 1,
|
FileVersion = 1,
|
||||||
TagUuid = uid,
|
TagUuid = uid,
|
||||||
AmiiboId = finalID,
|
AmiiboId = finalID,
|
||||||
|
NickName = nickName,
|
||||||
FirstWriteDate = initDateTime,
|
FirstWriteDate = initDateTime,
|
||||||
LastWriteDate = writeDateTime,
|
LastWriteDate = writeDateTime,
|
||||||
WriteCounter = writeCounterValue,
|
WriteCounter = writeCounterValue,
|
||||||
};
|
};
|
||||||
VirtualAmiibo.applicationBytes = applicationAreas;
|
if (writeCounterValue>0)
|
||||||
|
{
|
||||||
|
VirtualAmiibo.applicationBytes = applicationAreas;
|
||||||
|
}
|
||||||
|
|
||||||
return virtualAmiiboFile;
|
return virtualAmiiboFile;
|
||||||
}
|
}
|
||||||
@ -225,11 +201,10 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
|||||||
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
|
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsPageEncrypted(int page)
|
public static bool HasKeyRetailBinPath()
|
||||||
{
|
{
|
||||||
return (page >= 5 && page <= 12) || (page >= 40 && page <= 129);
|
return File.Exists(GetKeyRetailBinPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DateTime DateTimeFromTag(ushort value)
|
public static DateTime DateTimeFromTag(ushort value)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -1,83 +1,36 @@
|
|||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
|
||||||
using Ryujinx.HLE.HOS.Services.Mii.Types;
|
|
||||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Linq;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
||||||
{
|
{
|
||||||
public class AmiiboDecrypter
|
public class AmiiboDecrypter
|
||||||
{
|
{
|
||||||
public readonly byte[] _hmacKey; // HMAC key
|
public AmiiboMasterKey DataKey { get; private set; }
|
||||||
public readonly byte[] _aesKey; // AES key
|
public AmiiboMasterKey TagKey { get; private set; }
|
||||||
|
|
||||||
public AmiiboDecrypter(string keyRetailBinPath)
|
public AmiiboDecrypter(string keyRetailBinPath)
|
||||||
{
|
{
|
||||||
var keys = AmiiboMasterKey.FromCombinedBin(File.ReadAllBytes(keyRetailBinPath));
|
var combinedKeys = File.ReadAllBytes(keyRetailBinPath);
|
||||||
_hmacKey = keys.DataKey.HmacKey;
|
var keys = AmiiboMasterKey.FromCombinedBin(combinedKeys);
|
||||||
_aesKey = keys.DataKey.XorPad;
|
DataKey = keys.DataKey;
|
||||||
|
TagKey = keys.TagKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] DecryptAmiiboData(byte[] encryptedData, byte[] counter)
|
public AmiiboDump DecryptAmiiboDump(byte[] encryptedDumpData)
|
||||||
{
|
{
|
||||||
// Ensure the counter length matches the block size
|
// Initialize AmiiboDump with encrypted data
|
||||||
if (counter.Length != 16)
|
AmiiboDump amiiboDump = new AmiiboDump(encryptedDumpData, DataKey, TagKey, isLocked: true);
|
||||||
{
|
|
||||||
throw new ArgumentException("Counter must be 16 bytes long for AES block size.");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] decryptedData = new byte[encryptedData.Length];
|
// Unlock (decrypt) the dump
|
||||||
|
amiiboDump.Unlock();
|
||||||
|
|
||||||
using (Aes aesAlg = Aes.Create())
|
// Optional: Verify HMACs
|
||||||
{
|
amiiboDump.VerifyHMACs();
|
||||||
aesAlg.Key = _aesKey;
|
|
||||||
aesAlg.Mode = CipherMode.ECB; // Use ECB mode to handle the counter encryption
|
|
||||||
aesAlg.Padding = PaddingMode.None;
|
|
||||||
|
|
||||||
using (var encryptor = aesAlg.CreateEncryptor())
|
return amiiboDump;
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
389
src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDump.cs
Normal file
389
src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDump.cs
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,69 +1,45 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
||||||
{
|
{
|
||||||
public class AmiiboMasterKey
|
public class AmiiboMasterKey
|
||||||
{
|
{
|
||||||
private const int DataLength = 80;
|
public byte[] HmacKey { get; private set; } // 16 bytes
|
||||||
private const int CombinedLength = 160;
|
|
||||||
public byte[] HmacKey { get; private set; } // 16 bytes
|
|
||||||
public byte[] TypeString { get; private set; } // 14 bytes
|
public byte[] TypeString { get; private set; } // 14 bytes
|
||||||
public byte Rfu { get; private set; } // 1 byte reserved
|
public byte Rfu { get; private set; } // 1 byte
|
||||||
public byte MagicSize { get; private set; } // 1 byte
|
public byte MagicSize { get; private set; } // 1 byte
|
||||||
public byte[] MagicBytes { get; private set; } // 16 bytes
|
public byte[] MagicBytes { get; private set; } // 16 bytes
|
||||||
public byte[] XorPad { get; private set; } // 32 bytes
|
public byte[] XorPad { get; private set; } // 32 bytes
|
||||||
|
|
||||||
private AmiiboMasterKey(byte[] data)
|
public AmiiboMasterKey(byte[] data)
|
||||||
{
|
{
|
||||||
if (data.Length != DataLength)
|
if (data.Length != 80)
|
||||||
throw new ArgumentException($"Data is {data.Length} bytes (should be {DataLength}).");
|
throw new ArgumentException("Master key data must be 80 bytes.");
|
||||||
|
|
||||||
|
HmacKey = data.Take(16).ToArray();
|
||||||
// Unpack the data
|
TypeString = data.Skip(16).Take(14).ToArray();
|
||||||
HmacKey = data[..16];
|
|
||||||
TypeString = data[16..30];
|
|
||||||
Rfu = data[30];
|
Rfu = data[30];
|
||||||
MagicSize = data[31];
|
MagicSize = data[31];
|
||||||
MagicBytes = data[32..48];
|
MagicBytes = data.Skip(32).Take(16).ToArray();
|
||||||
XorPad = data[48..];
|
XorPad = data.Skip(48).Take(32).ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromCombinedBin(byte[] combinedBin)
|
||||||
{
|
{
|
||||||
if (combinedBin.Length != CombinedLength)
|
if (combinedBin.Length != 160)
|
||||||
throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be {CombinedLength}).");
|
throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be 160).");
|
||||||
|
|
||||||
byte[] dataBin = combinedBin[..DataLength];
|
byte[] dataBin = combinedBin.Take(80).ToArray();
|
||||||
byte[] tagBin = combinedBin[DataLength..];
|
byte[] tagBin = combinedBin.Skip(80).Take(80).ToArray();
|
||||||
return FromSeparateBin(dataBin, tagBin);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] HexToBytes(string hex)
|
AmiiboMasterKey dataKey = new AmiiboMasterKey(dataBin);
|
||||||
{
|
AmiiboMasterKey tagKey = new AmiiboMasterKey(tagBin);
|
||||||
int length = hex.Length / 2;
|
|
||||||
byte[] bytes = new byte[length];
|
return (dataKey, tagKey);
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
{
|
|
||||||
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||||||
if (_state == State.Initialized)
|
if (_state == State.Initialized)
|
||||||
{
|
{
|
||||||
_cancelTokenSource?.Cancel();
|
_cancelTokenSource?.Cancel();
|
||||||
|
|
||||||
// NOTE: All events are destroyed here.
|
// NOTE: All events are destroyed here.
|
||||||
context.Device.System.NfpDevices.Clear();
|
context.Device.System.NfpDevices.Clear();
|
||||||
|
|
||||||
@ -146,9 +145,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_cancelTokenSource = new CancellationTokenSource();
|
_cancelTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
@ -199,7 +196,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,7 +225,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Found how the MountTarget is handled.
|
// TODO: Found how the MountTarget is handled.
|
||||||
|
|
||||||
for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
|
for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
|
||||||
{
|
{
|
||||||
if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
|
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
|
#pragma warning disable IDE0059 // Remove unnecessary value assignment
|
||||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||||
#pragma warning restore IDE0059
|
#pragma warning restore IDE0059
|
||||||
|
|
||||||
if (context.Device.System.NfpDevices.Count == 0)
|
if (context.Device.System.NfpDevices.Count == 0)
|
||||||
{
|
{
|
||||||
return ResultCode.DeviceNotFound;
|
return ResultCode.DeviceNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Since we handle amiibo through VirtualAmiibo, we don't have to flush anything in our case.
|
// NOTE: Since we handle amiibo through VirtualAmiibo, we don't have to flush anything in our case.
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -884,7 +877,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResultCode.DeviceNotFound;
|
return ResultCode.DeviceNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ using Ryujinx.HLE;
|
|||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Ryujinx.HLE.HOS.Services.Nfc.Bin;
|
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
|
||||||
using Ryujinx.HLE.UI;
|
using Ryujinx.HLE.UI;
|
||||||
using Ryujinx.Input.HLE;
|
using Ryujinx.Input.HLE;
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
@ -318,6 +318,16 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public bool IsBinAmiiboRequested
|
||||||
|
{
|
||||||
|
get => IsAmiiboRequested && AmiiboBinReader.HasKeyRetailBinPath();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isAmiiboRequested = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool ShowLoadProgress
|
public bool ShowLoadProgress
|
||||||
{
|
{
|
||||||
|
@ -243,11 +243,11 @@
|
|||||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Name="ScanAmiiboMenuItemFromBin"
|
Name="ScanAmiiboMenuItemFromBin"
|
||||||
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
|
AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree"
|
||||||
Click="OpenBinFile"
|
Click="OpenBinFile"
|
||||||
Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
|
Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
|
||||||
Icon="{ext:Icon mdi-cube-scan}"
|
Icon="{ext:Icon mdi-cube-scan}"
|
||||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
IsEnabled="{Binding IsBinAmiiboRequested}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding TakeScreenshot}"
|
Command="{Binding TakeScreenshot}"
|
||||||
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"
|
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"
|
||||||
|
@ -11,6 +11,7 @@ using Ryujinx.Ava.UI.ViewModels;
|
|||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common;
|
using Ryujinx.UI.Common;
|
||||||
using Ryujinx.UI.Common.Configuration;
|
using Ryujinx.UI.Common.Configuration;
|
||||||
@ -166,6 +167,12 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||||||
ViewModel.IsAmiiboRequested = ViewModel.AppHost.Device.System.SearchingForAmiibo(out _);
|
ViewModel.IsAmiiboRequested = ViewModel.AppHost.Device.System.SearchingForAmiibo(out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ScanBinAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is MenuItem)
|
||||||
|
ViewModel.IsBinAmiiboRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasKeyRetailBinPath();
|
||||||
|
}
|
||||||
|
|
||||||
private async void InstallFileTypes_Click(object sender, RoutedEventArgs e)
|
private async void InstallFileTypes_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();
|
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user