diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index d961e1e6a..3b314daf8 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -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)) diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs index 9bdf9c481..f701c2a30 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs @@ -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 diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs index ffaef6558..8e63c4149 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs @@ -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; } } } 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..16c1e72d3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDump.cs @@ -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 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 index 9ec2d0b80..e8eb0eb7a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs @@ -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); } } } 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/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 83443c6d8..6acbbe230 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -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 { diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 153cfd379..e3fc3d33d 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -243,11 +243,11 @@ IsEnabled="{Binding IsAmiiboRequested}" /> + IsEnabled="{Binding IsBinAmiiboRequested}" />