nfc_device.c 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. #include "nfc_device.h"
  2. #include "nfc_types.h"
  3. #include <lib/toolbox/path.h>
  4. #include <lib/toolbox/hex.h>
  5. #include "protocols/nfc_util.h"
  6. #include <flipper_format/flipper_format.h>
  7. #define TAG "NfcDevice"
  8. #define NFC_DEVICE_KEYS_FOLDER EXT_PATH("nfc/.cache")
  9. #define NFC_DEVICE_KEYS_EXTENSION ".keys"
  10. static const char* nfc_file_header = "Flipper NFC device";
  11. static const uint32_t nfc_file_version = 3;
  12. // Protocols format versions
  13. static const uint32_t nfc_mifare_ultralight_data_format_version = 1;
  14. NfcDevice* nfc_device_alloc() {
  15. NfcDevice* nfc_dev = malloc(sizeof(NfcDevice));
  16. nfc_dev->storage = furi_record_open(RECORD_STORAGE);
  17. nfc_dev->dialogs = furi_record_open(RECORD_DIALOGS);
  18. nfc_dev->load_path = furi_string_alloc();
  19. nfc_dev->dev_data.parsed_data = furi_string_alloc();
  20. nfc_dev->folder = furi_string_alloc();
  21. return nfc_dev;
  22. }
  23. void nfc_device_free(NfcDevice* nfc_dev) {
  24. furi_assert(nfc_dev);
  25. nfc_device_clear(nfc_dev);
  26. furi_record_close(RECORD_STORAGE);
  27. furi_record_close(RECORD_DIALOGS);
  28. furi_string_free(nfc_dev->load_path);
  29. if(nfc_dev->dev_data.parsed_data != NULL) {
  30. furi_string_free(nfc_dev->dev_data.parsed_data);
  31. }
  32. furi_string_free(nfc_dev->folder);
  33. free(nfc_dev);
  34. }
  35. static void nfc_device_prepare_format_string(NfcDevice* dev, FuriString* format_string) {
  36. if(dev->format == NfcDeviceSaveFormatUid) {
  37. furi_string_set(format_string, "UID");
  38. } else if(dev->format == NfcDeviceSaveFormatBankCard) {
  39. furi_string_set(format_string, "Bank card");
  40. } else if(dev->format == NfcDeviceSaveFormatMifareUl) {
  41. furi_string_set(format_string, nfc_mf_ul_type(dev->dev_data.mf_ul_data.type, true));
  42. } else if(dev->format == NfcDeviceSaveFormatMifareClassic) {
  43. furi_string_set(format_string, "Mifare Classic");
  44. } else if(dev->format == NfcDeviceSaveFormatMifareDesfire) {
  45. furi_string_set(format_string, "Mifare DESFire");
  46. } else if(dev->format == NfcDeviceSaveFormatNfcV) {
  47. furi_string_set(format_string, "ISO15693");
  48. } else {
  49. furi_string_set(format_string, "Unknown");
  50. }
  51. }
  52. static bool nfc_device_parse_format_string(NfcDevice* dev, FuriString* format_string) {
  53. if(furi_string_start_with_str(format_string, "UID")) {
  54. dev->format = NfcDeviceSaveFormatUid;
  55. dev->dev_data.protocol = NfcDeviceProtocolUnknown;
  56. return true;
  57. }
  58. if(furi_string_start_with_str(format_string, "Bank card")) {
  59. dev->format = NfcDeviceSaveFormatBankCard;
  60. dev->dev_data.protocol = NfcDeviceProtocolEMV;
  61. return true;
  62. }
  63. // Check Mifare Ultralight types
  64. for(MfUltralightType type = MfUltralightTypeUnknown; type < MfUltralightTypeNum; type++) {
  65. if(furi_string_equal(format_string, nfc_mf_ul_type(type, true))) {
  66. dev->format = NfcDeviceSaveFormatMifareUl;
  67. dev->dev_data.protocol = NfcDeviceProtocolMifareUl;
  68. dev->dev_data.mf_ul_data.type = type;
  69. return true;
  70. }
  71. }
  72. if(furi_string_start_with_str(format_string, "Mifare Classic")) {
  73. dev->format = NfcDeviceSaveFormatMifareClassic;
  74. dev->dev_data.protocol = NfcDeviceProtocolMifareClassic;
  75. return true;
  76. }
  77. if(furi_string_start_with_str(format_string, "Mifare DESFire")) {
  78. dev->format = NfcDeviceSaveFormatMifareDesfire;
  79. dev->dev_data.protocol = NfcDeviceProtocolMifareDesfire;
  80. return true;
  81. }
  82. if(furi_string_start_with_str(format_string, "ISO15693")) {
  83. dev->format = NfcDeviceSaveFormatNfcV;
  84. dev->dev_data.protocol = NfcDeviceProtocolNfcV;
  85. return true;
  86. }
  87. return false;
  88. }
  89. static bool nfc_device_save_mifare_ul_data(FlipperFormat* file, NfcDevice* dev) {
  90. bool saved = false;
  91. MfUltralightData* data = &dev->dev_data.mf_ul_data;
  92. FuriString* temp_str;
  93. temp_str = furi_string_alloc();
  94. // Save Mifare Ultralight specific data
  95. do {
  96. if(!flipper_format_write_comment_cstr(file, "Mifare Ultralight specific data")) break;
  97. if(!flipper_format_write_uint32(
  98. file, "Data format version", &nfc_mifare_ultralight_data_format_version, 1))
  99. break;
  100. if(!flipper_format_write_hex(file, "Signature", data->signature, sizeof(data->signature)))
  101. break;
  102. if(!flipper_format_write_hex(
  103. file, "Mifare version", (uint8_t*)&data->version, sizeof(data->version)))
  104. break;
  105. // Write conters and tearing flags data
  106. bool counters_saved = true;
  107. for(uint8_t i = 0; i < 3; i++) {
  108. furi_string_printf(temp_str, "Counter %d", i);
  109. if(!flipper_format_write_uint32(
  110. file, furi_string_get_cstr(temp_str), &data->counter[i], 1)) {
  111. counters_saved = false;
  112. break;
  113. }
  114. furi_string_printf(temp_str, "Tearing %d", i);
  115. if(!flipper_format_write_hex(
  116. file, furi_string_get_cstr(temp_str), &data->tearing[i], 1)) {
  117. counters_saved = false;
  118. break;
  119. }
  120. }
  121. if(!counters_saved) break;
  122. // Write pages data
  123. uint32_t pages_total = data->data_size / 4;
  124. if(!flipper_format_write_uint32(file, "Pages total", &pages_total, 1)) break;
  125. uint32_t pages_read = data->data_read / 4;
  126. if(!flipper_format_write_uint32(file, "Pages read", &pages_read, 1)) break;
  127. bool pages_saved = true;
  128. for(uint16_t i = 0; i < data->data_size; i += 4) {
  129. furi_string_printf(temp_str, "Page %d", i / 4);
  130. if(!flipper_format_write_hex(file, furi_string_get_cstr(temp_str), &data->data[i], 4)) {
  131. pages_saved = false;
  132. break;
  133. }
  134. }
  135. if(!pages_saved) break;
  136. // Write authentication counter
  137. uint32_t auth_counter = data->curr_authlim;
  138. if(!flipper_format_write_uint32(file, "Failed authentication attempts", &auth_counter, 1))
  139. break;
  140. saved = true;
  141. } while(false);
  142. furi_string_free(temp_str);
  143. return saved;
  144. }
  145. bool nfc_device_load_mifare_ul_data(FlipperFormat* file, NfcDevice* dev) {
  146. bool parsed = false;
  147. MfUltralightData* data = &dev->dev_data.mf_ul_data;
  148. FuriString* temp_str;
  149. temp_str = furi_string_alloc();
  150. uint32_t data_format_version = 0;
  151. do {
  152. // Read Mifare Ultralight format version
  153. if(!flipper_format_read_uint32(file, "Data format version", &data_format_version, 1)) {
  154. if(!flipper_format_rewind(file)) break;
  155. }
  156. // Read signature
  157. if(!flipper_format_read_hex(file, "Signature", data->signature, sizeof(data->signature)))
  158. break;
  159. // Read Mifare version
  160. if(!flipper_format_read_hex(
  161. file, "Mifare version", (uint8_t*)&data->version, sizeof(data->version)))
  162. break;
  163. // Read counters and tearing flags
  164. bool counters_parsed = true;
  165. for(uint8_t i = 0; i < 3; i++) {
  166. furi_string_printf(temp_str, "Counter %d", i);
  167. if(!flipper_format_read_uint32(
  168. file, furi_string_get_cstr(temp_str), &data->counter[i], 1)) {
  169. counters_parsed = false;
  170. break;
  171. }
  172. furi_string_printf(temp_str, "Tearing %d", i);
  173. if(!flipper_format_read_hex(
  174. file, furi_string_get_cstr(temp_str), &data->tearing[i], 1)) {
  175. counters_parsed = false;
  176. break;
  177. }
  178. }
  179. if(!counters_parsed) break;
  180. // Read pages
  181. uint32_t pages_total = 0;
  182. if(!flipper_format_read_uint32(file, "Pages total", &pages_total, 1)) break;
  183. uint32_t pages_read = 0;
  184. if(data_format_version < nfc_mifare_ultralight_data_format_version) {
  185. pages_read = pages_total;
  186. } else {
  187. if(!flipper_format_read_uint32(file, "Pages read", &pages_read, 1)) break;
  188. }
  189. data->data_size = pages_total * 4;
  190. data->data_read = pages_read * 4;
  191. if(data->data_size > MF_UL_MAX_DUMP_SIZE || data->data_read > MF_UL_MAX_DUMP_SIZE) break;
  192. bool pages_parsed = true;
  193. for(uint16_t i = 0; i < pages_total; i++) {
  194. furi_string_printf(temp_str, "Page %d", i);
  195. if(!flipper_format_read_hex(
  196. file, furi_string_get_cstr(temp_str), &data->data[i * 4], 4)) {
  197. pages_parsed = false;
  198. break;
  199. }
  200. }
  201. if(!pages_parsed) break;
  202. // Read authentication counter
  203. uint32_t auth_counter;
  204. if(!flipper_format_read_uint32(file, "Failed authentication attempts", &auth_counter, 1))
  205. auth_counter = 0;
  206. data->curr_authlim = auth_counter;
  207. data->auth_success = mf_ul_is_full_capture(data);
  208. parsed = true;
  209. } while(false);
  210. furi_string_free(temp_str);
  211. return parsed;
  212. }
  213. void nfc_device_set_name(NfcDevice* dev, const char* name) {
  214. furi_assert(dev);
  215. strlcpy(dev->dev_name, name, NFC_DEV_NAME_MAX_LEN);
  216. }
  217. static void nfc_device_get_path_without_ext(FuriString* orig_path, FuriString* shadow_path) {
  218. // TODO: this won't work if there is ".nfc" anywhere in the path other than
  219. // at the end
  220. size_t ext_start = furi_string_search(orig_path, NFC_APP_FILENAME_EXTENSION);
  221. furi_string_set_n(shadow_path, orig_path, 0, ext_start);
  222. }
  223. static void nfc_device_get_shadow_path(FuriString* orig_path, FuriString* shadow_path) {
  224. nfc_device_get_path_without_ext(orig_path, shadow_path);
  225. furi_string_cat_printf(shadow_path, "%s", NFC_APP_SHADOW_EXTENSION);
  226. }
  227. static void nfc_device_get_folder_from_path(FuriString* path, FuriString* folder) {
  228. size_t last_slash = furi_string_search_rchar(path, '/');
  229. if(last_slash == FURI_STRING_FAILURE) {
  230. // No slashes in the path, treat the whole path as a folder
  231. furi_string_set(folder, path);
  232. } else {
  233. furi_string_set_n(folder, path, 0, last_slash);
  234. }
  235. }
  236. bool nfc_device_save(NfcDevice* dev, const char* dev_name) {
  237. return false;
  238. furi_assert(dev);
  239. bool saved = false;
  240. FlipperFormat* file = flipper_format_file_alloc(dev->storage);
  241. FurryHalNfcDevData* data = &dev->dev_data.nfc_data;
  242. FuriString* temp_str;
  243. temp_str = furi_string_alloc();
  244. do {
  245. // Create directory if necessary
  246. FuriString* folder = furi_string_alloc();
  247. // Get folder from filename (filename is in the form of "folder/filename.nfc", so the folder is "folder/")
  248. furi_string_set(temp_str, dev_name);
  249. // Get folder from filename
  250. nfc_device_get_folder_from_path(temp_str, folder);
  251. FURI_LOG_I("Nfc", "Saving to folder %s", furi_string_get_cstr(folder));
  252. if(!storage_simply_mkdir(dev->storage, furi_string_get_cstr(folder))) {
  253. FURI_LOG_E("Nfc", "Failed to create folder %s", furi_string_get_cstr(folder));
  254. break;
  255. }
  256. furi_string_free(folder);
  257. // First remove nfc device file if it was saved
  258. // Open file
  259. if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break;
  260. // Write header
  261. if(!flipper_format_write_header_cstr(file, nfc_file_header, nfc_file_version)) break;
  262. // Write nfc device type
  263. if(!flipper_format_write_comment_cstr(
  264. file, "Nfc device type can be UID, Mifare Ultralight, Mifare Classic or ISO15693"))
  265. break;
  266. nfc_device_prepare_format_string(dev, temp_str);
  267. if(!flipper_format_write_string(file, "Device type", temp_str)) break;
  268. // Write UID
  269. if(!flipper_format_write_comment_cstr(file, "UID is common for all formats")) break;
  270. if(!flipper_format_write_hex(file, "UID", data->uid, data->uid_len)) break;
  271. if(dev->format != NfcDeviceSaveFormatNfcV) {
  272. // Write ATQA, SAK
  273. if(!flipper_format_write_comment_cstr(file, "ISO14443 specific fields")) break;
  274. // Save ATQA in MSB order for correct companion apps display
  275. uint8_t atqa[2] = {data->atqa[1], data->atqa[0]};
  276. if(!flipper_format_write_hex(file, "ATQA", atqa, 2)) break;
  277. if(!flipper_format_write_hex(file, "SAK", &data->sak, 1)) break;
  278. }
  279. // Save more data if necessary
  280. if(dev->format == NfcDeviceSaveFormatMifareUl) {
  281. if(!nfc_device_save_mifare_ul_data(file, dev)) break;
  282. }
  283. saved = true;
  284. } while(0);
  285. if(!saved) { //-V547
  286. dialog_message_show_storage_error(dev->dialogs, "Can not save\nkey file");
  287. }
  288. furi_string_free(temp_str);
  289. flipper_format_free(file);
  290. return saved;
  291. }
  292. bool nfc_device_save_shadow(NfcDevice* dev, const char* path) {
  293. return false;
  294. dev->shadow_file_exist = true;
  295. // Replace extension from .nfc to .shd if necessary
  296. FuriString* orig_path = furi_string_alloc();
  297. furi_string_set_str(orig_path, path);
  298. FuriString* shadow_path = furi_string_alloc();
  299. nfc_device_get_shadow_path(orig_path, shadow_path);
  300. bool file_saved = nfc_device_save(dev, furi_string_get_cstr(shadow_path));
  301. furi_string_free(orig_path);
  302. furi_string_free(shadow_path);
  303. return file_saved;
  304. }
  305. static bool nfc_device_load_data(NfcDevice* dev, FuriString* path, bool show_dialog) {
  306. bool parsed = false;
  307. FlipperFormat* file = flipper_format_file_alloc(dev->storage);
  308. FurryHalNfcDevData* data = &dev->dev_data.nfc_data;
  309. uint32_t data_cnt = 0;
  310. FuriString* temp_str;
  311. temp_str = furi_string_alloc();
  312. bool deprecated_version = false;
  313. // Version 2 of file format had ATQA bytes swapped
  314. uint32_t version_with_lsb_atqa = 2;
  315. if(dev->loading_cb) {
  316. dev->loading_cb(dev->loading_cb_ctx, true);
  317. }
  318. do {
  319. // Check existence of shadow file
  320. nfc_device_get_shadow_path(path, temp_str);
  321. dev->shadow_file_exist =
  322. storage_common_stat(dev->storage, furi_string_get_cstr(temp_str), NULL) == FSE_OK;
  323. // Open shadow file if it exists. If not - open original
  324. if(dev->shadow_file_exist) {
  325. if(!flipper_format_file_open_existing(file, furi_string_get_cstr(temp_str))) break;
  326. } else {
  327. if(!flipper_format_file_open_existing(file, furi_string_get_cstr(path))) break;
  328. }
  329. // Read and verify file header
  330. uint32_t version = 0;
  331. if(!flipper_format_read_header(file, temp_str, &version)) break;
  332. if(furi_string_cmp_str(temp_str, nfc_file_header)) break;
  333. if(version != nfc_file_version) {
  334. if(version < version_with_lsb_atqa) {
  335. deprecated_version = true;
  336. break;
  337. }
  338. }
  339. // Read Nfc device type
  340. if(!flipper_format_read_string(file, "Device type", temp_str)) break;
  341. if(!nfc_device_parse_format_string(dev, temp_str)) break;
  342. // Read and parse UID, ATQA and SAK
  343. if(!flipper_format_get_value_count(file, "UID", &data_cnt)) break;
  344. if(!(data_cnt == 4 || data_cnt == 7 || data_cnt == 8)) break;
  345. data->uid_len = data_cnt;
  346. if(!flipper_format_read_hex(file, "UID", data->uid, data->uid_len)) break;
  347. if(dev->format != NfcDeviceSaveFormatNfcV) {
  348. if(version == version_with_lsb_atqa) {
  349. if(!flipper_format_read_hex(file, "ATQA", data->atqa, 2)) break;
  350. } else {
  351. uint8_t atqa[2] = {};
  352. if(!flipper_format_read_hex(file, "ATQA", atqa, 2)) break;
  353. data->atqa[0] = atqa[1];
  354. data->atqa[1] = atqa[0];
  355. }
  356. if(!flipper_format_read_hex(file, "SAK", &data->sak, 1)) break;
  357. }
  358. // Load CUID
  359. uint8_t* cuid_start = data->uid;
  360. if(data->uid_len == 7) {
  361. cuid_start = &data->uid[3];
  362. }
  363. data->cuid = (cuid_start[0] << 24) | (cuid_start[1] << 16) | (cuid_start[2] << 8) |
  364. (cuid_start[3]);
  365. // Parse other data
  366. if(dev->format == NfcDeviceSaveFormatMifareUl) {
  367. if(!nfc_device_load_mifare_ul_data(file, dev)) break;
  368. }
  369. parsed = true;
  370. } while(false);
  371. if(dev->loading_cb) {
  372. dev->loading_cb(dev->loading_cb_ctx, false);
  373. }
  374. if((!parsed) && (show_dialog)) {
  375. if(deprecated_version) {
  376. dialog_message_show_storage_error(dev->dialogs, "File format deprecated");
  377. } else {
  378. dialog_message_show_storage_error(dev->dialogs, "Can not parse\nfile");
  379. }
  380. }
  381. furi_string_free(temp_str);
  382. flipper_format_free(file);
  383. return parsed;
  384. }
  385. bool nfc_device_load(NfcDevice* dev, const char* file_path, bool show_dialog) {
  386. furi_assert(dev);
  387. furi_assert(file_path);
  388. // Load device data
  389. furi_string_set(dev->load_path, file_path);
  390. bool dev_load = nfc_device_load_data(dev, dev->load_path, show_dialog);
  391. if(dev_load) {
  392. // Set device name
  393. FuriString* filename;
  394. filename = furi_string_alloc();
  395. path_extract_filename_no_ext(file_path, filename);
  396. nfc_device_set_name(dev, furi_string_get_cstr(filename));
  397. furi_string_free(filename);
  398. }
  399. return dev_load;
  400. }
  401. void nfc_device_data_clear(NfcDeviceData* dev_data) {
  402. if(dev_data->protocol == NfcDeviceProtocolMifareUl) {
  403. mf_ul_reset(&dev_data->mf_ul_data);
  404. }
  405. memset(&dev_data->nfc_data, 0, sizeof(FurryHalNfcDevData));
  406. dev_data->protocol = NfcDeviceProtocolUnknown;
  407. if(dev_data->parsed_data != NULL) {
  408. furi_string_reset(dev_data->parsed_data);
  409. }
  410. }
  411. void nfc_device_clear(NfcDevice* dev) {
  412. furi_assert(dev);
  413. nfc_device_set_name(dev, "");
  414. nfc_device_data_clear(&dev->dev_data);
  415. dev->format = NfcDeviceSaveFormatUid;
  416. furi_string_reset(dev->load_path);
  417. }
  418. bool nfc_device_delete(NfcDevice* dev, bool use_load_path) {
  419. furi_assert(dev);
  420. bool deleted = false;
  421. FuriString* file_path;
  422. file_path = furi_string_alloc();
  423. do {
  424. // Delete original file
  425. if(use_load_path && !furi_string_empty(dev->load_path)) {
  426. furi_string_set(file_path, dev->load_path);
  427. } else {
  428. furi_string_printf(
  429. file_path,
  430. "%s/%s%s",
  431. furi_string_get_cstr(dev->folder),
  432. dev->dev_name,
  433. NFC_APP_FILENAME_EXTENSION);
  434. }
  435. if(!storage_simply_remove(dev->storage, furi_string_get_cstr(file_path))) break;
  436. // Delete shadow file if it exists
  437. if(dev->shadow_file_exist) {
  438. if(use_load_path && !furi_string_empty(dev->load_path)) {
  439. nfc_device_get_shadow_path(dev->load_path, file_path);
  440. } else {
  441. furi_string_printf(
  442. file_path,
  443. "%s/%s%s",
  444. furi_string_get_cstr(dev->folder),
  445. dev->dev_name,
  446. NFC_APP_SHADOW_EXTENSION);
  447. }
  448. if(!storage_simply_remove(dev->storage, furi_string_get_cstr(file_path))) break;
  449. }
  450. deleted = true;
  451. } while(0);
  452. if(!deleted) {
  453. dialog_message_show_storage_error(dev->dialogs, "Can not remove file");
  454. }
  455. furi_string_free(file_path);
  456. return deleted;
  457. }
  458. bool nfc_device_restore(NfcDevice* dev, bool use_load_path) {
  459. furi_assert(dev);
  460. furi_assert(dev->shadow_file_exist);
  461. bool restored = false;
  462. FuriString* path;
  463. path = furi_string_alloc();
  464. do {
  465. if(use_load_path && !furi_string_empty(dev->load_path)) {
  466. nfc_device_get_shadow_path(dev->load_path, path);
  467. } else {
  468. furi_string_printf(
  469. path,
  470. "%s/%s%s",
  471. furi_string_get_cstr(dev->folder),
  472. dev->dev_name,
  473. NFC_APP_SHADOW_EXTENSION);
  474. }
  475. if(!storage_simply_remove(dev->storage, furi_string_get_cstr(path))) break;
  476. dev->shadow_file_exist = false;
  477. if(use_load_path && !furi_string_empty(dev->load_path)) {
  478. furi_string_set(path, dev->load_path);
  479. } else {
  480. furi_string_printf(
  481. path,
  482. "%s/%s%s",
  483. furi_string_get_cstr(dev->folder),
  484. dev->dev_name,
  485. NFC_APP_FILENAME_EXTENSION);
  486. }
  487. if(!nfc_device_load_data(dev, path, true)) break;
  488. restored = true;
  489. } while(0);
  490. furi_string_free(path);
  491. return restored;
  492. }
  493. void nfc_device_set_loading_callback(NfcDevice* dev, NfcLoadingCallback callback, void* context) {
  494. furi_assert(dev);
  495. dev->loading_cb = callback;
  496. dev->loading_cb_ctx = context;
  497. }