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:
Jacobwasbeast 2024-12-05 01:33:51 -06:00
parent 334e71528e
commit e6b11ad76d
9 changed files with 490 additions and 179 deletions

View File

@ -16,7 +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.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.Nv;
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)
{
if (VirtualAmiibo.applicationBytes.Length > 0)
{
VirtualAmiibo.applicationBytes = new byte[0];
}
if (NfpDevices[nfpDeviceId].State == NfpDeviceState.SearchingForTag)
{
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
@ -347,6 +352,10 @@ namespace Ryujinx.HLE.HOS
}
public void ScanAmiiboFromBin(string 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))

View File

@ -5,8 +5,9 @@ using Ryujinx.HLE.HOS.Tamper;
using System;
using System.IO;
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
{
@ -40,22 +41,9 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
}
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[] usedCharacter = 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 formData = 0;
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];
bool isEncrypted = IsPageEncrypted(page);
byte[] sourceBytes = isEncrypted ? decryptedFileBytes : fileBytes;
if (pageStartIdx + pageSize > sourceBytes.Length)
{
break;
}
byte[] sourceBytes = dataFull;
Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
// Special handling for specific pages
switch (page)
{
case 0:
case 0: // Page 0 (UID + BCC0)
Console.WriteLine("Page 0: UID and BCC0.");
break;
case 2:
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 5:
Array.Copy(pageData, 0, settingsBytes, 0, 2);
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 and <= 12:
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 40:
case 41:
int appIdOffset = (page - 40) * 4;
Array.Copy(decryptedFileBytes, pageStartIdx, appId, appIdOffset, 4);
break;
case 64:
case 65:
// Extract title ID
int titleIdOffset = (page - 64) * 4;
Array.Copy(sourceBytes, pageStartIdx, titleId, titleIdOffset, 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;
case >= 76 and <= 129:
// Pages 76 to 127 are application areas
case >= 76 and <= 127:
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;
}
}
@ -150,13 +123,12 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
string finalID = head + tail;
ushort settingsValue = BitConverter.ToUInt16(settingsBytes, 0);
string nickName = Encoding.BigEndianUnicode.GetString(nickNameBytes).TrimEnd('\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
@ -164,11 +136,15 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
FileVersion = 1,
TagUuid = uid,
AmiiboId = finalID,
NickName = nickName,
FirstWriteDate = initDateTime,
LastWriteDate = writeDateTime,
WriteCounter = writeCounterValue,
};
VirtualAmiibo.applicationBytes = applicationAreas;
if (writeCounterValue>0)
{
VirtualAmiibo.applicationBytes = applicationAreas;
}
return virtualAmiiboFile;
}
@ -225,11 +201,10 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.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)
{
try

View File

@ -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.Collections.Generic;
using System.Linq;
using System.IO;
using System.Collections.Generic;
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 readonly byte[] _hmacKey; // HMAC key
public readonly byte[] _aesKey; // AES key
public AmiiboMasterKey DataKey { get; private set; }
public AmiiboMasterKey TagKey { get; private set; }
public AmiiboDecrypter(string keyRetailBinPath)
{
var keys = AmiiboMasterKey.FromCombinedBin(File.ReadAllBytes(keyRetailBinPath));
_hmacKey = keys.DataKey.HmacKey;
_aesKey = keys.DataKey.XorPad;
var combinedKeys = File.ReadAllBytes(keyRetailBinPath);
var keys = AmiiboMasterKey.FromCombinedBin(combinedKeys);
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
if (counter.Length != 16)
{
throw new ArgumentException("Counter must be 16 bytes long for AES block size.");
}
// Initialize AmiiboDump with encrypted data
AmiiboDump amiiboDump = new AmiiboDump(encryptedDumpData, DataKey, TagKey, isLocked: true);
byte[] decryptedData = new byte[encryptedData.Length];
// Unlock (decrypt) the dump
amiiboDump.Unlock();
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = _aesKey;
aesAlg.Mode = CipherMode.ECB; // Use ECB mode to handle the counter encryption
aesAlg.Padding = PaddingMode.None;
// Optional: Verify HMACs
amiiboDump.VerifyHMACs();
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
}
return amiiboDump;
}
}
}

View 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;
}
}
}
}
}

View File

@ -1,69 +1,45 @@
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
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
{
public class AmiiboMasterKey
{
private const int DataLength = 80;
private const int CombinedLength = 160;
public byte[] HmacKey { get; private set; } // 16 bytes
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 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
private AmiiboMasterKey(byte[] data)
public AmiiboMasterKey(byte[] data)
{
if (data.Length != DataLength)
throw new ArgumentException($"Data is {data.Length} bytes (should be {DataLength}).");
if (data.Length != 80)
throw new ArgumentException("Master key data must be 80 bytes.");
// Unpack the data
HmacKey = data[..16];
TypeString = data[16..30];
HmacKey = data.Take(16).ToArray();
TypeString = data.Skip(16).Take(14).ToArray();
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));
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 != CombinedLength)
throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be {CombinedLength}).");
if (combinedBin.Length != 160)
throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be 160).");
byte[] dataBin = combinedBin[..DataLength];
byte[] tagBin = combinedBin[DataLength..];
return FromSeparateBin(dataBin, tagBin);
}
byte[] dataBin = combinedBin.Take(80).ToArray();
byte[] tagBin = combinedBin.Skip(80).Take(80).ToArray();
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;
AmiiboMasterKey dataKey = new AmiiboMasterKey(dataBin);
AmiiboMasterKey tagKey = new AmiiboMasterKey(tagBin);
return (dataKey, tagKey);
}
}
}

View File

@ -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;
}

View File

@ -28,7 +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.Bin;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.UI;
using Ryujinx.Input.HLE;
using Ryujinx.UI.App.Common;
@ -318,6 +318,16 @@ namespace Ryujinx.Ava.UI.ViewModels
OnPropertyChanged();
}
}
public bool IsBinAmiiboRequested
{
get => IsAmiiboRequested && AmiiboBinReader.HasKeyRetailBinPath();
set
{
_isAmiiboRequested = value;
OnPropertyChanged();
}
}
public bool ShowLoadProgress
{

View File

@ -243,11 +243,11 @@
IsEnabled="{Binding IsAmiiboRequested}" />
<MenuItem
Name="ScanAmiiboMenuItemFromBin"
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree"
Click="OpenBinFile"
Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
Icon="{ext:Icon mdi-cube-scan}"
IsEnabled="{Binding IsAmiiboRequested}" />
IsEnabled="{Binding IsBinAmiiboRequested}" />
<MenuItem
Command="{Binding TakeScreenshot}"
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"

View File

@ -11,6 +11,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.UI.App.Common;
using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
@ -166,6 +167,12 @@ namespace Ryujinx.Ava.UI.Views.Main
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)
{
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();