AmiiboBinReader.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. using Ryujinx.Common.Configuration;
  2. using Ryujinx.Common.Logging;
  3. using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
  4. using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
  5. using System;
  6. using System.IO;
  7. namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
  8. {
  9. public class AmiiboBinReader
  10. {
  11. private static byte CalculateBCC0(byte[] uid)
  12. {
  13. return (byte)(uid[0] ^ uid[1] ^ uid[2] ^ 0x88);
  14. }
  15. private static byte CalculateBCC1(byte[] uid)
  16. {
  17. return (byte)(uid[3] ^ uid[4] ^ uid[5] ^ uid[6]);
  18. }
  19. public static VirtualAmiiboFile ReadBinFile(byte[] fileBytes)
  20. {
  21. string keyRetailBinPath = GetKeyRetailBinPath();
  22. if (string.IsNullOrEmpty(keyRetailBinPath))
  23. {
  24. return new VirtualAmiiboFile();
  25. }
  26. byte[] initialCounter = new byte[16];
  27. const int totalPages = 135;
  28. const int pageSize = 4;
  29. const int totalBytes = totalPages * pageSize;
  30. if (fileBytes.Length == 532)
  31. {
  32. // add 8 bytes to the end of the file
  33. byte[] newFileBytes = new byte[totalBytes];
  34. Array.Copy(fileBytes, newFileBytes, fileBytes.Length);
  35. fileBytes = newFileBytes;
  36. }
  37. AmiiboDecryptor amiiboDecryptor = new(keyRetailBinPath);
  38. AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(fileBytes);
  39. byte[] titleId = new byte[8];
  40. byte[] usedCharacter = new byte[2];
  41. byte[] variation = new byte[2];
  42. byte[] amiiboID = new byte[2];
  43. byte[] setID = new byte[1];
  44. byte[] initDate = new byte[2];
  45. byte[] writeDate = new byte[2];
  46. byte[] writeCounter = new byte[2];
  47. byte[] appId = new byte[8];
  48. byte[] settingsBytes = new byte[2];
  49. byte formData = 0;
  50. byte[] applicationAreas = new byte[216];
  51. byte[] dataFull = amiiboDump.GetData();
  52. Logger.Debug?.Print(LogClass.ServiceNfp, $"Data Full Length: {dataFull.Length}");
  53. byte[] uid = new byte[7];
  54. Array.Copy(dataFull, 0, uid, 0, 7);
  55. byte bcc0 = CalculateBCC0(uid);
  56. byte bcc1 = CalculateBCC1(uid);
  57. LogDebugData(uid, bcc0, bcc1);
  58. for (int page = 0; page < 128; page++) // NTAG215 has 128 pages
  59. {
  60. int pageStartIdx = page * 4; // Each page is 4 bytes
  61. byte[] pageData = new byte[4];
  62. byte[] sourceBytes = dataFull;
  63. Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
  64. // Special handling for specific pages
  65. switch (page)
  66. {
  67. case 0: // Page 0 (UID + BCC0)
  68. Logger.Debug?.Print(LogClass.ServiceNfp, "Page 0: UID and BCC0.");
  69. break;
  70. case 2: // Page 2 (BCC1 + Internal Value)
  71. byte internalValue = pageData[1];
  72. Logger.Debug?.Print(LogClass.ServiceNfp, $"Page 2: BCC1 + Internal Value 0x{internalValue:X2} (Expected 0x48).");
  73. break;
  74. case 6:
  75. // Bytes 0 and 1 are init date, bytes 2 and 3 are write date
  76. Array.Copy(pageData, 0, initDate, 0, 2);
  77. Array.Copy(pageData, 2, writeDate, 0, 2);
  78. break;
  79. case 21:
  80. // Bytes 0 and 1 are used character, bytes 2 and 3 are variation
  81. Array.Copy(pageData, 0, usedCharacter, 0, 2);
  82. Array.Copy(pageData, 2, variation, 0, 2);
  83. break;
  84. case 22:
  85. // Bytes 0 and 1 are amiibo ID, byte 2 is set ID, byte 3 is form data
  86. Array.Copy(pageData, 0, amiiboID, 0, 2);
  87. setID[0] = pageData[2];
  88. formData = pageData[3];
  89. break;
  90. case 64:
  91. case 65:
  92. // Extract title ID
  93. int titleIdOffset = (page - 64) * 4;
  94. Array.Copy(pageData, 0, titleId, titleIdOffset, 4);
  95. break;
  96. case 66:
  97. // Bytes 0 and 1 are write counter
  98. Array.Copy(pageData, 0, writeCounter, 0, 2);
  99. break;
  100. // Pages 76 to 127 are application areas
  101. case >= 76 and <= 127:
  102. int appAreaOffset = (page - 76) * 4;
  103. Array.Copy(pageData, 0, applicationAreas, appAreaOffset, 4);
  104. break;
  105. }
  106. }
  107. string usedCharacterStr = Convert.ToHexString(usedCharacter);
  108. string variationStr = Convert.ToHexString(variation);
  109. string amiiboIDStr = Convert.ToHexString(amiiboID);
  110. string setIDStr = Convert.ToHexString(setID);
  111. string head = usedCharacterStr + variationStr;
  112. string tail = amiiboIDStr + setIDStr + "02";
  113. string finalID = head + tail;
  114. ushort settingsValue = BitConverter.ToUInt16(settingsBytes, 0);
  115. ushort initDateValue = BitConverter.ToUInt16(initDate, 0);
  116. ushort writeDateValue = BitConverter.ToUInt16(writeDate, 0);
  117. DateTime initDateTime = DateTimeFromTag(initDateValue);
  118. DateTime writeDateTime = DateTimeFromTag(writeDateValue);
  119. ushort writeCounterValue = BitConverter.ToUInt16(writeCounter, 0);
  120. string nickName = amiiboDump.AmiiboNickname;
  121. LogFinalData(titleId, appId, head, tail, finalID, nickName, initDateTime, writeDateTime, settingsValue, writeCounterValue, applicationAreas);
  122. VirtualAmiiboFile virtualAmiiboFile = new VirtualAmiiboFile
  123. {
  124. FileVersion = 1,
  125. TagUuid = uid,
  126. AmiiboId = finalID,
  127. NickName = nickName,
  128. FirstWriteDate = initDateTime,
  129. LastWriteDate = writeDateTime,
  130. WriteCounter = writeCounterValue,
  131. };
  132. if (writeCounterValue > 0)
  133. {
  134. VirtualAmiibo.ApplicationBytes = applicationAreas;
  135. }
  136. VirtualAmiibo.NickName = nickName;
  137. return virtualAmiiboFile;
  138. }
  139. public static bool SaveBinFile(string inputFile, byte[] appData)
  140. {
  141. Logger.Info?.Print(LogClass.ServiceNfp, "Saving bin file.");
  142. byte[] readBytes;
  143. try
  144. {
  145. readBytes = File.ReadAllBytes(inputFile);
  146. }
  147. catch (Exception ex)
  148. {
  149. Logger.Error?.Print(LogClass.ServiceNfp, $"Error reading file: {ex.Message}");
  150. return false;
  151. }
  152. string keyRetailBinPath = GetKeyRetailBinPath();
  153. if (string.IsNullOrEmpty(keyRetailBinPath))
  154. {
  155. Logger.Error?.Print(LogClass.ServiceNfp, "Key retail path is empty.");
  156. return false;
  157. }
  158. if (appData.Length != 216) // Ensure application area size is valid
  159. {
  160. Logger.Error?.Print(LogClass.ServiceNfp, "Invalid application data length. Expected 216 bytes.");
  161. return false;
  162. }
  163. if (readBytes.Length == 532)
  164. {
  165. // add 8 bytes to the end of the file
  166. byte[] newFileBytes = new byte[540];
  167. Array.Copy(readBytes, newFileBytes, readBytes.Length);
  168. readBytes = newFileBytes;
  169. }
  170. AmiiboDecryptor amiiboDecryptor = new AmiiboDecryptor(keyRetailBinPath);
  171. AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(readBytes);
  172. byte[] oldData = amiiboDump.GetData();
  173. if (oldData.Length != 540) // Verify the expected length for NTAG215 tags
  174. {
  175. Logger.Error?.Print(LogClass.ServiceNfp, "Invalid tag data length. Expected 540 bytes.");
  176. return false;
  177. }
  178. byte[] newData = new byte[oldData.Length];
  179. Array.Copy(oldData, newData, oldData.Length);
  180. // Replace application area with appData
  181. int appAreaOffset = 76 * 4; // Starting page (76) times 4 bytes per page
  182. Array.Copy(appData, 0, newData, appAreaOffset, appData.Length);
  183. AmiiboDump encryptedDump = amiiboDecryptor.EncryptAmiiboDump(newData);
  184. byte[] encryptedData = encryptedDump.GetData();
  185. if (encryptedData == null || encryptedData.Length != readBytes.Length)
  186. {
  187. Logger.Error?.Print(LogClass.ServiceNfp, "Failed to encrypt data correctly.");
  188. return false;
  189. }
  190. inputFile = inputFile.Replace("_modified", string.Empty);
  191. // Save the encrypted data to file or return it for saving externally
  192. string outputFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_modified.bin");
  193. try
  194. {
  195. File.WriteAllBytes(outputFilePath, encryptedData);
  196. Logger.Info?.Print(LogClass.ServiceNfp, $"Modified Amiibo data saved to {outputFilePath}.");
  197. return true;
  198. }
  199. catch (Exception ex)
  200. {
  201. Logger.Error?.Print(LogClass.ServiceNfp, $"Error saving file: {ex.Message}");
  202. return false;
  203. }
  204. }
  205. public static bool SaveBinFile(string inputFile, string newNickName)
  206. {
  207. Logger.Info?.Print(LogClass.ServiceNfp, "Saving bin file.");
  208. byte[] readBytes;
  209. try
  210. {
  211. readBytes = File.ReadAllBytes(inputFile);
  212. }
  213. catch (Exception ex)
  214. {
  215. Logger.Error?.Print(LogClass.ServiceNfp, $"Error reading file: {ex.Message}");
  216. return false;
  217. }
  218. string keyRetailBinPath = GetKeyRetailBinPath();
  219. if (string.IsNullOrEmpty(keyRetailBinPath))
  220. {
  221. Logger.Error?.Print(LogClass.ServiceNfp, "Key retail path is empty.");
  222. return false;
  223. }
  224. if (readBytes.Length == 532)
  225. {
  226. // add 8 bytes to the end of the file
  227. byte[] newFileBytes = new byte[540];
  228. Array.Copy(readBytes, newFileBytes, readBytes.Length);
  229. readBytes = newFileBytes;
  230. }
  231. AmiiboDecryptor amiiboDecryptor = new AmiiboDecryptor(keyRetailBinPath);
  232. AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(readBytes);
  233. amiiboDump.AmiiboNickname = newNickName;
  234. byte[] oldData = amiiboDump.GetData();
  235. if (oldData.Length != 540) // Verify the expected length for NTAG215 tags
  236. {
  237. Logger.Error?.Print(LogClass.ServiceNfp, "Invalid tag data length. Expected 540 bytes.");
  238. return false;
  239. }
  240. byte[] encryptedData = amiiboDecryptor.EncryptAmiiboDump(oldData).GetData();
  241. if (encryptedData == null || encryptedData.Length != readBytes.Length)
  242. {
  243. Logger.Error?.Print(LogClass.ServiceNfp, "Failed to encrypt data correctly.");
  244. return false;
  245. }
  246. inputFile = inputFile.Replace("_modified", string.Empty);
  247. // Save the encrypted data to file or return it for saving externally
  248. string outputFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_modified.bin");
  249. try
  250. {
  251. File.WriteAllBytes(outputFilePath, encryptedData);
  252. Logger.Info?.Print(LogClass.ServiceNfp, $"Modified Amiibo data saved to {outputFilePath}.");
  253. return true;
  254. }
  255. catch (Exception ex)
  256. {
  257. Logger.Error?.Print(LogClass.ServiceNfp, $"Error saving file: {ex.Message}");
  258. return false;
  259. }
  260. }
  261. private static void LogDebugData(byte[] uid, byte bcc0, byte bcc1)
  262. {
  263. Logger.Debug?.Print(LogClass.ServiceNfp, $"UID: {BitConverter.ToString(uid)}");
  264. Logger.Debug?.Print(LogClass.ServiceNfp, $"BCC0: 0x{bcc0:X2}, BCC1: 0x{bcc1:X2}");
  265. }
  266. 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)
  267. {
  268. Logger.Debug?.Print(LogClass.ServiceNfp, $"Title ID: 0x{Convert.ToHexString(titleId)}");
  269. Logger.Debug?.Print(LogClass.ServiceNfp, $"Application Program ID: 0x{Convert.ToHexString(appId)}");
  270. Logger.Debug?.Print(LogClass.ServiceNfp, $"Head: {head}");
  271. Logger.Debug?.Print(LogClass.ServiceNfp, $"Tail: {tail}");
  272. Logger.Debug?.Print(LogClass.ServiceNfp, $"Final ID: {finalID}");
  273. Logger.Debug?.Print(LogClass.ServiceNfp, $"Nickname: {nickName}");
  274. Logger.Debug?.Print(LogClass.ServiceNfp, $"Init Date: {initDateTime}");
  275. Logger.Debug?.Print(LogClass.ServiceNfp, $"Write Date: {writeDateTime}");
  276. Logger.Debug?.Print(LogClass.ServiceNfp, $"Settings: 0x{settingsValue:X4}");
  277. Logger.Debug?.Print(LogClass.ServiceNfp, $"Write Counter: {writeCounterValue}");
  278. Logger.Debug?.Print(LogClass.ServiceNfp, "Length of Application Areas: " + applicationAreas.Length);
  279. }
  280. private static uint CalculateCRC32(byte[] input)
  281. {
  282. uint[] table = new uint[256];
  283. uint polynomial = 0xEDB88320;
  284. for (uint i = 0; i < table.Length; ++i)
  285. {
  286. uint crc = i;
  287. for (int j = 0; j < 8; ++j)
  288. {
  289. if ((crc & 1) != 0)
  290. crc = (crc >> 1) ^ polynomial;
  291. else
  292. crc >>= 1;
  293. }
  294. table[i] = crc;
  295. }
  296. uint result = 0xFFFFFFFF;
  297. foreach (byte b in input)
  298. {
  299. byte index = (byte)((result & 0xFF) ^ b);
  300. result = (result >> 8) ^ table[index];
  301. }
  302. return ~result;
  303. }
  304. private static string GetKeyRetailBinPath()
  305. {
  306. return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
  307. }
  308. public static bool HasAmiiboKeyFile => File.Exists(GetKeyRetailBinPath());
  309. public static DateTime DateTimeFromTag(ushort value)
  310. {
  311. try
  312. {
  313. int day = value & 0x1F;
  314. int month = (value >> 5) & 0x0F;
  315. int year = (value >> 9) & 0x7F;
  316. if (day == 0 || month == 0 || month > 12 || day > DateTime.DaysInMonth(2000 + year, month))
  317. throw new ArgumentOutOfRangeException();
  318. return new DateTime(2000 + year, month, day);
  319. }
  320. catch
  321. {
  322. return DateTime.Now;
  323. }
  324. }
  325. }
  326. }