| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- 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 == 532)
- {
- // add 8 bytes to the end of the file
- byte[] newFileBytes = new byte[totalBytes];
- Array.Copy(fileBytes, newFileBytes, fileBytes.Length);
- fileBytes = newFileBytes;
- }
- AmiiboDecryptor amiiboDecryptor = new(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 = Convert.ToHexString(usedCharacter);
- string variationStr = Convert.ToHexString(variation);
- string amiiboIDStr = Convert.ToHexString(amiiboID);
- string setIDStr = Convert.ToHexString(setID);
- 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;
- }
- if (readBytes.Length == 532)
- {
- // add 8 bytes to the end of the file
- byte[] newFileBytes = new byte[540];
- Array.Copy(readBytes, newFileBytes, readBytes.Length);
- readBytes = newFileBytes;
- }
- AmiiboDecryptor amiiboDecryptor = new AmiiboDecryptor(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;
- }
- if (readBytes.Length == 532)
- {
- // add 8 bytes to the end of the file
- byte[] newFileBytes = new byte[540];
- Array.Copy(readBytes, newFileBytes, readBytes.Length);
- readBytes = newFileBytes;
- }
- AmiiboDecryptor amiiboDecryptor = new AmiiboDecryptor(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{Convert.ToHexString(titleId)}");
- Logger.Debug?.Print(LogClass.ServiceNfp, $"Application Program ID: 0x{Convert.ToHexString(appId)}");
- 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 HasAmiiboKeyFile => 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;
- }
- }
- }
- }
|