diff --git a/README.md b/README.md index 9bacc96..1d529bb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TagTinker V2.0 +# TagTinker V2.1

Infrared ESL Research Toolkit for Flipper Zero
@@ -29,6 +29,7 @@ This tool is built for IoT security curiosity, learning about obscure protocols, - **TagTinker Flipper App:** High-performance, zero-allocation RLE streaming IR engine. - **TagTinker Android Companion:** Edit and dither images directly on your phone and sync them instantly to the Flipper Zero over BLE. +- **NFC Tag Scan:** Instantly identify ESL targets by scanning their NFC tag — no manual barcode entry needed. - Display text, custom images, and test-patterns. - Support for monochrome and accent-color (red/yellow) graphics tags. @@ -61,6 +62,8 @@ To understand the underlying protocol, signal structure, and history, please rea - **Furrtek’s ESL research:** [https://www.furrtek.org/?a=esl](https://www.furrtek.org/?a=esl) - **PrecIR reference implementation:** [https://github.com/furrtek/PrecIR](https://github.com/furrtek/PrecIR) +NFC tag decoding contributed by **7h30th3r0n3**. + ## Disclaimer > [!CAUTION] diff --git a/application.fam b/application.fam index 5f73695..53a1021 100644 --- a/application.fam +++ b/application.fam @@ -3,13 +3,13 @@ App( name="TagTinker", apptype=FlipperAppType.EXTERNAL, entry_point="tagtinker_app_main", - requires=["gui", "notification", "dialogs", "storage", "bt"], + requires=["gui", "notification", "dialogs", "storage", "bt", "nfc"], stack_size=12 * 1024, fap_icon="tagtinker_10px.png", fap_category="Infrared", fap_author="i12bp8", fap_description="Educational ESL study tool for owned hardware", - fap_version="2.0", + fap_version="2.1", sources=[ "tagtinker_app.c", "ir/tagtinker_ir.c", @@ -32,5 +32,7 @@ App( "scenes/tagtinker_scene_text_box.c", "scenes/tagtinker_scene_warning.c", "views/numlock_input.c", + "nfc/tagtinker_nfc.c", + "scenes/tagtinker_scene_nfc_scan.c", ], ) diff --git a/nfc/tagtinker_nfc.c b/nfc/tagtinker_nfc.c new file mode 100644 index 0000000..d972a61 --- /dev/null +++ b/nfc/tagtinker_nfc.c @@ -0,0 +1,108 @@ +/* + * TagTinker — ESL NFC tag decoder (implementation) + * + * ESL tags contain an NDEF URI whose last 10 characters + * encode the ESL ID using a custom base64 alphabet. + * This module decodes them into the 17-char barcode format + * expected by tagtinker_barcode_to_plid(). + */ + +#include "tagtinker_nfc.h" +#include + +/* Direct ASCII-to-index lookup table, -1 = not in alphabet */ +static const int8_t CHAR_LUT[128] = { + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,35,-1,-1, + 28,24,30,38,58, 5,23, 6, 3,40,-1,-1,-1,-1,-1,-1, + -1,20,15,54,16,44,46,63, 4,48,34,19,37, 0,26, 1, + 8,41,31, 2,45,55,60,12,11,57,33,-1,-1,-1,-1,50, + -1,13,39, 9,43,18,29,52,59, 7,61,62,14,25,32,56, + 42,47,53,22,36,49,10,21,17,27,51,-1,-1,-1,-1,-1, +}; + +static int alphabet_index(char c) { + uint8_t idx = (uint8_t)c; + if(idx >= 128) return -1; + return CHAR_LUT[idx]; +} + +static uint32_t decode_b64(const char* s, int len) { + uint32_t r = 0; + for(int i = 0; i < len; i++) { + int idx = alphabet_index(s[(len - 1) - i]); + if(idx < 0) return 0; + r = (r * 64) + (uint32_t)idx; + } + return r; +} + +static bool decode_tag(const char* tag, char barcode[18]) { + if(strlen(tag) != 10) return false; + + for(int i = 0; i < 10; i++) { + if(alphabet_index(tag[i]) < 0) return false; + } + + uint32_t val1 = decode_b64(tag + 5, 5); + uint32_t val2 = decode_b64(tag, 5); + + char raw[20]; + snprintf(raw, sizeof(raw), "%09lu%09lu", (unsigned long)val1, (unsigned long)val2); + + int lc = (raw[0] - '0') * 10 + (raw[1] - '0'); + if(lc > 25) return false; + char letter = (char)(lc + 65); + + barcode[0] = letter; + memcpy(barcode + 1, raw + 2, 16); + barcode[17] = '\0'; + + if(barcode[1] != '4') return false; + + int cs = 0; + for(int i = 0; i < 16; i++) { + char c = barcode[i]; + cs += (c >= 'a' && c <= 'z') ? (c - 32) : c; + } + return (cs % 10) == (barcode[16] - '0'); +} + +static bool extract_from_pages(const MfUltralightData* mfu, char barcode[18]) { + if(mfu->pages_read < 11) return false; + + const uint8_t* p3 = mfu->page[3].data; + if(p3[0] != 0xE1) return false; + + const uint8_t* p4 = mfu->page[4].data; + if(p4[0] != 0x03) return false; + uint8_t ndef_len = p4[1]; + if(ndef_len < 5) return false; + + uint8_t flat[28]; + for(int i = 0; i < 7; i++) { + memcpy(flat + i * 4, mfu->page[4 + i].data, 4); + } + + int payload_end = 6 + (ndef_len - 4); + if(payload_end > 28) payload_end = 28; + + char url_body[40] = {0}; + int j = 0; + for(int i = 6; i < payload_end && j < 39; i++) { + if(flat[i] == 0xFE) break; + url_body[j++] = (char)flat[i]; + } + + const char* last_slash = strrchr(url_body, '/'); + if(!last_slash) return false; + + return decode_tag(last_slash + 1, barcode); +} + +bool tagtinker_nfc_decode_barcode(const MfUltralightData* mfu_data, char barcode[18]) { + if(!mfu_data) return false; + barcode[0] = '\0'; + return extract_from_pages(mfu_data, barcode); +} diff --git a/nfc/tagtinker_nfc.h b/nfc/tagtinker_nfc.h new file mode 100644 index 0000000..51ae045 --- /dev/null +++ b/nfc/tagtinker_nfc.h @@ -0,0 +1,14 @@ +/* + * TagTinker — ESL NFC tag decoder + * + * Decodes NDEF URI from ESL Mifare Ultralight tags + * into the 17-character barcode format used by TagTinker. + */ + +#pragma once + +#include +#include +#include + +bool tagtinker_nfc_decode_barcode(const MfUltralightData* mfu_data, char barcode[18]); diff --git a/scenes/tagtinker_scene.c b/scenes/tagtinker_scene.c index 5c86df3..a13bdf1 100644 --- a/scenes/tagtinker_scene.c +++ b/scenes/tagtinker_scene.c @@ -21,6 +21,7 @@ void(*const tagtinker_scene_on_enter_handlers[])(void*) = { tagtinker_scene_transmit_on_enter, tagtinker_scene_about_on_enter, tagtinker_scene_text_box_on_enter, + tagtinker_scene_nfc_scan_on_enter, }; bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = { @@ -40,6 +41,7 @@ bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = { tagtinker_scene_transmit_on_event, tagtinker_scene_about_on_event, tagtinker_scene_text_box_on_event, + tagtinker_scene_nfc_scan_on_event, }; void(*const tagtinker_scene_on_exit_handlers[])(void*) = { @@ -59,6 +61,7 @@ void(*const tagtinker_scene_on_exit_handlers[])(void*) = { tagtinker_scene_transmit_on_exit, tagtinker_scene_about_on_exit, tagtinker_scene_text_box_on_exit, + tagtinker_scene_nfc_scan_on_exit, }; const SceneManagerHandlers tagtinker_scene_handlers = { diff --git a/scenes/tagtinker_scene.h b/scenes/tagtinker_scene.h index 5e038f9..aa24341 100644 --- a/scenes/tagtinker_scene.h +++ b/scenes/tagtinker_scene.h @@ -23,6 +23,7 @@ typedef enum { TagTinkerSceneTransmit, TagTinkerSceneAbout, TagTinkerSceneTextBox, + TagTinkerSceneNfcScan, TagTinkerSceneCount, } TagTinkerScene; @@ -89,3 +90,7 @@ void tagtinker_scene_about_on_exit(void* ctx); void tagtinker_scene_text_box_on_enter(void* ctx); bool tagtinker_scene_text_box_on_event(void* ctx, SceneManagerEvent event); void tagtinker_scene_text_box_on_exit(void* ctx); + +void tagtinker_scene_nfc_scan_on_enter(void* ctx); +bool tagtinker_scene_nfc_scan_on_event(void* ctx, SceneManagerEvent event); +void tagtinker_scene_nfc_scan_on_exit(void* ctx); diff --git a/scenes/tagtinker_scene_about.c b/scenes/tagtinker_scene_about.c index 440c0b6..df376c9 100644 --- a/scenes/tagtinker_scene_about.c +++ b/scenes/tagtinker_scene_about.c @@ -524,6 +524,7 @@ static void about_draw_cb(Canvas* canvas, void* _model) { canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, TAGTINKER_DISPLAY_NAME " v" TAGTINKER_VERSION); canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignTop, "Ported by I12BP8"); canvas_draw_str_aligned(canvas, 64, 34, AlignCenter, AlignTop, "Research by furrtek"); + canvas_draw_str_aligned(canvas, 64, 44, AlignCenter, AlignTop, "NFC by 7h30th3r0n3"); } } diff --git a/scenes/tagtinker_scene_nfc_scan.c b/scenes/tagtinker_scene_nfc_scan.c new file mode 100644 index 0000000..5e6ecb9 --- /dev/null +++ b/scenes/tagtinker_scene_nfc_scan.c @@ -0,0 +1,134 @@ +/* + * NFC Scan scene — scan an ESL NFC tag to fill barcode + */ + +#include "../tagtinker_app.h" + +enum { + NfcScanEventSuccess = 1, + NfcScanEventNotEsl = 2, +}; + +static int32_t nfc_scan_thread(void* ctx) { + TagTinkerApp* app = ctx; + + while(app->nfc_scanning) { + MfUltralightData* mfu_data = mf_ultralight_alloc(); + MfUltralightError err = + mf_ultralight_poller_sync_read_card(app->nfc, mfu_data, NULL); + + if(err == MfUltralightErrorNone) { + char barcode[18]; + bool decoded = tagtinker_nfc_decode_barcode(mfu_data, barcode); + mf_ultralight_free(mfu_data); + + if(!app->nfc_scanning) return 0; + + if(decoded) { + memcpy(app->barcode, barcode, TAGTINKER_BC_LEN); + app->barcode[TAGTINKER_BC_LEN] = '\0'; + view_dispatcher_send_custom_event( + app->view_dispatcher, NfcScanEventSuccess); + } else { + view_dispatcher_send_custom_event( + app->view_dispatcher, NfcScanEventNotEsl); + } + return 0; + } + + mf_ultralight_free(mfu_data); + + if(!app->nfc_scanning) break; + furi_delay_ms(100); + } + + return 0; +} + +void tagtinker_scene_nfc_scan_on_enter(void* ctx) { + TagTinkerApp* app = ctx; + + popup_reset(app->popup); + popup_set_header(app->popup, "Scan NFC Tag", 64, 10, AlignCenter, AlignTop); + popup_set_text( + app->popup, "Hold ESL tag\nto Flipper back", 64, 32, AlignCenter, AlignCenter); + + view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewPopup); + + notification_message(app->notifications, &sequence_blink_start_cyan); + + app->nfc = nfc_alloc(); + app->nfc_scanning = true; + + furi_thread_set_callback(app->nfc_thread, nfc_scan_thread); + furi_thread_set_context(app->nfc_thread, app); + furi_thread_start(app->nfc_thread); +} + +bool tagtinker_scene_nfc_scan_on_event(void* ctx, SceneManagerEvent event) { + TagTinkerApp* app = ctx; + + if(event.type != SceneManagerEventTypeCustom) return false; + + if(event.event == NfcScanEventSuccess) { + int8_t idx = tagtinker_ensure_target(app, app->barcode); + + if(idx < 0) { + popup_reset(app->popup); + popup_set_header( + app->popup, "Decode Error", 64, 20, AlignCenter, AlignCenter); + popup_set_text( + app->popup, "Tag read but\nbarcode invalid", 64, 36, AlignCenter, AlignCenter); + popup_set_timeout(app->popup, 2000); + popup_enable_timeout(app->popup); + popup_set_callback(app->popup, NULL); + view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewPopup); + return true; + } + + tagtinker_select_target(app, (uint8_t)idx); + + FURI_LOG_I( + TAGTINKER_TAG, + "NFC: %s -> PLID %02X%02X%02X%02X", + app->barcode, + app->plid[3], + app->plid[2], + app->plid[1], + app->plid[0]); + + notification_message(app->notifications, &sequence_success); + scene_manager_next_scene(app->scene_manager, TagTinkerSceneTargetActions); + return true; + } + + if(event.event == NfcScanEventNotEsl) { + popup_reset(app->popup); + popup_set_header( + app->popup, "Not an ESL tag", 64, 20, AlignCenter, AlignCenter); + popup_set_text( + app->popup, "Tag detected but\nno valid ESL data", 64, 36, AlignCenter, AlignCenter); + popup_set_timeout(app->popup, 2000); + popup_enable_timeout(app->popup); + popup_set_callback(app->popup, NULL); + view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewPopup); + return true; + } + + return false; +} + +void tagtinker_scene_nfc_scan_on_exit(void* ctx) { + TagTinkerApp* app = ctx; + + app->nfc_scanning = false; + + if(app->nfc) { + furi_thread_join(app->nfc_thread); + nfc_free(app->nfc); + app->nfc = NULL; + } + + notification_message(app->notifications, &sequence_blink_stop); + popup_reset(app->popup); +} diff --git a/scenes/tagtinker_scene_target_menu.c b/scenes/tagtinker_scene_target_menu.c index 017e108..d3df7e5 100644 --- a/scenes/tagtinker_scene_target_menu.c +++ b/scenes/tagtinker_scene_target_menu.c @@ -5,6 +5,7 @@ #include "../tagtinker_app.h" enum { + TargetMenuScanNfc = 99, TargetMenuAddNew = 100, }; @@ -20,7 +21,8 @@ void tagtinker_scene_target_menu_on_enter(void* ctx) { submenu_set_header(app->submenu, "Targeted Payloads"); /* Add new target */ - submenu_add_item(app->submenu, "+ New Target", TargetMenuAddNew, target_menu_cb, app); + submenu_add_item(app->submenu, "+ Scan NFC", TargetMenuScanNfc, target_menu_cb, app); + submenu_add_item(app->submenu, "+ Type Barcode", TargetMenuAddNew, target_menu_cb, app); /* List saved targets */ for(uint8_t i = 0; i < app->target_count; i++) { @@ -40,6 +42,11 @@ bool tagtinker_scene_target_menu_on_event(void* ctx, SceneManagerEvent event) { TagTinkerApp* app = ctx; if(event.type != SceneManagerEventTypeCustom) return false; + if(event.event == TargetMenuScanNfc) { + scene_manager_next_scene(app->scene_manager, TagTinkerSceneNfcScan); + return true; + } + if(event.event == TargetMenuAddNew) { /* Go to barcode input, then come back */ scene_manager_set_scene_state( diff --git a/tagtinker_app.c b/tagtinker_app.c index 05d8a37..ecd4a22 100644 --- a/tagtinker_app.c +++ b/tagtinker_app.c @@ -740,6 +740,14 @@ static void app_free(TagTinkerApp* app) { furi_thread_free(app->tx_thread); + /* NFC cleanup */ + if(app->nfc) { + app->nfc_scanning = false; + furi_thread_join(app->nfc_thread); + nfc_free(app->nfc); + } + furi_thread_free(app->nfc_thread); + furi_record_close(RECORD_GUI); furi_record_close(RECORD_NOTIFICATION); furi_record_close(RECORD_DIALOGS); @@ -883,6 +891,13 @@ static TagTinkerApp* app_alloc(void) { furi_thread_set_priority(app->tx_thread, FuriThreadPriorityHighest); furi_thread_set_context(app->tx_thread, app); + /* NFC scan thread */ + app->nfc_thread = furi_thread_alloc(); + furi_thread_set_name(app->nfc_thread, "TagTinkerNfc"); + furi_thread_set_stack_size(app->nfc_thread, 2048); + app->nfc = NULL; + app->nfc_scanning = false; + return app; } diff --git a/tagtinker_app.h b/tagtinker_app.h index aec30bb..42f2338 100644 --- a/tagtinker_app.h +++ b/tagtinker_app.h @@ -21,15 +21,20 @@ #include #include +#include +#include +#include + #include "views/numlock_input.h" #include "scenes/tagtinker_scene.h" #include "protocol/tagtinker_proto.h" #include "ir/tagtinker_ir.h" +#include "nfc/tagtinker_nfc.h" #define TAGTINKER_TAG "TagTinker" #define TAGTINKER_DISPLAY_NAME "TagTinker" -#define TAGTINKER_VERSION "2.0" +#define TAGTINKER_VERSION "2.1" #define TAGTINKER_BC_LEN 17 #define TAGTINKER_HEX_LEN 64 #define TAGTINKER_MAX_TARGETS 16 @@ -127,6 +132,11 @@ struct TagTinkerApp { bool tx_active; FuriThread* tx_thread; + /* NFC scan state */ + Nfc* nfc; + FuriThread* nfc_thread; + volatile bool nfc_scanning; + /* Broadcast settings */ uint8_t broadcast_type; uint8_t page;