diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index 64b08e309..c585aed54 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -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) { diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs new file mode 100644 index 000000000..13a5ef998 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs @@ -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; + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs new file mode 100644 index 000000000..71758c947 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs @@ -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; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDump.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDump.cs new file mode 100644 index 000000000..7343a40ca --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDump.cs @@ -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 seed = new List(); + + // 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; + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs new file mode 100644 index 000000000..f61f8c84d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs @@ -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); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/INfp.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/INfp.cs index 20f67a4ef..3256684f4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/INfp.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/INfp.cs @@ -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; } diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs index e1db98e5f..9450e1db5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs @@ -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 ApplicationAreas { get; set; } } - struct VirtualAmiiboApplicationArea + public struct VirtualAmiiboApplicationArea { public uint ApplicationAreaId { get; set; } public byte[] ApplicationArea { get; set; } diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs index 7cb62ae41..ee43fc307 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs @@ -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")); + } } } diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index a85f77cb9..024a36ad0 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -25,6 +25,7 @@ "MenuBarActions": "_الإجراءات", "MenuBarOptionsSimulateWakeUpMessage": "محاكاة رسالة الاستيقاظ", "MenuBarActionsScanAmiibo": "‫فحص Amiibo", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "_الأدوات", "MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت", "MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP", diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index 8bd301a40..7674968c8 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -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", diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index a9d172e70..9d43b59fb 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -25,6 +25,7 @@ "MenuBarActions": "_Δράσεις", "MenuBarOptionsSimulateWakeUpMessage": "Προσομοίωση Μηνύματος Αφύπνισης", "MenuBarActionsScanAmiibo": "Σάρωση Amiibo", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "_Εργαλεία", "MenuBarToolsInstallFirmware": "Εγκατάσταση Firmware", "MenuBarFileToolsInstallFirmwareFromFile": "Εγκατάσταση Firmware από XCI ή ZIP", diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 8fa02e1b6..382ee7d22 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -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", diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index ab9723589..d8d9169f8 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -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", diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index c50689e4d..56505d96b 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -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", diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json index 240058f96..4c5be45a9 100644 --- a/src/Ryujinx/Assets/Locales/he_IL.json +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -25,6 +25,7 @@ "MenuBarActions": "_פעולות", "MenuBarOptionsSimulateWakeUpMessage": "דמה הודעת השכמה", "MenuBarActionsScanAmiibo": "סרוק אמיבו", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "_כלים", "MenuBarToolsInstallFirmware": "התקן קושחה", "MenuBarFileToolsInstallFirmwareFromFile": "התקן קושחה מקובץ- ZIP/XCI", diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 523c8a46a..b59f79f35 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -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", diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index 3ee14f33d..cdd1d5513 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -25,6 +25,7 @@ "MenuBarActions": "アクション(_A)", "MenuBarOptionsSimulateWakeUpMessage": "スリープ復帰メッセージをシミュレート", "MenuBarActionsScanAmiibo": "Amiibo をスキャン", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "ツール(_T)", "MenuBarToolsInstallFirmware": "ファームウェアをインストール", "MenuBarFileToolsInstallFirmwareFromFile": "XCI または ZIP からファームウェアをインストール", diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index 0e595a6af..f2b7cde0f 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -25,6 +25,7 @@ "MenuBarActions": "_동작", "MenuBarOptionsSimulateWakeUpMessage": "깨우기 메시지 시뮬레이션", "MenuBarActionsScanAmiibo": "Amiibo 스캔", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "_도구", "MenuBarToolsInstallFirmware": "펌웨어 설치", "MenuBarFileToolsInstallFirmwareFromFile": "XCI 또는 ZIP에서 펌웨어 설치", diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index 206e2dde6..4d5017671 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -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", diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index 2b8f74437..784a06795 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -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", diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index 87061e928..f254627bd 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -25,6 +25,7 @@ "MenuBarActions": "_Действия", "MenuBarOptionsSimulateWakeUpMessage": "Имитировать сообщение пробуждения", "MenuBarActionsScanAmiibo": "Сканировать Amiibo", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "_Инструменты", "MenuBarToolsInstallFirmware": "Установка прошивки", "MenuBarFileToolsInstallFirmwareFromFile": "Установить прошивку из XCI или ZIP", diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index e1bf12939..a783629c6 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -25,6 +25,7 @@ "MenuBarActions": "การดำเนินการ", "MenuBarOptionsSimulateWakeUpMessage": "จำลองข้อความปลุก", "MenuBarActionsScanAmiibo": "สแกนหา Amiibo", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "_เครื่องมือ", "MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์", "MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP", diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index 2a4079613..3afb04963 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -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", diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index 9e26c24fa..1e92be4d9 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -25,6 +25,7 @@ "MenuBarActions": "_Дії", "MenuBarOptionsSimulateWakeUpMessage": "Симулювати повідомлення про пробудження", "MenuBarActionsScanAmiibo": "Сканувати Amiibo", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "_Інструменти", "MenuBarToolsInstallFirmware": "Установити прошивку", "MenuBarFileToolsInstallFirmwareFromFile": "Установити прошивку з XCI або ZIP", diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json index f3a5309d8..d26ab98ea 100644 --- a/src/Ryujinx/Assets/Locales/zh_CN.json +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -25,6 +25,7 @@ "MenuBarActions": "操作(_A)", "MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息", "MenuBarActionsScanAmiibo": "扫描 Amiibo", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "工具(_T)", "MenuBarToolsInstallFirmware": "安装系统固件", "MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件", diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index f89a22fbb..d02d8dd14 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -25,6 +25,7 @@ "MenuBarActions": "動作(_A)", "MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息", "MenuBarActionsScanAmiibo": "掃描 Amiibo", + "MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)", "MenuBarTools": "工具(_T)", "MenuBarToolsInstallFirmware": "安裝韌體", "MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體", diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 31bd5576b..5069fc681 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -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 + { + 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) diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 0823c5b7c..3e66023d8 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -171,6 +171,12 @@ Click="OpenAmiiboWindow" Header="{locale:Locale MenuBarActionsScanAmiibo}" IsEnabled="{Binding IsAmiiboRequested}" /> + 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())