diff --git a/.gitmodules b/.gitmodules index 1eefbed..c6eda15 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "plugin"] - path = plugin - url = https://gitlab.com/bettse/flipper-wiegand-plugin.git [submodule "lib/host_tests/vendor/munit"] path = lib/host_tests/vendor/munit url = https://github.com/nemequ/munit.git diff --git a/Makefile b/Makefile index 8de0b2a..83080b2 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,12 @@ asn1: build: ufbt +HOST_TEST_CFLAGS = -std=c11 -Wall -Wextra -Werror -DSEADER_HOST_TEST -Ilib/host_tests/vendor/munit -Ilib/host_tests -Ilib/asn1 -I. +ASN1_TEST_CFLAGS = -std=c11 -Wall -Wextra -Werror -Wno-error=unused-function -Wno-error=unused-parameter -DASN_DISABLE_PER_SUPPORT -DASN_DISABLE_OER_SUPPORT -DASN_DISABLE_XER_SUPPORT -DASN_DISABLE_RANDOM_FILL -Ilib/host_tests/vendor/munit -Ilib/host_tests -Ilib/asn1 -Ilib/asn1_skeletons -I. + test-host: mkdir -p build/host_tests - cc -std=c11 -Wall -Wextra -Werror -DSEADER_HOST_TEST -Ilib/host_tests/vendor/munit -Ilib/host_tests -I. \ + cc $(HOST_TEST_CFLAGS) \ lib/host_tests/vendor/munit/munit.c \ lib/host_tests/test_main.c \ lib/host_tests/test_lrc.c \ @@ -21,10 +24,13 @@ test-host: lib/host_tests/test_t1_protocol.c \ lib/host_tests/test_snmp.c \ lib/host_tests/test_uhf_status_label.c \ + lib/host_tests/test_credential_sio_label.c \ + lib/host_tests/test_runtime_policy.c \ lib/host_tests/t1_test_stubs.c \ lib/host_tests/bit_buffer_mock.c \ lrc.c \ ccid_logic.c \ + credential_sio_label.c \ t_1_logic.c \ t_1.c \ sam_key_label.c \ @@ -34,9 +40,51 @@ test-host: uhf_status_label.c \ uhf_tag_config_view.c \ uhf_snmp_probe.c \ + runtime_policy.c \ -o build/host_tests/seader_tests ./build/host_tests/seader_tests +test-asn1-integration: + mkdir -p build/host_tests + cc $(ASN1_TEST_CFLAGS) \ + lib/host_tests/vendor/munit/munit.c \ + lib/host_tests/test_card_details_main.c \ + lib/host_tests/test_card_details_builder.c \ + card_details_builder.c \ + lib/asn1/CardDetails.c \ + lib/asn1/Protocol.c \ + lib/asn1/FrameProtocol.c \ + lib/asn1/RunTimerValue.c \ + lib/asn1_skeletons/OCTET_STRING.c \ + lib/asn1_skeletons/BOOLEAN.c \ + lib/asn1_skeletons/NativeInteger.c \ + lib/asn1_skeletons/NativeEnumerated.c \ + lib/asn1_skeletons/INTEGER.c \ + lib/asn1_skeletons/OPEN_TYPE.c \ + lib/asn1_skeletons/constr_CHOICE.c \ + lib/asn1_skeletons/constr_SEQUENCE.c \ + lib/asn1_skeletons/constr_TYPE.c \ + lib/asn1_skeletons/asn_application.c \ + lib/asn1_skeletons/asn_codecs_prim.c \ + lib/asn1_skeletons/ber_tlv_tag.c \ + lib/asn1_skeletons/ber_tlv_length.c \ + lib/asn1_skeletons/ber_decoder.c \ + lib/asn1_skeletons/der_encoder.c \ + lib/asn1_skeletons/constraints.c \ + lib/asn1_skeletons/asn_internal.c \ + -o build/host_tests/seader_card_details_tests + ./build/host_tests/seader_card_details_tests + +test-runtime-integration: + mkdir -p build/host_tests + cc $(HOST_TEST_CFLAGS) \ + lib/host_tests/vendor/munit/munit.c \ + lib/host_tests/test_runtime_integration_main.c \ + lib/host_tests/test_hf_release_sequence.c \ + hf_release_sequence.c \ + -o build/host_tests/seader_runtime_integration_tests + ./build/host_tests/seader_runtime_integration_tests + launch: ufbt launch diff --git a/apdu_runner.c b/apdu_runner.c index 5652e1e..ae9c40a 100644 --- a/apdu_runner.c +++ b/apdu_runner.c @@ -1,4 +1,9 @@ #include "apdu_runner.h" +#include "seader_i.h" + +#include +#include +#include #define TAG "APDU_Runner" @@ -6,7 +11,14 @@ #define SEADER_APDU_MAX_LEN 732 void seader_apdu_runner_cleanup(Seader* seader, SeaderWorkerEvent event) { + furi_check(seader); + SeaderWorker* seader_worker = seader->worker; + if(!seader_worker) { + apdu_log_free(seader->apdu_log); + seader->apdu_log = NULL; + return; + } seader_worker_change_state(seader_worker, SeaderWorkerStateReady); apdu_log_free(seader->apdu_log); seader->apdu_log = NULL; @@ -16,7 +28,10 @@ void seader_apdu_runner_cleanup(Seader* seader, SeaderWorkerEvent event) { } bool seader_apdu_runner_send_next_line(Seader* seader) { + furi_check(seader); SeaderWorker* seader_worker = seader->worker; + furi_check(seader_worker); + furi_check(seader_worker->uart); SeaderUartBridge* seader_uart = seader_worker->uart; SeaderAPDURunnerContext* apdu_runner_ctx = &(seader->apdu_runner_ctx); @@ -86,6 +101,9 @@ void seader_apdu_runner_init(Seader* seader) { } bool seader_apdu_runner_response(Seader* seader, uint8_t* r_apdu, size_t r_len) { + furi_check(seader); + furi_check(seader->worker); + furi_check(seader->worker->uart); SeaderUartBridge* seader_uart = seader->worker->uart; SeaderAPDURunnerContext* apdu_runner_ctx = &(seader->apdu_runner_ctx); uint8_t GET_RESPONSE[] = {0x00, 0xc0, 0x00, 0x00, 0xff}; diff --git a/apdu_runner.h b/apdu_runner.h index ce5cbf2..4da0486 100644 --- a/apdu_runner.h +++ b/apdu_runner.h @@ -2,7 +2,10 @@ #pragma once #include -#include "seader_i.h" +#include +#include + +#include "seader.h" #define SEADER_APDU_RUNNER_FILE_NAME APP_DATA_PATH("script.apdu") diff --git a/application.fam b/application.fam index 444f63b..12e27b3 100644 --- a/application.fam +++ b/application.fam @@ -20,7 +20,8 @@ App( sources=[ "*.c", "aeabi_uldivmod.sx", - "!plugin/*.c", + "!hf_interface_fal/*.c", + "!wiegand_interface_fal/*.c", ], fap_icon="icons/logo.png", fap_category="NFC", @@ -60,6 +61,15 @@ App( apptype=FlipperAppType.PLUGIN, entry_point="plugin_wiegand_ep", requires=["seader"], - sources=["plugin/wiegand.c"], + sources=["wiegand_interface_fal/wiegand.c"], + fal_embedded=True, +) + +App( + appid="plugin_hf", + apptype=FlipperAppType.PLUGIN, + entry_point="plugin_hf_ep", + requires=["seader"], + sources=["hf_interface_fal/hf.c"], fal_embedded=True, ) diff --git a/card_details_builder.c b/card_details_builder.c new file mode 100644 index 0000000..4e28d39 --- /dev/null +++ b/card_details_builder.c @@ -0,0 +1,82 @@ +#include "card_details_builder.h" + +#include + +#include + +/* Build the ASN.1-owned CardDetails payload used for cardDetected. Optional members + must be heap/ASN.1-owned because the caller always releases the structure through + ASN_STRUCT_FREE_CONTENTS_ONLY(). */ +bool seader_card_details_build( + CardDetails_t* card_details, + uint8_t sak, + const uint8_t* uid, + uint8_t uid_len, + const uint8_t* ats, + uint8_t ats_len) { + if(!card_details || !uid || uid_len == 0U) { + return false; + } + + memset(card_details, 0, sizeof(*card_details)); + + if(OCTET_STRING_fromBuf(&card_details->csn, (const char*)uid, uid_len) != 0) { + return false; + } + + uint8_t protocol_bytes[] = {0x00, 0x00}; + if(ats != NULL) { + /* ISO14443-4A cards report ATS and SAK to the SAM. */ + protocol_bytes[1] = FrameProtocol_nfc; + if(OCTET_STRING_fromBuf( + &card_details->protocol, (const char*)protocol_bytes, sizeof(protocol_bytes)) != + 0) { + seader_card_details_reset(card_details); + return false; + } + card_details->sak = calloc(1, sizeof(*card_details->sak)); + card_details->atsOrAtqbOrAtr = calloc(1, sizeof(*card_details->atsOrAtqbOrAtr)); + if(!card_details->sak || !card_details->atsOrAtqbOrAtr || + OCTET_STRING_fromBuf(card_details->sak, (const char*)&sak, 1) != 0 || + OCTET_STRING_fromBuf(card_details->atsOrAtqbOrAtr, (const char*)ats, ats_len) != 0) { + seader_card_details_reset(card_details); + return false; + } + } else if(uid_len == 8U) { + /* Picopass does not provide ATS/SAK in this path; uid_len==8 is the existing discriminator. */ + protocol_bytes[1] = FrameProtocol_iclass; + if(OCTET_STRING_fromBuf( + &card_details->protocol, (const char*)protocol_bytes, sizeof(protocol_bytes)) != + 0) { + seader_card_details_reset(card_details); + return false; + } + } else { + /* MIFARE Classic still identifies as NFC here, but carries a one-byte SAK. */ + protocol_bytes[1] = FrameProtocol_nfc; + if(OCTET_STRING_fromBuf( + &card_details->protocol, (const char*)protocol_bytes, sizeof(protocol_bytes)) != + 0) { + seader_card_details_reset(card_details); + return false; + } + card_details->sak = calloc(1, sizeof(*card_details->sak)); + if(!card_details->sak || + OCTET_STRING_fromBuf(card_details->sak, (const char*)&sak, 1) != 0) { + seader_card_details_reset(card_details); + return false; + } + } + + return true; +} + +/* Release a builder result through the same ASN.1 ownership boundary used by production code. */ +void seader_card_details_reset(CardDetails_t* card_details) { + if(!card_details) { + return; + } + + ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_CardDetails, card_details); + memset(card_details, 0, sizeof(*card_details)); +} diff --git a/card_details_builder.h b/card_details_builder.h new file mode 100644 index 0000000..17e329e --- /dev/null +++ b/card_details_builder.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +#ifndef ASN_EMIT_DEBUG +#define ASN_EMIT_DEBUG 0 +#endif + +#include + +bool seader_card_details_build( + CardDetails_t* card_details, + uint8_t sak, + const uint8_t* uid, + uint8_t uid_len, + const uint8_t* ats, + uint8_t ats_len); + +void seader_card_details_reset(CardDetails_t* card_details); diff --git a/ccid.c b/ccid.c index ade07a9..aef2a92 100644 --- a/ccid.c +++ b/ccid.c @@ -30,6 +30,13 @@ static uint8_t seader_ccid_next_sequence(SeaderUartBridge* seader_uart, uint8_t return seader_ccid_sequence_advance(&slot_state->sequence); } +static SeaderUartBridge* seader_ccid_active_uart(Seader* seader) { + furi_check(seader); + furi_check(seader->worker); + furi_check(seader->worker->uart); + return seader->worker->uart; +} + void seader_ccid_IccPowerOn(SeaderUartBridge* seader_uart, uint8_t slot) { SeaderCcidSlotState* slot_state = seader_ccid_slot_state(seader_uart, slot); if(slot_state->powered) { @@ -92,8 +99,7 @@ void seader_ccid_GetSlotStatus(SeaderUartBridge* seader_uart, uint8_t slot) { } void seader_ccid_SetParameters(Seader* seader, uint8_t slot) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_ccid_active_uart(seader); FURI_LOG_D(TAG, "seader_ccid_SetParameters(%d)", slot); uint8_t payloadLen = 0; @@ -227,8 +233,8 @@ void seader_ccid_XfrBlockToSlot( } size_t seader_ccid_process(Seader* seader, uint8_t* cmd, size_t cmd_len) { + SeaderUartBridge* seader_uart = seader_ccid_active_uart(seader); SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; CCID_Message message; message.consumed = 0; SeaderCcidState* ccid_state = seader_ccid_state(seader_uart); diff --git a/credential_sio_label.c b/credential_sio_label.c new file mode 100644 index 0000000..a84daf9 --- /dev/null +++ b/credential_sio_label.c @@ -0,0 +1,36 @@ +#include "credential_sio_label.h" + +#include + +bool seader_sio_label_format( + bool has_sio, + bool is_picopass_sio_context, + uint8_t sio_start_block, + char* out, + size_t out_size) { + if(out && out_size > 0U) { + out[0] = '\0'; + } + + if(!out || out_size == 0U || !has_sio) { + return false; + } + + if(!is_picopass_sio_context) { + snprintf(out, out_size, "+SIO"); + return true; + } + + /* Picopass/iClass-only SIO labeling. DESFire/other media do not use block-derived SR/SE labels. */ + switch(sio_start_block) { + case 6: + snprintf(out, out_size, "+SIO(SE)"); + return true; + case 10: + snprintf(out, out_size, "+SIO(SR)"); + return true; + default: + snprintf(out, out_size, "+SIO(?)"); + return true; + } +} diff --git a/credential_sio_label.h b/credential_sio_label.h new file mode 100644 index 0000000..3015c3c --- /dev/null +++ b/credential_sio_label.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include + +#include "seader_credential_type.h" + +bool seader_sio_label_format( + bool has_sio, + bool is_picopass_sio_context, + uint8_t sio_start_block, + char* out, + size_t out_size); diff --git a/hf_interface_fal/HF_README.md b/hf_interface_fal/HF_README.md new file mode 100644 index 0000000..52c917a --- /dev/null +++ b/hf_interface_fal/HF_README.md @@ -0,0 +1,9 @@ +# Seader embedded HF plugin sources + +This directory is part of the main Seader repository. + +It contains the embedded HF `.fal` plugin sources used by Seader: +- `hf.c` +- `hf_interface.h` + +The HF plugin source path in `application.fam` must point at `hf_interface_fal/hf.c`. diff --git a/hf_interface_fal/hf.c b/hf_interface_fal/hf.c new file mode 100644 index 0000000..7d37260 --- /dev/null +++ b/hf_interface_fal/hf.c @@ -0,0 +1,830 @@ +#include "hf_interface.h" + +#include "../protocol/picopass_poller.h" +#include "../protocol/rfal_picopass.h" + +#include +#include +#include +#include +#include +#include +#include + +#define TAG "PluginHF" +#ifdef HF_HARDEN_DIAG +#define HF_DIAG_D(...) FURI_LOG_D(TAG, __VA_ARGS__) +#define HF_DIAG_I(...) FURI_LOG_I(TAG, __VA_ARGS__) +#else +#define HF_DIAG_D(...) \ + do { \ + } while(0) +#define HF_DIAG_I(...) \ + do { \ + } while(0) +#endif + +#define HF_PLUGIN_POLLER_MAX_FWT (200000U) +#define HF_PLUGIN_POLLER_MAX_BUFFER_SIZE (258U) + +// ATS bit definitions +#define ISO14443_4A_ATS_T0_TA1 (1U << 4) +#define ISO14443_4A_ATS_T0_TB1 (1U << 5) +#define ISO14443_4A_ATS_T0_TC1 (1U << 6) + +typedef struct { + const PluginHfHostApi* api; + void* host_ctx; + Nfc* nfc; + NfcDevice* nfc_device; + NfcPoller* poller; + Iso14443_4aPoller* iso14443_4a_poller; + MfClassicPoller* mfc_poller; + SeaderCredentialType active_type; +} PluginHfContext; + +static const uint8_t plugin_hf_update_block2[] = {RFAL_PICOPASS_CMD_UPDATE, 0x02}; +static const uint8_t plugin_hf_select_seos_app[] = + {0x00, 0xa4, 0x04, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00}; +static const uint8_t plugin_hf_select_desfire_app_no_le[] = + {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x00}; +static const uint8_t plugin_hf_file_not_found[] = {0x6a, 0x82}; + +static bool plugin_hf_validate_host_api(const PluginHfHostApi* api) { + if(!api) { + FURI_LOG_E(TAG, "Missing HF host API"); + return false; + } + +#define HF_REQUIRE_API(field) \ + do { \ + if(!(api->field)) { \ + FURI_LOG_E(TAG, "Missing host API: " #field); \ + return false; \ + } \ + } while(false) + + HF_REQUIRE_API(notify_card_detected); + HF_REQUIRE_API(notify_worker_exit); + HF_REQUIRE_API(sam_can_accept_card); + HF_REQUIRE_API(send_card_detected); + HF_REQUIRE_API(send_nfc_rx); + HF_REQUIRE_API(run_conversation); + HF_REQUIRE_API(set_stage); + HF_REQUIRE_API(get_stage); + HF_REQUIRE_API(set_credential_type); + HF_REQUIRE_API(get_credential_type); + HF_REQUIRE_API(get_desfire_ev2); + HF_REQUIRE_API(set_desfire_ev2); + HF_REQUIRE_API(append_picopass_sio); + HF_REQUIRE_API(set_14a_sio); + HF_REQUIRE_API(get_nfc); + HF_REQUIRE_API(get_nfc_device); + HF_REQUIRE_API(picopass_detect); + HF_REQUIRE_API(picopass_start); + HF_REQUIRE_API(picopass_stop); + HF_REQUIRE_API(picopass_get_csn); + HF_REQUIRE_API(picopass_transmit); + +#undef HF_REQUIRE_API + return true; +} + +static PluginHfContext* plugin_hf_require_ctx(void* plugin_ctx) { + PluginHfContext* ctx = plugin_ctx; + furi_check(ctx); + furi_check(ctx->api); + furi_check(ctx->host_ctx); + furi_check(ctx->nfc); + furi_check(ctx->nfc_device); + return ctx; +} + +static void plugin_hf_cleanup_pollers(PluginHfContext* ctx) { + ctx = plugin_hf_require_ctx(ctx); + if(ctx->poller) { + nfc_poller_stop(ctx->poller); + nfc_poller_free(ctx->poller); + ctx->poller = NULL; + } + ctx->iso14443_4a_poller = NULL; + ctx->mfc_poller = NULL; + if(ctx->api->picopass_stop) { + ctx->api->picopass_stop(ctx->host_ctx); + } +} + +static void plugin_hf_set_read_error(PluginHfContext* ctx, const char* text) { + ctx = plugin_hf_require_ctx(ctx); + if(ctx->api->set_read_error) { + ctx->api->set_read_error(ctx->host_ctx, text); + } +} + +static void plugin_hf_add_detected_type( + SeaderCredentialType* detected_types, + size_t* detected_type_count, + size_t detected_capacity, + SeaderCredentialType type) { + for(size_t i = 0; i < *detected_type_count; i++) { + if(detected_types[i] == type) { + return; + } + } + + if(*detected_type_count < detected_capacity) { + detected_types[*detected_type_count] = type; + (*detected_type_count)++; + } +} + +static PicopassError plugin_hf_fake_epurse_update(BitBuffer* tx_buffer, BitBuffer* rx_buffer) { + const uint8_t* buffer = bit_buffer_get_data(tx_buffer); + uint8_t fake_response[8]; + memset(fake_response, 0, sizeof(fake_response)); + memcpy(fake_response + 0, buffer + 6, 4); + memcpy(fake_response + 4, buffer + 2, 4); + + bit_buffer_append_bytes(rx_buffer, fake_response, sizeof(fake_response)); + iso13239_crc_append(Iso13239CrcTypePicopass, rx_buffer); + + return PicopassErrorNone; +} + +static void + plugin_hf_capture_sio(PluginHfContext* ctx, BitBuffer* tx_buffer, BitBuffer* rx_buffer) { + ctx = plugin_hf_require_ctx(ctx); + furi_check(tx_buffer); + furi_check(rx_buffer); + const uint8_t* buffer = bit_buffer_get_data(tx_buffer); + size_t len = bit_buffer_get_size_bytes(tx_buffer); + const uint8_t* rx_buffer_data = bit_buffer_get_data(rx_buffer); + if(!buffer || !rx_buffer_data || len == 0U) return; + + if(ctx->api->get_credential_type(ctx->host_ctx) == SeaderCredentialTypePicopass) { + if(buffer[0] == RFAL_PICOPASS_CMD_READ4) { + uint8_t block_num = buffer[1]; + ctx->api->append_picopass_sio( + ctx->host_ctx, block_num, rx_buffer_data, PICOPASS_BLOCK_LEN * 4); + } + } else if(ctx->api->get_credential_type(ctx->host_ctx) == SeaderCredentialType14A) { + uint8_t desfire_read[] = {0x90, 0xbd, 0x00, 0x00, 0x07, 0x0f, 0x00, 0x00, 0x00}; + if(len == 13 && memcmp(buffer, desfire_read, sizeof(desfire_read)) == 0 && + rx_buffer_data[0] == 0x30) { + size_t sio_len = bit_buffer_get_size_bytes(rx_buffer) - 2; + ctx->api->set_14a_sio(ctx->host_ctx, rx_buffer_data, sio_len); + } + } +} + +static void plugin_hf_iso15693_transmit(PluginHfContext* ctx, uint8_t* buffer, size_t len) { + ctx = plugin_hf_require_ctx(ctx); + if(!buffer || len == 0U) { + FURI_LOG_W(TAG, "Skip picopass transmit invalid input"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return; + } + BitBuffer* tx_buffer = bit_buffer_alloc(len); + BitBuffer* rx_buffer = bit_buffer_alloc(HF_PLUGIN_POLLER_MAX_BUFFER_SIZE); + uint8_t rx_data[HF_PLUGIN_POLLER_MAX_BUFFER_SIZE]; + size_t rx_len = 0U; + if(!tx_buffer || !rx_buffer) { + FURI_LOG_E(TAG, "Failed to allocate picopass buffers"); + if(tx_buffer) bit_buffer_free(tx_buffer); + if(rx_buffer) bit_buffer_free(rx_buffer); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return; + } + + do { + bit_buffer_append_bytes(tx_buffer, buffer, len); + + if(memcmp(buffer, plugin_hf_update_block2, sizeof(plugin_hf_update_block2)) == 0) { + if(plugin_hf_fake_epurse_update(tx_buffer, rx_buffer) != PicopassErrorNone) { + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + break; + } + } else { + if(!ctx->api->picopass_transmit || !ctx->api->picopass_transmit( + ctx->host_ctx, + buffer, + len, + rx_data, + sizeof(rx_data), + &rx_len, + HF_PLUGIN_POLLER_MAX_FWT)) { + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + break; + } + bit_buffer_append_bytes(rx_buffer, rx_data, rx_len); + } + + plugin_hf_capture_sio(ctx, tx_buffer, rx_buffer); + ctx->api->send_nfc_rx( + ctx->host_ctx, + (uint8_t*)bit_buffer_get_data(rx_buffer), + bit_buffer_get_size_bytes(rx_buffer)); + } while(false); + + bit_buffer_free(tx_buffer); + bit_buffer_free(rx_buffer); +} + +static void plugin_hf_iso14443a_transmit( + PluginHfContext* ctx, + uint8_t* buffer, + size_t len, + uint16_t timeout, + uint8_t format[3]) { + UNUSED(timeout); + UNUSED(format); + + ctx = plugin_hf_require_ctx(ctx); + if(!buffer || len == 0U || !ctx->iso14443_4a_poller) { + FURI_LOG_W(TAG, "Skip 14A transmit invalid state"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return; + } + + BitBuffer* tx_buffer = bit_buffer_alloc(len + 1); + BitBuffer* rx_buffer = bit_buffer_alloc(HF_PLUGIN_POLLER_MAX_BUFFER_SIZE); + if(!tx_buffer || !rx_buffer) { + FURI_LOG_E(TAG, "Failed to allocate 14A buffers"); + if(tx_buffer) bit_buffer_free(tx_buffer); + if(rx_buffer) bit_buffer_free(rx_buffer); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return; + } + + do { + bit_buffer_append_bytes(tx_buffer, buffer, len); + + if(ctx->api->get_desfire_ev2(ctx->host_ctx) && + sizeof(plugin_hf_select_desfire_app_no_le) == len && + memcmp(buffer, plugin_hf_select_desfire_app_no_le, len) == 0) { + bit_buffer_append_byte(tx_buffer, 0x00); + } + + Iso14443_4aError error = + iso14443_4a_poller_send_block(ctx->iso14443_4a_poller, tx_buffer, rx_buffer); + if(error != Iso14443_4aErrorNone) { + FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + break; + } + + if(sizeof(plugin_hf_select_seos_app) == len && + memcmp(buffer, plugin_hf_select_seos_app, len) == 0 && + bit_buffer_get_size_bytes(rx_buffer) == 38) { + const uint8_t ev2_select_reply_prefix[] = {0x6F, 0x22, 0x85, 0x20}; + const uint8_t* rapdu = bit_buffer_get_data(rx_buffer); + if(memcmp(ev2_select_reply_prefix, rapdu, sizeof(ev2_select_reply_prefix)) == 0) { + ctx->api->set_desfire_ev2(ctx->host_ctx, true); + bit_buffer_reset(rx_buffer); + bit_buffer_append_bytes( + rx_buffer, plugin_hf_file_not_found, sizeof(plugin_hf_file_not_found)); + } + } + + plugin_hf_capture_sio(ctx, tx_buffer, rx_buffer); + ctx->api->send_nfc_rx( + ctx->host_ctx, + (uint8_t*)bit_buffer_get_data(rx_buffer), + bit_buffer_get_size_bytes(rx_buffer)); + } while(false); + + bit_buffer_free(tx_buffer); + bit_buffer_free(rx_buffer); +} + +static void plugin_hf_mfc_transmit( + PluginHfContext* ctx, + uint8_t* buffer, + size_t len, + uint16_t timeout, + uint8_t format[3]) { + UNUSED(timeout); + + ctx = plugin_hf_require_ctx(ctx); + if(!buffer || len == 0U || !ctx->mfc_poller) { + FURI_LOG_W(TAG, "Skip MFC transmit invalid state"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return; + } + + BitBuffer* tx_buffer = bit_buffer_alloc(len); + BitBuffer* rx_buffer = bit_buffer_alloc(HF_PLUGIN_POLLER_MAX_BUFFER_SIZE); + if(!tx_buffer || !rx_buffer) { + FURI_LOG_E(TAG, "Failed to allocate MFC buffers"); + if(tx_buffer) bit_buffer_free(tx_buffer); + if(rx_buffer) bit_buffer_free(rx_buffer); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return; + } + + do { + if(format[0] == 0x00 && format[1] == 0xC0 && format[2] == 0x00) { + bit_buffer_append_bytes(tx_buffer, buffer, len); + MfClassicError error = + mf_classic_poller_send_frame(ctx->mfc_poller, tx_buffer, rx_buffer, 60000); + if(error != MfClassicErrorNone) { + FURI_LOG_W(TAG, "mf_classic_poller_send_frame error %d", error); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + break; + } + } else if( + (format[0] == 0x00 && format[1] == 0x00 && format[2] == 0x40) || + (format[0] == 0x00 && format[1] == 0x00 && format[2] == 0x24) || + (format[0] == 0x00 && format[1] == 0x00 && format[2] == 0x44)) { + uint8_t tx_parity = 0; + uint8_t len_without_parity = len - 1; + + for(size_t i = 0; i < len; i++) { + bit_lib_reverse_bits(buffer + i, 0, 8); + } + + for(size_t i = 0; i < len_without_parity; i++) { + bool val = bit_lib_get_bit(buffer + i + 1, i); + bit_lib_set_bit(&tx_parity, i, val); + } + + for(size_t i = 0; i < len_without_parity; i++) { + buffer[i] = (buffer[i] << i) | (buffer[i + 1] >> (8 - i)); + } + bit_buffer_append_bytes(tx_buffer, buffer, len_without_parity); + + for(size_t i = 0; i < len_without_parity; i++) { + bit_lib_reverse_bits(buffer + i, 0, 8); + bit_buffer_set_byte_with_parity( + tx_buffer, i, buffer[i], bit_lib_get_bit(&tx_parity, i)); + } + + MfClassicError error = mf_classic_poller_send_custom_parity_frame( + ctx->mfc_poller, tx_buffer, rx_buffer, 60000); + if(error != MfClassicErrorNone) { + if(error == MfClassicErrorTimeout && + ctx->api->get_credential_type(ctx->host_ctx) == + SeaderCredentialTypeMifareClassic) { + plugin_hf_set_read_error( + ctx, "Protected read timed out.\nNo supported data\nor wrong key."); + } + FURI_LOG_W(TAG, "mf_classic_poller_send_custom_parity_frame error %d", error); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + break; + } + + size_t length = bit_buffer_get_size_bytes(rx_buffer); + const uint8_t* rx_parity = bit_buffer_get_parity(rx_buffer); + uint8_t with_parity[HF_PLUGIN_POLLER_MAX_BUFFER_SIZE]; + memset(with_parity, 0, sizeof(with_parity)); + + for(size_t i = 0; i < length; i++) { + uint8_t b = bit_buffer_get_byte(rx_buffer, i); + bit_lib_reverse_bits(&b, 0, 8); + bit_buffer_set_byte(rx_buffer, i, b); + } + + length = length + (length / 8) + 1; + uint8_t parts = 1 + length / 9; + for(size_t p = 0; p < parts; p++) { + uint8_t doffset = p * 9; + uint8_t soffset = p * 8; + + for(size_t i = 0; i < 9; i++) { + with_parity[i + doffset] = bit_buffer_get_byte(rx_buffer, i + soffset) >> i; + if(i > 0) { + with_parity[i + doffset] |= bit_buffer_get_byte(rx_buffer, i + soffset - 1) + << (9 - i); + } + if(i > 0) { + bool val = bit_lib_get_bit(rx_parity, i - 1); + bit_lib_set_bit(with_parity + i, i - 1, val); + } + } + } + + for(size_t i = 0; i < length; i++) { + bit_lib_reverse_bits(with_parity + i, 0, 8); + } + + bit_buffer_copy_bytes(rx_buffer, with_parity, length); + } else { + FURI_LOG_W(TAG, "Unhandled MFC format"); + } + + ctx->api->send_nfc_rx( + ctx->host_ctx, + (uint8_t*)bit_buffer_get_data(rx_buffer), + bit_buffer_get_size_bytes(rx_buffer)); + } while(false); + + bit_buffer_free(tx_buffer); + bit_buffer_free(rx_buffer); +} + +static NfcCommand plugin_hf_poller_callback_iso14443_4a(NfcGenericEvent event, void* context) { + PluginHfContext* ctx = plugin_hf_require_ctx(context); + NfcCommand ret = NfcCommandContinue; + const Iso14443_4aPollerEvent* iso_event = event.event_data; + if(event.protocol != NfcProtocolIso14443_4a || !iso_event) { + FURI_LOG_W(TAG, "14A callback invalid event"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return NfcCommandStop; + } + PluginHfStage stage = ctx->api->get_stage(ctx->host_ctx); + ctx->iso14443_4a_poller = event.instance; + + if(iso_event->type == Iso14443_4aPollerEventTypeReady) { + HF_DIAG_D("14A ready stage=%d", stage); + if(stage == PluginHfStageCardDetect) { + ctx->api->notify_card_detected(ctx->host_ctx); + if(!ctx->api->sam_can_accept_card(ctx->host_ctx)) { + return NfcCommandContinue; + } + + furi_check(ctx->poller); + furi_check(ctx->nfc_device); + const void* poller_data = nfc_poller_get_data(ctx->poller); + if(!poller_data) { + FURI_LOG_E(TAG, "14A ready without poller data"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return NfcCommandStop; + } + nfc_device_set_data(ctx->nfc_device, NfcProtocolIso14443_4a, poller_data); + + size_t uid_len = 0; + const uint8_t* uid = nfc_device_get_uid(ctx->nfc_device, &uid_len); + const Iso14443_4aData* iso_data = + nfc_device_get_data(ctx->nfc_device, NfcProtocolIso14443_4a); + if(!uid || !iso_data) { + FURI_LOG_E(TAG, "14A data unavailable uid=%p iso=%p", (void*)uid, (void*)iso_data); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return NfcCommandStop; + } + const Iso14443_3aData* iso3a = iso14443_4a_get_base_data(iso_data); + if(!iso3a) { + FURI_LOG_E(TAG, "14A base data unavailable"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return NfcCommandStop; + } + + uint32_t t1_tk_size = 0; + if(iso_data->ats_data.t1_tk != NULL) { + t1_tk_size = simple_array_get_count(iso_data->ats_data.t1_tk); + if(t1_tk_size > 0xFF) { + t1_tk_size = 0; + } + } + + uint8_t ats_len = 0; + uint8_t* ats = malloc(4 + t1_tk_size); + if(!ats) { + FURI_LOG_E(TAG, "Failed to allocate ATS buffer"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return NfcCommandStop; + } + if(iso_data->ats_data.tl > 1) { + ats[ats_len++] = iso_data->ats_data.t0; + if(iso_data->ats_data.t0 & ISO14443_4A_ATS_T0_TA1) + ats[ats_len++] = iso_data->ats_data.ta_1; + if(iso_data->ats_data.t0 & ISO14443_4A_ATS_T0_TB1) + ats[ats_len++] = iso_data->ats_data.tb_1; + if(iso_data->ats_data.t0 & ISO14443_4A_ATS_T0_TC1) + ats[ats_len++] = iso_data->ats_data.tc_1; + if(t1_tk_size != 0) { + memcpy( + ats + ats_len, + simple_array_cget_data(iso_data->ats_data.t1_tk), + t1_tk_size); + ats_len += t1_tk_size; + } + } + + ctx->api->send_card_detected( + ctx->host_ctx, iso14443_3a_get_sak(iso3a), uid, uid_len, ats, ats_len); + FURI_LOG_D(TAG, "14A cardDetected delivered uid_len=%u ats_len=%u", uid_len, ats_len); + free(ats); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageConversation); + } else if(stage == PluginHfStageConversation) { + FURI_LOG_D(TAG, "14A enter conversation"); + ctx->api->run_conversation(ctx->host_ctx); + stage = ctx->api->get_stage(ctx->host_ctx); + if(stage == PluginHfStageComplete) { + ret = NfcCommandStop; + } else if(stage == PluginHfStageFail) { + ctx->api->notify_worker_exit(ctx->host_ctx); + ret = NfcCommandStop; + } + } else if(stage == PluginHfStageComplete) { + ret = NfcCommandStop; + } else if(stage == PluginHfStageFail) { + ctx->api->notify_worker_exit(ctx->host_ctx); + ret = NfcCommandStop; + } + } else if(iso_event->type == Iso14443_4aPollerEventTypeError) { + Iso14443_4aPollerEventData* data = iso_event->data; + if(data->error == Iso14443_4aErrorProtocol) { + ret = NfcCommandStop; + } + } + + return ret; +} + +static NfcCommand plugin_hf_poller_callback_mfc(NfcGenericEvent event, void* context) { + PluginHfContext* ctx = plugin_hf_require_ctx(context); + NfcCommand ret = NfcCommandContinue; + MfClassicPollerEvent* mfc_event = event.event_data; + if(event.protocol != NfcProtocolMfClassic || !mfc_event) { + FURI_LOG_W(TAG, "MFC callback invalid event"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return NfcCommandStop; + } + PluginHfStage stage = ctx->api->get_stage(ctx->host_ctx); + ctx->mfc_poller = event.instance; + + if(mfc_event->type == MfClassicPollerEventTypeSuccess) { + HF_DIAG_D("MFC success stage=%d", stage); + if(stage == PluginHfStageCardDetect) { + ctx->api->notify_card_detected(ctx->host_ctx); + if(!ctx->api->sam_can_accept_card(ctx->host_ctx)) { + return NfcCommandContinue; + } + + furi_check(ctx->poller); + const MfClassicData* mfc_data = nfc_poller_get_data(ctx->poller); + if(!mfc_data || !mfc_data->iso14443_3a_data) { + FURI_LOG_E(TAG, "MFC data unavailable"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return NfcCommandStop; + } + size_t uid_len = 0; + const uint8_t* uid = mf_classic_get_uid(mfc_data, &uid_len); + if(!uid) { + FURI_LOG_E(TAG, "MFC uid unavailable"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageFail); + return NfcCommandStop; + } + ctx->api->send_card_detected( + ctx->host_ctx, + iso14443_3a_get_sak(mfc_data->iso14443_3a_data), + uid, + uid_len, + NULL, + 0); + FURI_LOG_D(TAG, "MFC cardDetected delivered uid_len=%u", uid_len); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageConversation); + } else if(stage == PluginHfStageConversation) { + FURI_LOG_D(TAG, "MFC enter conversation"); + ctx->api->run_conversation(ctx->host_ctx); + stage = ctx->api->get_stage(ctx->host_ctx); + if(stage == PluginHfStageComplete) { + ret = NfcCommandStop; + } else if(stage == PluginHfStageFail) { + ctx->api->notify_worker_exit(ctx->host_ctx); + ret = NfcCommandStop; + } + } else if(stage == PluginHfStageComplete) { + ret = NfcCommandStop; + } else if(stage == PluginHfStageFail) { + ctx->api->notify_worker_exit(ctx->host_ctx); + ret = NfcCommandStop; + } + } else if(mfc_event->type == MfClassicPollerEventTypeFail) { + ctx->api->notify_worker_exit(ctx->host_ctx); + ret = NfcCommandStop; + } + + return ret; +} + +static NfcCommand plugin_hf_poller_callback_picopass(PicopassPollerEvent event, void* context) { + PluginHfContext* ctx = plugin_hf_require_ctx(context); + NfcCommand ret = NfcCommandContinue; + PluginHfStage stage = ctx->api->get_stage(ctx->host_ctx); + + if(event.type == PicopassPollerEventTypeCardDetected) { + HF_DIAG_D("Picopass card detected"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageCardDetect); + } else if(event.type == PicopassPollerEventTypeSuccess) { + HF_DIAG_D("Picopass success stage=%d", stage); + if(stage == PluginHfStageCardDetect) { + ctx->api->notify_card_detected(ctx->host_ctx); + if(!ctx->api->sam_can_accept_card(ctx->host_ctx)) { + return NfcCommandContinue; + } + uint8_t* csn = ctx->api->picopass_get_csn(ctx->host_ctx); + furi_check(csn); + ctx->api->send_card_detected( + ctx->host_ctx, 0, csn, sizeof(PicopassSerialNum), NULL, 0); + FURI_LOG_D(TAG, "Picopass cardDetected delivered"); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageConversation); + } else if(stage == PluginHfStageConversation) { + FURI_LOG_D(TAG, "Picopass enter conversation"); + ctx->api->run_conversation(ctx->host_ctx); + stage = ctx->api->get_stage(ctx->host_ctx); + if(stage == PluginHfStageComplete) { + ret = NfcCommandStop; + } else if(stage == PluginHfStageFail) { + ctx->api->notify_worker_exit(ctx->host_ctx); + ret = NfcCommandStop; + } + } else if(stage == PluginHfStageComplete) { + ret = NfcCommandStop; + } else if(stage == PluginHfStageFail) { + ctx->api->notify_worker_exit(ctx->host_ctx); + ret = NfcCommandStop; + } + } else if(event.type == PicopassPollerEventTypeFail) { + ret = NfcCommandStop; + } + + return ret; +} + +static void* plugin_hf_alloc(const PluginHfHostApi* api, void* host_ctx) { + PluginHfContext* ctx = calloc(1, sizeof(PluginHfContext)); + if(!ctx) { + FURI_LOG_E(TAG, "Failed to allocate plugin context"); + return NULL; + } + if(!host_ctx) { + FURI_LOG_E(TAG, "Missing HF host context"); + free(ctx); + return NULL; + } + if(!plugin_hf_validate_host_api(api)) { + free(ctx); + return NULL; + } + ctx->api = api; + ctx->host_ctx = host_ctx; + ctx->nfc = api->get_nfc ? api->get_nfc(host_ctx) : NULL; + ctx->nfc_device = api->get_nfc_device ? api->get_nfc_device(host_ctx) : NULL; + if(!ctx->nfc || !ctx->nfc_device) { + FURI_LOG_E( + TAG, + "Host NFC objects unavailable nfc=%p device=%p", + (void*)ctx->nfc, + (void*)ctx->nfc_device); + free(ctx); + return NULL; + } + return ctx; +} + +static void plugin_hf_free(void* plugin_ctx) { + PluginHfContext* ctx = plugin_hf_require_ctx(plugin_ctx); + plugin_hf_cleanup_pollers(ctx); + free(ctx); +} + +static size_t plugin_hf_detect_supported_types( + void* plugin_ctx, + SeaderCredentialType* detected_types, + size_t detected_capacity) { + PluginHfContext* ctx = plugin_hf_require_ctx(plugin_ctx); + furi_check(detected_types); + furi_check(detected_capacity > 0U); + size_t detected_type_count = 0; + HF_DIAG_D("Detect supported HF types"); + NfcPoller* poller_detect = nfc_poller_alloc(ctx->nfc, NfcProtocolIso14443_4a); + if(!poller_detect) { + FURI_LOG_W(TAG, "Failed to allocate 14A detect poller"); + } else if(nfc_poller_detect(poller_detect)) { + plugin_hf_add_detected_type( + detected_types, &detected_type_count, detected_capacity, SeaderCredentialType14A); + } + if(poller_detect) nfc_poller_free(poller_detect); + + poller_detect = nfc_poller_alloc(ctx->nfc, NfcProtocolMfClassic); + if(!poller_detect) { + FURI_LOG_W(TAG, "Failed to allocate MFC detect poller"); + } else if(nfc_poller_detect(poller_detect)) { + plugin_hf_add_detected_type( + detected_types, + &detected_type_count, + detected_capacity, + SeaderCredentialTypeMifareClassic); + } + if(poller_detect) nfc_poller_free(poller_detect); + + if(ctx->api->picopass_detect && ctx->api->picopass_detect(ctx->host_ctx)) { + plugin_hf_add_detected_type( + detected_types, &detected_type_count, detected_capacity, SeaderCredentialTypePicopass); + } + + return detected_type_count; +} + +static bool plugin_hf_start_read_for_type(void* plugin_ctx, SeaderCredentialType type) { + PluginHfContext* ctx = plugin_hf_require_ctx(plugin_ctx); + NfcPoller* poller_detect = NULL; + + plugin_hf_cleanup_pollers(ctx); + ctx->active_type = type; + HF_DIAG_I("Start read type=%d", type); + + if(type == SeaderCredentialType14A) { + poller_detect = nfc_poller_alloc(ctx->nfc, NfcProtocolIso14443_4a); + if(!poller_detect) { + FURI_LOG_E(TAG, "Failed to allocate 14A detect poller"); + return false; + } + if(!nfc_poller_detect(poller_detect)) { + nfc_poller_free(poller_detect); + return false; + } + nfc_poller_free(poller_detect); + ctx->poller = nfc_poller_alloc(ctx->nfc, NfcProtocolIso14443_4a); + if(!ctx->poller) { + FURI_LOG_E(TAG, "Failed to allocate 14A poller"); + return false; + } + ctx->api->set_credential_type(ctx->host_ctx, SeaderCredentialType14A); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageCardDetect); + nfc_poller_start(ctx->poller, plugin_hf_poller_callback_iso14443_4a, ctx); + return true; + } else if(type == SeaderCredentialTypeMifareClassic) { + poller_detect = nfc_poller_alloc(ctx->nfc, NfcProtocolMfClassic); + if(!poller_detect) { + FURI_LOG_E(TAG, "Failed to allocate MFC detect poller"); + return false; + } + if(!nfc_poller_detect(poller_detect)) { + nfc_poller_free(poller_detect); + return false; + } + nfc_poller_free(poller_detect); + ctx->poller = nfc_poller_alloc(ctx->nfc, NfcProtocolMfClassic); + if(!ctx->poller) { + FURI_LOG_E(TAG, "Failed to allocate MFC poller"); + return false; + } + ctx->api->set_credential_type(ctx->host_ctx, SeaderCredentialTypeMifareClassic); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageCardDetect); + nfc_poller_start(ctx->poller, plugin_hf_poller_callback_mfc, ctx); + return true; + } else if(type == SeaderCredentialTypePicopass) { + if(!ctx->api->picopass_detect || !ctx->api->picopass_detect(ctx->host_ctx)) { + return false; + } + ctx->api->set_credential_type(ctx->host_ctx, SeaderCredentialTypePicopass); + ctx->api->set_stage(ctx->host_ctx, PluginHfStageCardDetect); + return ctx->api->picopass_start && + ctx->api->picopass_start(ctx->host_ctx, plugin_hf_poller_callback_picopass, ctx); + } + + return false; +} + +static void plugin_hf_stop(void* plugin_ctx) { + PluginHfContext* ctx = plugin_hf_require_ctx(plugin_ctx); + plugin_hf_cleanup_pollers(ctx); + ctx->active_type = SeaderCredentialTypeNone; +} + +static bool plugin_hf_handle_action(void* plugin_ctx, const PluginHfAction* action) { + PluginHfContext* ctx = plugin_hf_require_ctx(plugin_ctx); + furi_check(action); + HF_DIAG_D("Handle action type=%d len=%u", action->type, action->len); + + if(action->type == PluginHfActionTypePicopassTx) { + if(ctx->active_type != SeaderCredentialTypePicopass) return false; + plugin_hf_iso15693_transmit(ctx, action->data, action->len); + return true; + } else if(action->type == PluginHfActionTypeMfClassicTx) { + if(!ctx->poller) return false; + plugin_hf_mfc_transmit( + ctx, action->data, action->len, action->timeout, (uint8_t*)action->format); + return true; + } else if(action->type == PluginHfActionTypeIso14443Tx) { + if(!ctx->poller) return false; + plugin_hf_iso14443a_transmit( + ctx, action->data, action->len, action->timeout, (uint8_t*)action->format); + return true; + } + + FURI_LOG_W(TAG, "Unhandled HF action %d", action->type); + return false; +} + +static const PluginHf plugin_hf = { + .name = "Plugin HF", + .alloc = plugin_hf_alloc, + .free = plugin_hf_free, + .detect_supported_types = plugin_hf_detect_supported_types, + .start_read_for_type = plugin_hf_start_read_for_type, + .stop = plugin_hf_stop, + .handle_action = plugin_hf_handle_action, +}; + +static const FlipperAppPluginDescriptor plugin_hf_descriptor = { + .appid = HF_PLUGIN_APP_ID, + .ep_api_version = HF_PLUGIN_API_VERSION, + .entry_point = &plugin_hf, +}; + +const FlipperAppPluginDescriptor* plugin_hf_ep(void) { + return &plugin_hf_descriptor; +} diff --git a/hf_interface_fal/hf_interface.h b/hf_interface_fal/hf_interface.h new file mode 100644 index 0000000..9f60247 --- /dev/null +++ b/hf_interface_fal/hf_interface.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include + +#include "../protocol/picopass_poller.h" +#include "../seader_credential_type.h" +#include +#include + +#define HF_PLUGIN_APP_ID "plugin_hf" +#define HF_PLUGIN_API_VERSION 1 + +typedef enum { + PluginHfStageCardDetect = 0, + PluginHfStageConversation, + PluginHfStageComplete, + PluginHfStageSuccess, + PluginHfStageFail, +} PluginHfStage; + +typedef enum { + PluginHfActionTypeIso14443Tx, + PluginHfActionTypeMfClassicTx, + PluginHfActionTypePicopassTx, +} PluginHfActionType; + +typedef struct { + PluginHfActionType type; + uint8_t* data; + size_t len; + uint16_t timeout; + uint8_t format[3]; +} PluginHfAction; + +typedef struct { + /* Required runtime callbacks. A successful plugin alloc assumes these remain valid until free. */ + void (*notify_card_detected)(void* host_ctx); + void (*notify_worker_exit)(void* host_ctx); + bool (*sam_can_accept_card)(void* host_ctx); + void (*send_card_detected)( + void* host_ctx, + uint8_t sak, + const uint8_t* uid, + uint8_t uid_len, + const uint8_t* ats, + uint8_t ats_len); + void (*send_nfc_rx)(void* host_ctx, uint8_t* buffer, size_t len); + void (*run_conversation)(void* host_ctx); + void (*set_stage)(void* host_ctx, PluginHfStage stage); + PluginHfStage (*get_stage)(void* host_ctx); + void (*set_credential_type)(void* host_ctx, SeaderCredentialType type); + SeaderCredentialType (*get_credential_type)(void* host_ctx); + bool (*get_desfire_ev2)(void* host_ctx); + void (*set_desfire_ev2)(void* host_ctx, bool is_desfire_ev2); + void (*append_picopass_sio)(void* host_ctx, uint8_t block_num, const uint8_t* data, size_t len); + void (*set_14a_sio)(void* host_ctx, const uint8_t* data, size_t len); + Nfc* (*get_nfc)(void* host_ctx); + NfcDevice* (*get_nfc_device)(void* host_ctx); + + /* Required Picopass hooks. All Flippers expose Picopass through the HF host API. */ + bool (*picopass_detect)(void* host_ctx); + bool (*picopass_start)(void* host_ctx, PicopassPollerCallback callback, void* callback_ctx); + void (*picopass_stop)(void* host_ctx); + uint8_t* (*picopass_get_csn)(void* host_ctx); + bool (*picopass_transmit)( + void* host_ctx, + const uint8_t* tx_data, + size_t tx_len, + uint8_t* rx_data, + size_t rx_capacity, + size_t* rx_len, + uint32_t fwt_fc); + + /* Optional UX hook for richer read failure text. */ + void (*set_read_error)(void* host_ctx, const char* text); +} PluginHfHostApi; + +typedef struct { + const char* name; + void* (*alloc)(const PluginHfHostApi* api, void* host_ctx); + void (*free)(void* plugin_ctx); + size_t (*detect_supported_types)( + void* plugin_ctx, + SeaderCredentialType* detected_types, + size_t detected_capacity); + bool (*start_read_for_type)(void* plugin_ctx, SeaderCredentialType type); + void (*stop)(void* plugin_ctx); + bool (*handle_action)(void* plugin_ctx, const PluginHfAction* action); +} PluginHf; diff --git a/hf_release_sequence.c b/hf_release_sequence.c new file mode 100644 index 0000000..409bc62 --- /dev/null +++ b/hf_release_sequence.c @@ -0,0 +1,34 @@ +#include "hf_release_sequence.h" + +static void seader_hf_release_callback_invoke(SeaderHfReleaseCallback callback, void* context) { + if(callback) { + callback(context); + } +} + +/* This is the one canonical HF release order used by production teardown paths and by + the runtime-integration tests. It mirrors the ownership documentation so teardown ordering can + be reviewed and exercised without duplicating the sequence in multiple call sites. */ +void seader_hf_release_sequence_run(SeaderHfReleaseSequence* sequence) { + if(!sequence) { + return; + } + + if(sequence->hf_session_state) { + *sequence->hf_session_state = SeaderHfSessionStateTearingDown; + } + /* Stop live I/O before freeing any HF-owned or host-owned runtime objects. */ + seader_hf_release_callback_invoke(sequence->plugin_stop, sequence->context); + seader_hf_release_callback_invoke(sequence->host_poller_release, sequence->context); + seader_hf_release_callback_invoke(sequence->host_picopass_release, sequence->context); + seader_hf_release_callback_invoke(sequence->plugin_free, sequence->context); + seader_hf_release_callback_invoke(sequence->plugin_manager_unload, sequence->context); + /* Reset worker-visible session state before publishing Unloaded/None. */ + seader_hf_release_callback_invoke(sequence->worker_reset, sequence->context); + if(sequence->hf_session_state) { + *sequence->hf_session_state = SeaderHfSessionStateUnloaded; + } + if(sequence->mode_runtime && *sequence->mode_runtime == SeaderModeRuntimeHF) { + *sequence->mode_runtime = SeaderModeRuntimeNone; + } +} diff --git a/hf_release_sequence.h b/hf_release_sequence.h new file mode 100644 index 0000000..015f8f5 --- /dev/null +++ b/hf_release_sequence.h @@ -0,0 +1,19 @@ +#pragma once + +#include "seader.h" + +typedef void (*SeaderHfReleaseCallback)(void* context); + +typedef struct { + void* context; + SeaderHfSessionState* hf_session_state; + SeaderModeRuntime* mode_runtime; + SeaderHfReleaseCallback plugin_stop; + SeaderHfReleaseCallback host_poller_release; + SeaderHfReleaseCallback host_picopass_release; + SeaderHfReleaseCallback plugin_free; + SeaderHfReleaseCallback plugin_manager_unload; + SeaderHfReleaseCallback worker_reset; +} SeaderHfReleaseSequence; + +void seader_hf_release_sequence_run(SeaderHfReleaseSequence* sequence); diff --git a/lib/host_tests/README.md b/lib/host_tests/README.md new file mode 100644 index 0000000..a11f595 --- /dev/null +++ b/lib/host_tests/README.md @@ -0,0 +1,7 @@ +# Host Test Layers + +- `make test-host`: fast deterministic unit/policy tests. Keep this limited to Seader-owned helpers, formatting, parsing, and ownership-policy logic. +- `make test-asn1-integration`: narrow integration coverage for real ASN.1 ownership/free behavior. +- `make test-runtime-integration`: narrow mock-based integration coverage for HF release ordering and final state publication. + +Do not add generated ASN.1 or firmware-heavy runtime dependencies to `make test-host` unless they are already a supported part of that surface. diff --git a/lib/host_tests/t_1_host_env.h b/lib/host_tests/t_1_host_env.h index 78fd953..e589609 100644 --- a/lib/host_tests/t_1_host_env.h +++ b/lib/host_tests/t_1_host_env.h @@ -14,6 +14,13 @@ /* Keep the host harness aligned with the production UART scratchpad size. */ #define SEADER_UART_RX_BUF_SIZE (300) #define FURI_LOG_W(tag, fmt, ...) ((void)0) +#define furi_check(expr) \ + do { \ + if(!(expr)) { \ + fprintf(stderr, "furi_check failed: %s (%s:%d)\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while(0) typedef struct BitBuffer BitBuffer; typedef struct Seader Seader; diff --git a/lib/host_tests/test_card_details_builder.c b/lib/host_tests/test_card_details_builder.c new file mode 100644 index 0000000..d95406e --- /dev/null +++ b/lib/host_tests/test_card_details_builder.c @@ -0,0 +1,93 @@ +#include "munit.h" + +#include "card_details_builder.h" + +static MunitResult test_builds_type4_with_owned_optional_fields( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + const uint8_t uid[] = {0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + const uint8_t ats[] = {0x75, 0x77, 0x81, 0x02}; + CardDetails_t card_details = {0}; + + munit_assert_true( + seader_card_details_build(&card_details, 0x20U, uid, sizeof(uid), ats, sizeof(ats))); + munit_assert_size(card_details.csn.size, ==, sizeof(uid)); + munit_assert_not_null(card_details.sak); + munit_assert_not_null(card_details.atsOrAtqbOrAtr); + munit_assert_size(card_details.sak->size, ==, 1U); + munit_assert_size(card_details.atsOrAtqbOrAtr->size, ==, sizeof(ats)); + munit_assert_memory_equal(sizeof(uid), card_details.csn.buf, uid); + munit_assert_memory_equal(sizeof(ats), card_details.atsOrAtqbOrAtr->buf, ats); + + seader_card_details_reset(&card_details); + munit_assert_ptr_null(card_details.sak); + munit_assert_ptr_null(card_details.atsOrAtqbOrAtr); + munit_assert_ptr_null(card_details.csn.buf); + return MUNIT_OK; +} + +static MunitResult test_builds_picopass_without_optional_fields( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + const uint8_t uid[] = {1, 2, 3, 4, 5, 6, 7, 8}; + CardDetails_t card_details = {0}; + + munit_assert_true(seader_card_details_build(&card_details, 0U, uid, sizeof(uid), NULL, 0U)); + munit_assert_ptr_null(card_details.sak); + munit_assert_ptr_null(card_details.atsOrAtqbOrAtr); + + seader_card_details_reset(&card_details); + return MUNIT_OK; +} + +static MunitResult test_builds_mfc_with_owned_sak( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + const uint8_t uid[] = {0xDE, 0xAD, 0xBE, 0xEF}; + CardDetails_t card_details = {0}; + + munit_assert_true(seader_card_details_build(&card_details, 0x08U, uid, sizeof(uid), NULL, 0U)); + munit_assert_not_null(card_details.sak); + munit_assert_size(card_details.sak->size, ==, 1U); + munit_assert_ptr_null(card_details.atsOrAtqbOrAtr); + + seader_card_details_reset(&card_details); + munit_assert_ptr_null(card_details.sak); + return MUNIT_OK; +} + +static MunitResult test_rejects_invalid_input(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + CardDetails_t card_details = {0}; + + munit_assert_false(seader_card_details_build(&card_details, 0U, NULL, 0U, NULL, 0U)); + munit_assert_ptr_null(card_details.csn.buf); + return MUNIT_OK; +} + +static MunitTest test_card_details_builder_cases[] = { + {(char*)"/type4-owned-optional-fields", test_builds_type4_with_owned_optional_fields, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/picopass-no-optional-fields", test_builds_picopass_without_optional_fields, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/mfc-owned-sak", test_builds_mfc_with_owned_sak, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/invalid-input", test_rejects_invalid_input, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {NULL, NULL, NULL, NULL, 0, NULL}, +}; + +MunitSuite test_card_details_builder_suite = { + "", + test_card_details_builder_cases, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; diff --git a/lib/host_tests/test_card_details_main.c b/lib/host_tests/test_card_details_main.c new file mode 100644 index 0000000..12ff082 --- /dev/null +++ b/lib/host_tests/test_card_details_main.c @@ -0,0 +1,15 @@ +#include "munit.h" + +extern MunitSuite test_card_details_builder_suite; + +int main(int argc, char* argv[]) { + MunitSuite main_suite = { + "/card-details", + test_card_details_builder_suite.tests, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, + }; + + return munit_suite_main(&main_suite, NULL, argc, argv); +} diff --git a/lib/host_tests/test_credential_sio_label.c b/lib/host_tests/test_credential_sio_label.c new file mode 100644 index 0000000..820895f --- /dev/null +++ b/lib/host_tests/test_credential_sio_label.c @@ -0,0 +1,76 @@ +#include "munit.h" +#include "credential_sio_label.h" + +static MunitResult test_returns_false_without_sio(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + char label[16] = "unchanged"; + + munit_assert_false( + seader_sio_label_format(false, false, 0U, label, sizeof(label))); + munit_assert_string_equal(label, ""); + return MUNIT_OK; +} + +static MunitResult test_formats_generic_sio_for_desfire( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + char label[16] = {0}; + + munit_assert_true( + seader_sio_label_format(true, false, 0U, label, sizeof(label))); + munit_assert_string_equal(label, "+SIO"); + return MUNIT_OK; +} + +static MunitResult test_formats_sr_and_se_for_picopass( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + char label[16] = {0}; + + munit_assert_true( + seader_sio_label_format(true, true, 6U, label, sizeof(label))); + munit_assert_string_equal(label, "+SIO(SE)"); + + munit_assert_true( + seader_sio_label_format(true, true, 10U, label, sizeof(label))); + munit_assert_string_equal(label, "+SIO(SR)"); + return MUNIT_OK; +} + +static MunitResult test_formats_unknown_picopass_layout( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + char label[16] = {0}; + + munit_assert_true( + seader_sio_label_format(true, true, 0U, label, sizeof(label))); + munit_assert_string_equal(label, "+SIO(?)"); + return MUNIT_OK; +} + +static MunitTest test_credential_sio_label_cases[] = { + {(char*)"/no-sio", test_returns_false_without_sio, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/desfire-generic", test_formats_generic_sio_for_desfire, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/picopass-sr-se", test_formats_sr_and_se_for_picopass, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/picopass-unknown", test_formats_unknown_picopass_layout, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {NULL, NULL, NULL, NULL, 0, NULL}, +}; + +MunitSuite test_credential_sio_label_suite = { + "", + test_credential_sio_label_cases, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; diff --git a/lib/host_tests/test_hf_release_sequence.c b/lib/host_tests/test_hf_release_sequence.c new file mode 100644 index 0000000..ba24631 --- /dev/null +++ b/lib/host_tests/test_hf_release_sequence.c @@ -0,0 +1,108 @@ +#include "munit.h" + +#include "hf_release_sequence.h" + +typedef struct { + unsigned index; + const char* calls[8]; +} ReleaseRecorder; + +static void record_call(ReleaseRecorder* recorder, const char* name) { + if(recorder && recorder->index < (sizeof(recorder->calls) / sizeof(recorder->calls[0]))) { + recorder->calls[recorder->index++] = name; + } +} + +static void record_plugin_stop(void* context) { + record_call(context, "plugin-stop"); +} + +static void record_host_poller_release(void* context) { + record_call(context, "host-poller-release"); +} + +static void record_picopass_release(void* context) { + record_call(context, "picopass-release"); +} + +static void record_plugin_free(void* context) { + record_call(context, "plugin-free"); +} + +static void record_manager_unload(void* context) { + record_call(context, "plugin-manager-unload"); +} + +static void record_worker_reset(void* context) { + record_call(context, "worker-reset"); +} + +static MunitResult test_release_sequence_orders_operations_and_finalizes_state( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + ReleaseRecorder recorder = {0}; + SeaderHfSessionState hf_state = SeaderHfSessionStateActive; + SeaderModeRuntime mode_runtime = SeaderModeRuntimeHF; + SeaderHfReleaseSequence sequence = { + .context = &recorder, + .hf_session_state = &hf_state, + .mode_runtime = &mode_runtime, + .plugin_stop = record_plugin_stop, + .host_poller_release = record_host_poller_release, + .host_picopass_release = record_picopass_release, + .plugin_free = record_plugin_free, + .plugin_manager_unload = record_manager_unload, + .worker_reset = record_worker_reset, + }; + + seader_hf_release_sequence_run(&sequence); + + munit_assert_int(hf_state, ==, SeaderHfSessionStateUnloaded); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeNone); + munit_assert_uint(recorder.index, ==, 6); + munit_assert_string_equal(recorder.calls[0], "plugin-stop"); + munit_assert_string_equal(recorder.calls[1], "host-poller-release"); + munit_assert_string_equal(recorder.calls[2], "picopass-release"); + munit_assert_string_equal(recorder.calls[3], "plugin-free"); + munit_assert_string_equal(recorder.calls[4], "plugin-manager-unload"); + munit_assert_string_equal(recorder.calls[5], "worker-reset"); + return MUNIT_OK; +} + +static MunitResult test_release_sequence_tolerates_missing_callbacks( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderHfSessionState hf_state = SeaderHfSessionStateLoaded; + SeaderModeRuntime mode_runtime = SeaderModeRuntimeHF; + SeaderHfReleaseSequence sequence = { + .hf_session_state = &hf_state, + .mode_runtime = &mode_runtime, + .worker_reset = record_worker_reset, + }; + + seader_hf_release_sequence_run(&sequence); + + munit_assert_int(hf_state, ==, SeaderHfSessionStateUnloaded); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeNone); + return MUNIT_OK; +} + +static MunitTest test_hf_release_sequence_cases[] = { + {(char*)"/ordering", test_release_sequence_orders_operations_and_finalizes_state, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/missing-callbacks", test_release_sequence_tolerates_missing_callbacks, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {NULL, NULL, NULL, NULL, 0, NULL}, +}; + +MunitSuite test_hf_release_sequence_suite = { + "", + test_hf_release_sequence_cases, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; diff --git a/lib/host_tests/test_main.c b/lib/host_tests/test_main.c index e5fbb55..32ec92a 100644 --- a/lib/host_tests/test_main.c +++ b/lib/host_tests/test_main.c @@ -7,6 +7,8 @@ extern MunitSuite test_t1_existing_suite; extern MunitSuite test_t1_protocol_suite; extern MunitSuite test_snmp_suite; extern MunitSuite test_uhf_status_label_suite; +extern MunitSuite test_credential_sio_label_suite; +extern MunitSuite test_runtime_policy_suite; int main(int argc, char* argv[]) { MunitSuite child_suites[] = { @@ -17,6 +19,8 @@ int main(int argc, char* argv[]) { {"/ccid", test_ccid_logic_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/snmp", test_snmp_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/uhf-status-label", test_uhf_status_label_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, + {"/credential-sio-label", test_credential_sio_label_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, + {"/runtime-policy", test_runtime_policy_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {NULL, NULL, NULL, 0, 0}, }; MunitSuite main_suite = { diff --git a/lib/host_tests/test_runtime_integration_main.c b/lib/host_tests/test_runtime_integration_main.c new file mode 100644 index 0000000..7adf9bc --- /dev/null +++ b/lib/host_tests/test_runtime_integration_main.c @@ -0,0 +1,15 @@ +#include "munit.h" + +extern MunitSuite test_hf_release_sequence_suite; + +int main(int argc, char* argv[]) { + MunitSuite main_suite = { + "/runtime-integration", + test_hf_release_sequence_suite.tests, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, + }; + + return munit_suite_main(&main_suite, NULL, argc, argv); +} diff --git a/lib/host_tests/test_runtime_policy.c b/lib/host_tests/test_runtime_policy.c new file mode 100644 index 0000000..0ecd088 --- /dev/null +++ b/lib/host_tests/test_runtime_policy.c @@ -0,0 +1,139 @@ +#include + +#include "munit.h" +#include "runtime_policy.h" + +static MunitResult test_reset_cached_sam_metadata_clears_all_fields( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + uint8_t sam_version[2] = {1U, 99U}; + char uhf_status_label[16]; + memset(uhf_status_label, 'X', sizeof(uhf_status_label)); + SeaderUhfSnmpProbe probe = { + .stage = SeaderUhfSnmpProbeStageDone, + .has_monza4qt = true, + .has_higgs3 = true, + .monza4qt_key_present = true, + .higgs3_key_present = true, + .ice_value_len = 7U, + }; + + seader_runtime_reset_cached_sam_metadata( + sam_version, uhf_status_label, sizeof(uhf_status_label), &probe); + + munit_assert_uint8(sam_version[0], ==, 0U); + munit_assert_uint8(sam_version[1], ==, 0U); + munit_assert_char(uhf_status_label[0], ==, '\0'); + munit_assert_int(probe.stage, ==, SeaderUhfSnmpProbeStageDiscovery); + munit_assert_false(probe.has_monza4qt); + munit_assert_false(probe.has_higgs3); + munit_assert_false(probe.monza4qt_key_present); + munit_assert_false(probe.higgs3_key_present); + munit_assert_size(probe.ice_value_len, ==, 0U); + return MUNIT_OK; +} + +static MunitResult test_begin_uhf_probe_sets_runtime_and_initializes_probe( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderModeRuntime mode_runtime = SeaderModeRuntimeNone; + SeaderUhfSnmpProbe probe = {.stage = SeaderUhfSnmpProbeStageDone}; + + munit_assert_true(seader_runtime_begin_uhf_probe( + true, &mode_runtime, SeaderHfSessionStateUnloaded, &probe)); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeUHF); + munit_assert_int(probe.stage, ==, SeaderUhfSnmpProbeStageDiscovery); + return MUNIT_OK; +} + +static MunitResult test_begin_uhf_probe_rejects_invalid_states( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderModeRuntime mode_runtime = SeaderModeRuntimeNone; + SeaderUhfSnmpProbe probe = {0}; + + munit_assert_false(seader_runtime_begin_uhf_probe( + false, &mode_runtime, SeaderHfSessionStateUnloaded, &probe)); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeNone); + + munit_assert_false(seader_runtime_begin_uhf_probe( + true, &mode_runtime, SeaderHfSessionStateActive, &probe)); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeNone); + + mode_runtime = SeaderModeRuntimeHF; + munit_assert_false(seader_runtime_begin_uhf_probe( + true, &mode_runtime, SeaderHfSessionStateUnloaded, &probe)); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeHF); + return MUNIT_OK; +} + +static MunitResult test_finish_uhf_probe_restores_none( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderModeRuntime mode_runtime = SeaderModeRuntimeUHF; + seader_runtime_finish_uhf_probe(&mode_runtime); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeNone); + + mode_runtime = SeaderModeRuntimeHF; + seader_runtime_finish_uhf_probe(&mode_runtime); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeHF); + return MUNIT_OK; +} + +static MunitResult test_finalize_hf_release_sets_terminal_state( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderHfSessionState hf_state = SeaderHfSessionStateTearingDown; + SeaderModeRuntime mode_runtime = SeaderModeRuntimeHF; + + seader_runtime_finalize_hf_release(&hf_state, &mode_runtime); + + munit_assert_int(hf_state, ==, SeaderHfSessionStateUnloaded); + munit_assert_int(mode_runtime, ==, SeaderModeRuntimeNone); + return MUNIT_OK; +} + +static MunitResult test_begin_hf_teardown_sets_state( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderHfSessionState hf_state = SeaderHfSessionStateActive; + seader_runtime_begin_hf_teardown(&hf_state); + munit_assert_int(hf_state, ==, SeaderHfSessionStateTearingDown); + return MUNIT_OK; +} + +static MunitTest test_runtime_policy_cases[] = { + {(char*)"/reset-sam-metadata", test_reset_cached_sam_metadata_clears_all_fields, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/begin-uhf-probe", test_begin_uhf_probe_sets_runtime_and_initializes_probe, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/begin-uhf-probe-invalid", test_begin_uhf_probe_rejects_invalid_states, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/finish-uhf-probe", test_finish_uhf_probe_restores_none, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/begin-hf-teardown", test_begin_hf_teardown_sets_state, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/finalize-hf-release", test_finalize_hf_release_sets_terminal_state, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {NULL, NULL, NULL, NULL, 0, NULL}, +}; + +MunitSuite test_runtime_policy_suite = { + "", + test_runtime_policy_cases, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; diff --git a/lib/host_tests/test_uhf_status_label.c b/lib/host_tests/test_uhf_status_label.c index 0bf5d64..ec467bf 100644 --- a/lib/host_tests/test_uhf_status_label.c +++ b/lib/host_tests/test_uhf_status_label.c @@ -1,3 +1,5 @@ +#include + #include "munit.h" #include "uhf_status_label.h" @@ -19,9 +21,68 @@ static MunitResult test_formats_supported_key_states(const MunitParameter params return MUNIT_OK; } +static MunitResult test_handles_null_and_zero_sized_output( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + seader_uhf_status_label_format(true, true, false, false, NULL, 0U); + + char label[4] = {'X', 'Y', 'Z', 'W'}; + seader_uhf_status_label_format(true, true, false, false, label, 0U); + munit_assert_memory_equal(sizeof(label), label, ((char[]){'X', 'Y', 'Z', 'W'})); + return MUNIT_OK; +} + +static MunitResult test_nul_terminates_single_byte_output( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + char label[1] = {'X'}; + seader_uhf_status_label_format(true, false, true, false, label, sizeof(label)); + munit_assert_char(label[0], ==, '\0'); + return MUNIT_OK; +} + +static MunitResult test_truncates_safely_for_small_buffers( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + char label[8]; + memset(label, 'Z', sizeof(label)); + seader_uhf_status_label_format(true, false, true, false, label, sizeof(label)); + + munit_assert_char(label[sizeof(label) - 1], ==, '\0'); + munit_assert_char(label[0], ==, 'U'); + munit_assert_char(label[1], ==, 'H'); + return MUNIT_OK; +} + +static MunitResult test_small_buffer_for_none_is_safe(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + char label[4]; + memset(label, 'Q', sizeof(label)); + seader_uhf_status_label_format(false, false, false, false, label, sizeof(label)); + + munit_assert_char(label[sizeof(label) - 1], ==, '\0'); + munit_assert_char(label[0], ==, 'U'); + return MUNIT_OK; +} + static MunitTest test_uhf_status_label_cases[] = { {(char*)"/none", test_formats_none, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {(char*)"/supported-key-states", test_formats_supported_key_states, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/null-zero-output", test_handles_null_and_zero_sized_output, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/single-byte-output", test_nul_terminates_single_byte_output, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/small-buffer-truncation", test_truncates_safely_for_small_buffers, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/small-buffer-none", test_small_buffer_for_none_is_safe, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, {NULL, NULL, NULL, NULL, 0, NULL}, }; diff --git a/plugin b/plugin deleted file mode 160000 index dde6192..0000000 --- a/plugin +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dde619248681e51a1097c5a0eba7eae91aeddb9d diff --git a/runtime_policy.c b/runtime_policy.c new file mode 100644 index 0000000..5b8e44f --- /dev/null +++ b/runtime_policy.c @@ -0,0 +1,78 @@ +#include "runtime_policy.h" + +/* A newly accepted SAM must not inherit visible metadata from the previous card while + asynchronous version/serial/UHF maintenance responses are still in flight. */ +void seader_runtime_reset_cached_sam_metadata( + uint8_t sam_version[2], + char* uhf_status_label, + size_t label_size, + SeaderUhfSnmpProbe* probe) { + if(sam_version) { + sam_version[0] = 0U; + sam_version[1] = 0U; + } + + if(uhf_status_label && label_size > 0U) { + uhf_status_label[0] = '\0'; + } + + if(probe) { + seader_uhf_snmp_probe_init(probe); + } +} + +/* UHF maintenance is a mutually exclusive runtime mode. The probe may only start when + the SAM is present, HF is fully unloaded, and no other mode currently owns runtime. */ +bool seader_runtime_begin_uhf_probe( + bool sam_present, + SeaderModeRuntime* mode_runtime, + SeaderHfSessionState hf_session_state, + SeaderUhfSnmpProbe* probe) { + if(!sam_present || !mode_runtime || !probe) { + return false; + } + + if(hf_session_state != SeaderHfSessionStateUnloaded) { + return false; + } + + if(*mode_runtime != SeaderModeRuntimeNone) { + return false; + } + + *mode_runtime = SeaderModeRuntimeUHF; + seader_uhf_snmp_probe_init(probe); + return true; +} + +/* Clear the narrow UHF probe runtime only when it currently owns mode_runtime. */ +void seader_runtime_finish_uhf_probe(SeaderModeRuntime* mode_runtime) { + if(!mode_runtime) { + return; + } + + if(*mode_runtime == SeaderModeRuntimeUHF) { + *mode_runtime = SeaderModeRuntimeNone; + } +} + +/* Teardown publishes TearingDown before any runtime release so acquire paths and teardown + request paths can see that HF work is already shutting down. */ +void seader_runtime_begin_hf_teardown(SeaderHfSessionState* hf_session_state) { + if(hf_session_state) { + *hf_session_state = SeaderHfSessionStateTearingDown; + } +} + +/* Final state publication happens only after the caller has completed the release sequence. */ +void seader_runtime_finalize_hf_release( + SeaderHfSessionState* hf_session_state, + SeaderModeRuntime* mode_runtime) { + if(hf_session_state) { + *hf_session_state = SeaderHfSessionStateUnloaded; + } + + if(mode_runtime && *mode_runtime == SeaderModeRuntimeHF) { + *mode_runtime = SeaderModeRuntimeNone; + } +} diff --git a/runtime_policy.h b/runtime_policy.h new file mode 100644 index 0000000..b0534f5 --- /dev/null +++ b/runtime_policy.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include "seader.h" +#include "uhf_snmp_probe.h" + +void seader_runtime_reset_cached_sam_metadata( + uint8_t sam_version[2], + char* uhf_status_label, + size_t label_size, + SeaderUhfSnmpProbe* probe); + +bool seader_runtime_begin_uhf_probe( + bool sam_present, + SeaderModeRuntime* mode_runtime, + SeaderHfSessionState hf_session_state, + SeaderUhfSnmpProbe* probe); + +void seader_runtime_finish_uhf_probe(SeaderModeRuntime* mode_runtime); + +void seader_runtime_begin_hf_teardown(SeaderHfSessionState* hf_session_state); + +void seader_runtime_finalize_hf_release( + SeaderHfSessionState* hf_session_state, + SeaderModeRuntime* mode_runtime); diff --git a/sam_api.c b/sam_api.c index 973b40e..aa849f8 100644 --- a/sam_api.c +++ b/sam_api.c @@ -1,7 +1,11 @@ #include "sam_api.h" +#include "seader_i.h" +#include "protocol/rfal_picopass.h" #include "sam_key_label.h" #include "trace_log.h" #include "uhf_snmp_probe.h" +#include "runtime_policy.h" +#include "card_details_builder.h" #include "uhf_status_label.h" #include #include @@ -61,17 +65,42 @@ static void seader_update_uhf_status_label(Seader* seader) { seader_publish_sam_status(seader); } -static bool seader_snmp_probe_send_next_request(Seader* seader) { - SeaderWorker* seader_worker = seader ? seader->worker : NULL; - SeaderUartBridge* seader_uart = seader_worker ? seader_worker->uart : NULL; - uint8_t* scratch = seader_uart ? (seader_uart->tx_buf + MAX_FRAME_HEADERS) : NULL; - uint8_t* message = seader_uart ? seader_uart->rx_buf : NULL; - size_t message_len = 0U; +static SeaderWorker* seader_get_active_worker(Seader* seader) { + return seader ? seader->worker : NULL; +} - if(!seader || !scratch || !message) { - return false; +static SeaderUartBridge* seader_require_uart(Seader* seader) { + furi_check(seader); + furi_check(seader->uart); + return seader->uart; +} + +static SeaderWorker* seader_require_worker(Seader* seader) { + furi_check(seader); + furi_check(seader->worker); + return seader->worker; +} + +/* A newly inserted SAM should never inherit the previous card's cached firmware/UHF status + while maintenance probes for the new card are still pending. */ +static void seader_reset_cached_sam_metadata(Seader* seader) { + if(!seader) { + return; } + seader_runtime_reset_cached_sam_metadata( + seader->sam_version, + seader->uhf_status_label, + sizeof(seader->uhf_status_label), + &seader->snmp_probe); +} + +static bool seader_snmp_probe_send_next_request(Seader* seader) { + SeaderUartBridge* seader_uart = seader_require_uart(seader); + uint8_t* scratch = seader_uart->tx_buf + MAX_FRAME_HEADERS; + uint8_t* message = seader_uart->rx_buf; + size_t message_len = 0U; + if(!seader_uhf_snmp_probe_build_next_request( &seader->snmp_probe, scratch, @@ -85,20 +114,32 @@ static bool seader_snmp_probe_send_next_request(Seader* seader) { return seader_worker_send_process_snmp_message(seader, message, message_len); } +/* Finishing the maintenance probe returns mode ownership to the normal app flow and leaves + the SAM state machine idle for the next command. */ static void seader_snmp_probe_finish(Seader* seader) { if(!seader) { return; } + seader_runtime_finish_uhf_probe(&seader->mode_runtime); seader_sam_set_state(seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING); } +/* UHF maintenance is only legal when the SAM is present and HF runtime is fully unloaded. + The helper enforces that ownership boundary before any SNMP request is sent. */ static void seader_start_snmp_probe(Seader* seader) { if(!seader || !seader->sam_present) { return; } - seader_uhf_snmp_probe_init(&seader->snmp_probe); + if(!seader_runtime_begin_uhf_probe( + seader->sam_present, + &seader->mode_runtime, + seader->hf_session_state, + &seader->snmp_probe)) { + seader_snmp_probe_finish(seader); + return; + } seader_update_uhf_status_label(seader); seader_sam_set_state( seader, @@ -258,7 +299,6 @@ void* calloc(size_t count, size_t size) { } // Forward declarations -void seader_send_nfc_rx(Seader* seader, uint8_t* buffer, size_t len); static void seader_abort_active_read(Seader* seader); static void seader_sam_set_state( @@ -400,8 +440,7 @@ bool seader_send_apdu( uint8_t* payload, uint8_t payloadLen, bool in_scratchpad) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_require_uart(seader); bool extended = seader_uart->T == 1; uint8_t header_len = extended ? 7 : 5; @@ -486,8 +525,7 @@ void seader_send_payload( uint8_t from, uint8_t to, uint8_t replyTo) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_require_uart(seader); uint8_t* scratchpad = seader_uart->tx_buf + MAX_FRAME_HEADERS; size_t scratchpad_size = SEADER_UART_RX_BUF_SIZE - MAX_FRAME_HEADERS; @@ -634,6 +672,7 @@ void seader_worker_send_serial_number(Seader* seader) { void seader_worker_send_version(Seader* seader) { SamCommand_t samCommand = {0}; samCommand.present = SamCommand_PR_version; + seader_reset_cached_sam_metadata(seader); seader->sam_present = true; seader_update_sam_key_label(seader, NULL, 0U); seader_sam_set_state( @@ -651,7 +690,9 @@ bool seader_worker_send_process_snmp_message( Seader* seader, const uint8_t* message, size_t message_len) { - if(!seader || !message || message_len == 0U || message_len > UINT16_MAX) return false; + furi_check(seader); + furi_check(message); + if(message_len == 0U || message_len > UINT16_MAX) return false; SamCommand_t samCommand = {0}; samCommand.present = SamCommand_PR_processSNMPMessage; @@ -668,6 +709,9 @@ bool seader_worker_send_process_snmp_message( } void seader_send_card_detected(Seader* seader, CardDetails_t* cardDetails) { + furi_check(seader); + furi_check(cardDetails); + furi_check(cardDetails->csn.buf); CardDetected_t cardDetected = { .detectedCardDetails = *cardDetails, }; @@ -681,6 +725,13 @@ void seader_send_card_detected(Seader* seader, CardDetails_t* cardDetails) { payload.choice.samCommand = samCommand; seader_trace( TAG, "send cardDetected state=%d intent=%d", seader->sam_state, seader->sam_intent); + FURI_LOG_D( + TAG, + "Send cardDetected csn_len=%zu has_sak=%d has_ats=%d protocol_len=%zu", + cardDetails->csn.size, + cardDetails->sak != NULL, + cardDetails->atsOrAtqbOrAtr != NULL, + cardDetails->protocol.size); seader_send_payload( seader, &payload, ExternalApplicationA, SAMInterface, ExternalApplicationA); @@ -791,7 +842,7 @@ static bool seader_unpack_pacs2_bits(Seader* seader, const OCTET_STRING_t* pacs_ // ATR3: // 800207358106793D81F9F385820104A51E8004000000018106053000000000820B323330353139313232395A830152 #define MAX_VERSION_SIZE 60 -bool seader_parse_version(SeaderWorker* seader_worker, uint8_t* buf, size_t size) { +bool seader_parse_version(Seader* seader, uint8_t* buf, size_t size) { bool rtn = false; if(size > MAX_VERSION_SIZE) { // Too large to handle now @@ -820,12 +871,8 @@ bool seader_parse_version(SeaderWorker* seader_worker, uint8_t* buf, size_t size } #endif if(version.version.size == 2) { - memcpy(seader_worker->sam_version, version.version.buf, version.version.size); - FURI_LOG_I( - TAG, - "SAM Version: %d.%d", - seader_worker->sam_version[0], - seader_worker->sam_version[1]); + memcpy(seader->sam_version, version.version.buf, version.version.size); + FURI_LOG_I(TAG, "SAM Version: %d.%d", seader->sam_version[0], seader->sam_version[1]); } rtn = true; @@ -921,16 +968,19 @@ bool seader_parse_serial_number(Seader* seader, uint8_t* buf, size_t size) { } static void seader_abort_active_read(Seader* seader) { - SeaderWorker* seader_worker = seader->worker; - FURI_LOG_W(TAG, "Abort active read stage=%d sam=%d", seader_worker->stage, seader->samCommand); + SeaderWorker* seader_worker = seader_get_active_worker(seader); + const int stage = seader_worker ? (int)seader_worker->stage : -1; + FURI_LOG_W(TAG, "Abort active read stage=%d sam=%d", stage, seader->samCommand); seader_trace( TAG, "abort stage=%d sam=%d state=%d intent=%d", - seader_worker->stage, + stage, seader->samCommand, seader->sam_state, seader->sam_intent); - seader_worker->stage = SeaderPollerEventTypeFail; + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeFail; + } if(!seader_sam_has_active_card(seader) && seader->sam_state != SeaderSamStateClearPending) { seader_sam_set_state( seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING); @@ -961,7 +1011,10 @@ bool seader_parse_sam_response2(Seader* seader, SamResponse2_t* samResponse) { SeaderPacsMediaTypeUnknown; if(seader_unpack_pacs2_bits(seader, pacs)) { - seader->worker->stage = SeaderPollerEventTypeComplete; + SeaderWorker* seader_worker = seader_get_active_worker(seader); + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeComplete; + } seader_sam_set_state( seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING); } else { @@ -982,14 +1035,16 @@ bool seader_parse_sam_response2(Seader* seader, SamResponse2_t* samResponse) { } bool seader_parse_sam_response(Seader* seader, SamResponse_t* samResponse) { - SeaderWorker* seader_worker = seader->worker; + SeaderWorker* seader_worker = seader_get_active_worker(seader); switch(seader->sam_state) { case SeaderSamStateConversation: case SeaderSamStateFinishing: if(seader->sam_intent == SeaderSamIntentConfig) { FURI_LOG_I(TAG, "samResponse config"); - seader_worker->stage = SeaderPollerEventTypeFail; + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeFail; + } seader_sam_set_state( seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING); } else { @@ -999,7 +1054,7 @@ bool seader_parse_sam_response(Seader* seader, SamResponse_t* samResponse) { break; case SeaderSamStateVersionPending: FURI_LOG_I(TAG, "samResponse version"); - seader_parse_version(seader_worker, samResponse->buf, samResponse->size); + seader_parse_version(seader, samResponse->buf, samResponse->size); seader_worker_send_serial_number(seader); break; case SeaderSamStateSerialPending: @@ -1043,7 +1098,10 @@ bool seader_parse_sam_response(Seader* seader, SamResponse_t* samResponse) { break; case SeaderSamStateClearPending: FURI_LOG_I(TAG, "samResponse clear-detected-card ack"); - seader_trace(TAG, "cardDetected ack clear stage=%d", seader_worker->stage); + seader_trace( + TAG, + "cardDetected ack clear stage=%d", + seader_worker ? (int)seader_worker->stage : -1); seader_sam_set_state( seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING); break; @@ -1111,6 +1169,8 @@ void seader_capture_sio(BitBuffer* tx_buffer, BitBuffer* rx_buffer, SeaderCreden if(buffer[0] == RFAL_PICOPASS_CMD_READ4) { uint8_t block_num = buffer[1]; if(credential->sio_len == 0 && rxBuffer[0] == 0x30) { + /* Only Picopass uses block-derived SR/SE labeling, so remember where the + first ASN.1 SIO fragment was observed. */ credential->sio_start_block = block_num; } uint8_t offset = (block_num - credential->sio_start_block) * PICOPASS_BLOCK_LEN; @@ -1118,10 +1178,9 @@ void seader_capture_sio(BitBuffer* tx_buffer, BitBuffer* rx_buffer, SeaderCreden credential->sio_len += PICOPASS_BLOCK_LEN * 4; } } else if(credential->type == SeaderCredentialType14A) { - // Desfire EV1 passes SIO in the clear - // The desfire_read command is 13 bytes in total, but we deliberately don't check the read length as newer SAM - // firmware versions read 5 bytes first to determine the length of the SIO from the ASN.1 tag length then do a - // second read with just the required length to skip reading any additional bytes at the end of the file + /* DESFire exposes SIO as raw file data rather than as block-addressed Picopass reads. + Match the fixed read command body, but accept any response length that starts with + ASN.1 SEQUENCE data instead of expecting one exact returned payload size. */ uint8_t desfire_read[] = {0x90, 0xbd, 0x00, 0x00, 0x07, 0x0f, 0x00, 0x00, 0x00}; if(len == 13 && memcmp(buffer, desfire_read, sizeof(desfire_read)) == 0 && rxBuffer[0] == 0x30) { @@ -1141,7 +1200,7 @@ void seader_iso15693_transmit( PicopassPoller* picopass_poller, uint8_t* buffer, size_t len) { - SeaderWorker* seader_worker = seader->worker; + SeaderWorker* seader_worker = seader_get_active_worker(seader); BitBuffer* tx_buffer = bit_buffer_alloc(len); BitBuffer* rx_buffer = bit_buffer_alloc(SEADER_POLLER_MAX_BUFFER_SIZE); @@ -1162,7 +1221,9 @@ void seader_iso15693_transmit( } if(error != PicopassErrorNone) { - seader_worker->stage = SeaderPollerEventTypeFail; + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeFail; + } break; } @@ -1189,15 +1250,22 @@ void seader_iso14443a_transmit( UNUSED(timeout); UNUSED(format); - furi_assert(seader); - furi_assert(buffer); - furi_assert(iso14443_4a_poller); - SeaderWorker* seader_worker = seader->worker; + furi_check(seader); + furi_check(buffer); + furi_check(iso14443_4a_poller); + SeaderWorker* seader_worker = seader_require_worker(seader); SeaderCredential* credential = seader->credential; BitBuffer* tx_buffer = bit_buffer_alloc(len + 1); // extra byte to allow for appending a Le byte sometimes BitBuffer* rx_buffer = bit_buffer_alloc(SEADER_POLLER_MAX_BUFFER_SIZE); + if(!tx_buffer || !rx_buffer) { + FURI_LOG_E(TAG, "Failed to allocate 14A tx/rx buffers"); + if(tx_buffer) bit_buffer_free(tx_buffer); + if(rx_buffer) bit_buffer_free(rx_buffer); + if(seader_worker) seader_worker->stage = SeaderPollerEventTypeFail; + return; + } do { bit_buffer_append_bytes(tx_buffer, buffer, len); @@ -1217,7 +1285,9 @@ void seader_iso14443a_transmit( iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer); if(error != Iso14443_4aErrorNone) { FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error); - seader_worker->stage = SeaderPollerEventTypeFail; + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeFail; + } break; } @@ -1261,13 +1331,20 @@ void seader_mfc_transmit( uint8_t format[3]) { UNUSED(timeout); - furi_assert(seader); - furi_assert(buffer); - furi_assert(mfc_poller); - SeaderWorker* seader_worker = seader->worker; + furi_check(seader); + furi_check(buffer); + furi_check(mfc_poller); + SeaderWorker* seader_worker = seader_require_worker(seader); BitBuffer* tx_buffer = bit_buffer_alloc(len); BitBuffer* rx_buffer = bit_buffer_alloc(SEADER_POLLER_MAX_BUFFER_SIZE); + if(!tx_buffer || !rx_buffer) { + FURI_LOG_E(TAG, "Failed to allocate MFC tx/rx buffers"); + if(tx_buffer) bit_buffer_free(tx_buffer); + if(rx_buffer) bit_buffer_free(rx_buffer); + if(seader_worker) seader_worker->stage = SeaderPollerEventTypeFail; + return; + } do { seader_trace( @@ -1292,7 +1369,9 @@ void seader_mfc_transmit( if(error != MfClassicErrorNone) { FURI_LOG_W(TAG, "mf_classic_poller_send_frame error %d", error); seader_trace(TAG, "mfc send_frame error=%d", error); - seader_worker->stage = SeaderPollerEventTypeFail; + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeFail; + } break; } @@ -1358,7 +1437,9 @@ void seader_mfc_transmit( sizeof(seader->read_error), "Protected read timed out.\nNo supported data\nor wrong key."); } - seader_worker->stage = SeaderPollerEventTypeFail; + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeFail; + } break; } @@ -1440,43 +1521,48 @@ void seader_mfc_transmit( bit_buffer_free(rx_buffer); } -void seader_parse_nfc_command_transmit( - Seader* seader, - NFCSend_t* nfcSend, - SeaderPollerContainer* spc) { - long timeOut = nfcSend->timeOut; - Protocol_t protocol = nfcSend->protocol; - FrameProtocol_t frameProtocol = protocol.buf[1]; - +void seader_parse_nfc_command_transmit(Seader* seader, NFCSend_t* nfcSend) { #ifdef ASN1_DEBUG seader_log_hex_data(TAG, "Transmit data", nfcSend->data.buf, nfcSend->data.size); #endif + PluginHfAction action = { + .data = nfcSend->data.buf, + .len = nfcSend->data.size, + .timeout = nfcSend->timeOut, + }; + if(nfcSend->format) { + const size_t raw_format_len = (size_t)nfcSend->format->size; + const size_t format_len = raw_format_len < sizeof(action.format) ? raw_format_len : + sizeof(action.format); + memcpy(action.format, nfcSend->format->buf, format_len); + } + if(seader->credential->type == SeaderCredentialTypeVirtual) { seader_virtual_picopass_state_machine(seader, nfcSend->data.buf, nfcSend->data.size); - } else if(frameProtocol == FrameProtocol_iclass) { - seader_iso15693_transmit( - seader, spc->picopass_poller, nfcSend->data.buf, nfcSend->data.size); - } else if(frameProtocol == FrameProtocol_nfc) { - if(spc->iso14443_4a_poller) { - seader_iso14443a_transmit( - seader, - spc->iso14443_4a_poller, - nfcSend->data.buf, - nfcSend->data.size, - (uint16_t)timeOut, - nfcSend->format->buf); - } else if(spc->mfc_poller) { - seader_mfc_transmit( - seader, - spc->mfc_poller, - nfcSend->data.buf, - nfcSend->data.size, - (uint16_t)timeOut, - nfcSend->format->buf); + } else if(seader->plugin_hf && seader->hf_plugin_ctx) { + if(seader->credential->type == SeaderCredentialTypePicopass) { + action.type = PluginHfActionTypePicopassTx; + } else if(seader->credential->type == SeaderCredentialTypeMifareClassic) { + action.type = PluginHfActionTypeMfClassicTx; + } else { + action.type = PluginHfActionTypeIso14443Tx; + } + FURI_LOG_D( + TAG, + "Dispatch HF action type=%d len=%u timeout=%lu", + action.type, + action.len, + (unsigned long)action.timeout); + if(!seader->plugin_hf->handle_action(seader->hf_plugin_ctx, &action)) { + FURI_LOG_W(TAG, "HF plugin failed to handle action"); + SeaderWorker* seader_worker = seader_get_active_worker(seader); + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeFail; + } } } else { - FURI_LOG_W(TAG, "unknown frame protocol %lx", frameProtocol); + FURI_LOG_W(TAG, "No HF plugin available for nfcSend"); } } @@ -1502,13 +1588,15 @@ void seader_parse_nfc_off(Seader* seader) { void seader_parse_nfc_command(Seader* seader, NFCCommand_t* nfcCommand, SeaderPollerContainer* spc) { switch(nfcCommand->present) { case NFCCommand_PR_nfcSend: - furi_assert(spc); - seader_parse_nfc_command_transmit(seader, &nfcCommand->choice.nfcSend, spc); + seader_parse_nfc_command_transmit(seader, &nfcCommand->choice.nfcSend); break; case NFCCommand_PR_nfcOff: seader_parse_nfc_off(seader); if(spc != NULL) { - seader->worker->stage = SeaderPollerEventTypeComplete; + SeaderWorker* seader_worker = seader_get_active_worker(seader); + if(seader_worker) { + seader_worker->stage = SeaderPollerEventTypeComplete; + } } break; default: @@ -1585,6 +1673,8 @@ bool seader_process_success_response_i( Payload_t* payload_p = &payload; bool processed = false; + /* Seader wraps each ASN.1 payload with a 6-byte application header + {from, to, replyTo, 0x00, 0x00, 0x00}. Skip that prefix before decoding. */ asn_dec_rval_t rval = asn_decode(0, ATS_DER, &asn_DEF_Payload, (void**)&payload_p, apdu + 6, len - 6); if(rval.code == RC_OK) { @@ -1626,36 +1716,30 @@ NfcCommand seader_worker_card_detect( uint8_t* ats, uint8_t ats_len) { UNUSED(atqa); + furi_check(seader); + furi_check(seader->credential); + furi_check(uid); + furi_check(uid_len > 0U); SeaderCredential* credential = seader->credential; CardDetails_t cardDetails = {0}; + FURI_LOG_D(TAG, "Build card_detect sak=%02x uid_len=%u ats_len=%u", sak, uid_len, ats_len); - OCTET_STRING_fromBuf(&cardDetails.csn, (const char*)uid, uid_len); - OCTET_STRING_t sak_string = {.buf = &sak, .size = 1}; - OCTET_STRING_t ats_string = {.buf = ats, .size = ats_len}; - uint8_t protocol_bytes[] = {0x00, 0x00}; + /* The UID is reused as the current diversifier seed for formats that need one. This is + not universal across all media, but it is the intentional behavior for the cards Seader + currently supports on this read path. */ + size_t diversifier_len = uid_len; + if(diversifier_len > sizeof(credential->diversifier)) { + FURI_LOG_W( + TAG, "Clamp diversifier uid_len=%u to %zu", uid_len, sizeof(credential->diversifier)); + diversifier_len = sizeof(credential->diversifier); + } + memcpy(credential->diversifier, uid, diversifier_len); + credential->diversifier_len = diversifier_len; - // this won't hold true for Seos cards, but then we won't see the SIO from Seos cards anyway - // so it doesn't really matter - memcpy(credential->diversifier, uid, uid_len); - credential->diversifier_len = uid_len; - - if(ats != NULL) { // type 4 - protocol_bytes[1] = FrameProtocol_nfc; - OCTET_STRING_fromBuf( - &cardDetails.protocol, (const char*)protocol_bytes, sizeof(protocol_bytes)); - cardDetails.sak = &sak_string; - // TODO: Update asn1 to change atqa to ats - cardDetails.atsOrAtqbOrAtr = &ats_string; - } else if(uid_len == 8) { // picopass - protocol_bytes[1] = FrameProtocol_iclass; - OCTET_STRING_fromBuf( - &cardDetails.protocol, (const char*)protocol_bytes, sizeof(protocol_bytes)); - } else { // MFC - protocol_bytes[1] = FrameProtocol_nfc; - OCTET_STRING_fromBuf( - &cardDetails.protocol, (const char*)protocol_bytes, sizeof(protocol_bytes)); - cardDetails.sak = &sak_string; + if(!seader_card_details_build(&cardDetails, sak, uid, uid_len, ats, ats_len)) { + FURI_LOG_E(TAG, "Failed to build card details"); + return NfcCommandStop; } seader_sam_set_state( @@ -1663,7 +1747,10 @@ NfcCommand seader_worker_card_detect( SeaderSamStateDetectPending, seader_sam_card_intent(seader), SamCommand_PR_cardDetected); + /* cardDetails must remain valid until the SAM payload is encoded, then it can be released + through the ASN.1-owned reset helper. */ seader_send_card_detected(seader, &cardDetails); + FURI_LOG_D(TAG, "cardDetected sent"); // Print version information for app and firmware for later review in log const Version* version = version_get(); FURI_LOG_I( @@ -1673,6 +1760,6 @@ NfcCommand seader_worker_card_detect( version_get_version(version), FAP_VERSION); - ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_CardDetails, &cardDetails); + seader_card_details_reset(&cardDetails); return NfcCommandContinue; } diff --git a/sam_api.h b/sam_api.h index db6da1a..b96b784 100644 --- a/sam_api.h +++ b/sam_api.h @@ -1,17 +1,20 @@ #pragma once +#ifndef ASN_EMIT_DEBUG +#define ASN_EMIT_DEBUG 0 +#endif + #include #include #include +#include +#include +#include -#include "seader_i.h" -#include "seader_credential.h" -#include "seader_bridge.h" -#include "seader_worker.h" -#include "protocol/rfal_picopass.h" +typedef struct Seader Seader; +typedef struct SeaderPollerContainer SeaderPollerContainer; #include -#include #define ExternalApplicationA 0x44 #define NFCInterface 0x14 @@ -26,6 +29,7 @@ NfcCommand seader_worker_card_detect( uint8_t* ats, uint8_t ats_len); +void seader_send_nfc_rx(Seader* seader, uint8_t* buffer, size_t len); void seader_send_no_card_detected(Seader* seader); bool seader_sam_can_accept_card(const Seader* seader); bool seader_sam_has_active_card(const Seader* seader); diff --git a/scenes/seader_scene_apdu_runner.c b/scenes/seader_scene_apdu_runner.c index e68e0cc..81bb015 100644 --- a/scenes/seader_scene_apdu_runner.c +++ b/scenes/seader_scene_apdu_runner.c @@ -12,6 +12,7 @@ void seader_apdu_runner_worker_callback(uint32_t event, void* context) { void seader_scene_apdu_runner_on_enter(void* context) { Seader* seader = context; + seader_worker_acquire(seader); // Setup view Popup* popup = seader->popup; popup_set_header(popup, "APDU Runner", 68, 30, AlignLeft, AlignTop); @@ -73,4 +74,5 @@ void seader_scene_apdu_runner_on_exit(void* context) { // Clear view popup_reset(seader->popup); + seader_worker_release(seader); } diff --git a/scenes/seader_scene_card_menu.c b/scenes/seader_scene_card_menu.c index 594e58d..f1ef30b 100644 --- a/scenes/seader_scene_card_menu.c +++ b/scenes/seader_scene_card_menu.c @@ -33,7 +33,7 @@ void seader_scene_card_menu_on_enter(void* context) { SubmenuIndexSaveRFID, seader_scene_card_menu_submenu_callback, seader); - if(credential->sio[0] == 0x30 && credential->diversifier_len == RFAL_PICOPASS_UID_LEN) { + if(credential->sio[0] == 0x30 && credential->diversifier_len == PICOPASS_UID_LEN) { submenu_add_item( submenu, "Save SR", @@ -88,8 +88,7 @@ bool seader_scene_card_menu_on_event(void* context, SceneManagerEvent event) { consumed = true; } } else if(event.type == SceneManagerEventTypeBack) { - consumed = scene_manager_search_and_switch_to_previous_scene( - seader->scene_manager, SeaderSceneSamPresent); + consumed = scene_manager_previous_scene(seader->scene_manager); } return consumed; diff --git a/scenes/seader_scene_credential_info.c b/scenes/seader_scene_credential_info.c index d78c9e5..95d9448 100644 --- a/scenes/seader_scene_credential_info.c +++ b/scenes/seader_scene_credential_info.c @@ -1,8 +1,15 @@ #include "../seader_i.h" +#include "../credential_sio_label.h" #include #define TAG "SeaderCredentialInfoScene" +static bool seader_credential_is_picopass_sio_context(const SeaderCredential* credential) { + return credential && (credential->type == SeaderCredentialTypePicopass || + (credential->has_pacs_media_type && + credential->pacs_media_type == SeaderPacsMediaTypePicopass)); +} + void seader_scene_credential_info_widget_callback( GuiButtonType result, InputType type, @@ -16,7 +23,7 @@ void seader_scene_credential_info_widget_callback( void seader_scene_credential_info_on_enter(void* context) { Seader* seader = context; SeaderCredential* credential = seader->credential; - PluginWiegand* plugin = seader->plugin_wiegand; + seader_wiegand_plugin_acquire(seader); Widget* widget = seader->widget; // Use reusable strings instead of allocating new ones @@ -41,16 +48,13 @@ void seader_scene_credential_info_on_enter(void* context) { seader_scene_credential_info_widget_callback, seader); - if(plugin) { - size_t format_count = plugin->count(credential->bit_length, credential->credential); - if(format_count > 0) { - widget_add_button_element( - seader->widget, - GuiButtonTypeCenter, - "Parse", - seader_scene_credential_info_widget_callback, - seader); - } + if(credential->bit_length > 0) { + widget_add_button_element( + seader->widget, + GuiButtonTypeCenter, + "Parse", + seader_scene_credential_info_widget_callback, + seader); } widget_add_string_element( @@ -72,8 +76,13 @@ void seader_scene_credential_info_on_enter(void* context) { FontSecondary, furi_string_get_cstr(credential_str)); - if(credential->sio[0] == 0x30) { - furi_string_set(sio_str, "+SIO"); + if(seader_sio_label_format( + credential->sio[0] == 0x30, + seader_credential_is_picopass_sio_context(credential), + credential->sio_start_block, + seader->text_store, + sizeof(seader->text_store))) { + furi_string_set(sio_str, seader->text_store); widget_add_string_element( widget, 64, 48, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(sio_str)); } @@ -109,4 +118,5 @@ void seader_scene_credential_info_on_exit(void* context) { // Clear views widget_reset(seader->widget); + seader_wiegand_plugin_release(seader); } diff --git a/scenes/seader_scene_formats.c b/scenes/seader_scene_formats.c index a2f0508..7b0dbf3 100644 --- a/scenes/seader_scene_formats.c +++ b/scenes/seader_scene_formats.c @@ -3,7 +3,7 @@ void seader_scene_formats_on_enter(void* context) { Seader* seader = context; - PluginWiegand* plugin = seader->plugin_wiegand; + PluginWiegand* plugin = seader_wiegand_plugin_acquire(seader) ? seader->plugin_wiegand : NULL; SeaderCredential* credential = seader->credential; FuriString* str = seader->text_box_store; @@ -19,7 +19,12 @@ void seader_scene_formats_on_enter(void* context) { furi_string_cat_printf(str, "%s\n", furi_string_get_cstr(description)); } + if(format_count == 0) { + furi_string_set_str(str, "No known Wiegand formats matched."); + } // No need to free description as it's reused from seader struct + } else { + furi_string_set_str(str, "Wiegand parser unavailable."); } text_box_set_font(seader->text_box, TextBoxFontHex); @@ -46,4 +51,5 @@ void seader_scene_formats_on_exit(void* context) { // Clear views text_box_reset(seader->text_box); + seader_wiegand_plugin_release(seader); } diff --git a/scenes/seader_scene_read.c b/scenes/seader_scene_read.c index 0994f2f..dbe7741 100644 --- a/scenes/seader_scene_read.c +++ b/scenes/seader_scene_read.c @@ -4,6 +4,8 @@ void seader_scene_read_on_enter(void* context) { Seader* seader = context; + seader_hf_mode_activate(seader); + seader_worker_acquire(seader); dolphin_deed(DolphinDeedNfcRead); // Setup view @@ -16,9 +18,8 @@ void seader_scene_read_on_enter(void* context) { seader_scene_read_prepare(seader); seader_credential_clear(seader->credential); - if(seader->selected_read_type == SeaderCredentialTypeNone) { - seader->detected_card_type_count = 0; - memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types)); + if(seader_hf_mode_get_selected_read_type(seader) == SeaderCredentialTypeNone) { + seader_hf_mode_clear_detected_types(seader); } seader_worker_start( seader->worker, @@ -38,6 +39,11 @@ bool seader_scene_read_on_event(void* context, SceneManagerEvent event) { if(event.event == SeaderCustomEventWorkerExit) { scene_manager_next_scene(seader->scene_manager, SeaderSceneReadCardSuccess); consumed = true; + } else if(event.event == SeaderWorkerEventFail) { + scene_manager_next_scene(seader->scene_manager, SeaderSceneReadCardSuccess); + consumed = true; + } else if(event.event == SeaderWorkerEventHfTeardownComplete) { + consumed = seader_hf_finish_teardown_action(seader); } else if(event.event == SeaderCustomEventPollerDetect) { Popup* popup = seader->popup; popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop); @@ -50,12 +56,8 @@ bool seader_scene_read_on_event(void* context, SceneManagerEvent event) { consumed = true; } } else if(event.type == SceneManagerEventTypeBack) { - seader->selected_read_type = SeaderCredentialTypeNone; - seader->detected_card_type_count = 0; - memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types)); - scene_manager_search_and_switch_to_previous_scene( - seader->scene_manager, SeaderSceneSamPresent); - consumed = true; + seader_scene_read_abort_cleanup(seader); + consumed = seader_hf_request_teardown(seader, SeaderHfTeardownActionSamPresent); } return consumed; @@ -63,6 +65,6 @@ bool seader_scene_read_on_event(void* context, SceneManagerEvent event) { void seader_scene_read_on_exit(void* context) { Seader* seader = context; - seader_worker_stop(seader->worker); - seader_scene_read_cleanup(seader); + seader_scene_read_finish_cleanup(seader); + seader_worker_release(seader); } diff --git a/scenes/seader_scene_read_card_success.c b/scenes/seader_scene_read_card_success.c index 275f589..b58b9ca 100644 --- a/scenes/seader_scene_read_card_success.c +++ b/scenes/seader_scene_read_card_success.c @@ -1,8 +1,15 @@ #include "../seader_i.h" +#include "../credential_sio_label.h" #include #define TAG "SeaderSceneReadCardSuccess" +static bool seader_credential_is_picopass_sio_context(const SeaderCredential* credential) { + return credential && (credential->type == SeaderCredentialTypePicopass || + (credential->has_pacs_media_type && + credential->pacs_media_type == SeaderPacsMediaTypePicopass)); +} + void seader_scene_read_card_success_widget_callback( GuiButtonType result, InputType type, @@ -18,11 +25,8 @@ void seader_scene_read_card_success_widget_callback( void seader_scene_read_card_success_on_enter(void* context) { Seader* seader = context; SeaderCredential* credential = seader->credential; - PluginWiegand* plugin = seader->plugin_wiegand; + PluginWiegand* plugin = seader_wiegand_plugin_acquire(seader) ? seader->plugin_wiegand : NULL; Widget* widget = seader->widget; - seader->selected_read_type = SeaderCredentialTypeNone; - seader->detected_card_type_count = 0; - memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types)); // Use reusable strings instead of allocating new ones FuriString* type_str = seader->temp_string1; @@ -46,10 +50,8 @@ void seader_scene_read_card_success_on_enter(void* context) { furi_string_set(type_str, "Read error"); furi_string_set(bitlength_str, seader->read_error[0] ? seader->read_error : "Read failed"); - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; seader_t_1_reset(seader->uart); - seader_ccid_check_for_sam(seader_uart); + seader_ccid_check_for_sam(seader->uart); } widget_add_button_element( @@ -71,22 +73,25 @@ void seader_scene_read_card_success_on_enter(void* context) { seader); } - if(plugin && credential->bit_length > 0) { - size_t format_count = plugin->count(credential->bit_length, credential->credential); - FURI_LOG_D( - TAG, - "Plugin present, bit_length=%d, format_count=%zu", - credential->bit_length, - format_count); - if(format_count > 0) { - widget_add_button_element( - seader->widget, - GuiButtonTypeCenter, - "Parse", - seader_scene_read_card_success_widget_callback, - seader); + if(credential->bit_length > 0) { + if(plugin) { + size_t format_count = plugin->count(credential->bit_length, credential->credential); + FURI_LOG_D( + TAG, + "Plugin present, bit_length=%d, format_count=%zu", + credential->bit_length, + format_count); + } else { + FURI_LOG_D( + TAG, "Parse available without plugin bit_length=%d", credential->bit_length); } - } else { + widget_add_button_element( + seader->widget, + GuiButtonTypeCenter, + "Parse", + seader_scene_read_card_success_widget_callback, + seader); + } else if(!plugin) { FURI_LOG_D(TAG, "Plugin=%p, bit_length=%d", plugin, credential->bit_length); } @@ -108,19 +113,16 @@ void seader_scene_read_card_success_on_enter(void* context) { AlignCenter, FontSecondary, furi_string_get_cstr(credential_str)); - if(credential->sio[0] == 0x30) { - switch(credential->sio_start_block) { - case 6: - furi_string_set(sio_str, "+SIO(SE)"); - break; - case 10: - furi_string_set(sio_str, "+SIO(SR)"); - break; - default: + if(seader_sio_label_format( + credential->sio[0] == 0x30, + seader_credential_is_picopass_sio_context(credential), + credential->sio_start_block, + seader->text_store, + sizeof(seader->text_store))) { + if(strcmp(seader->text_store, "+SIO(?)") == 0) { FURI_LOG_E(TAG, "Unknown SIO start block: %d", credential->sio_start_block); - furi_string_set(sio_str, "+SIO(?)"); - break; } + furi_string_set(sio_str, seader->text_store); widget_add_string_element( widget, 64, 48, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(sio_str)); } @@ -136,23 +138,24 @@ bool seader_scene_read_card_success_on_event(void* context, SceneManagerEvent ev if(event.type == SceneManagerEventTypeCustom) { if(event.event == GuiButtonTypeLeft) { - consumed = scene_manager_previous_scene(seader->scene_manager); + consumed = seader_hf_request_teardown(seader, SeaderHfTeardownActionRestartRead); } else if(event.event == GuiButtonTypeRight) { if(seader->credential->bit_length > 0) { scene_manager_next_scene(seader->scene_manager, SeaderSceneCardMenu); } else { - scene_manager_search_and_switch_to_previous_scene( - seader->scene_manager, SeaderSceneSamPresent); + consumed = seader_hf_request_teardown(seader, SeaderHfTeardownActionSamPresent); + } + if(seader->credential->bit_length > 0) { + consumed = true; } - consumed = true; } else if(event.event == GuiButtonTypeCenter) { scene_manager_next_scene(seader->scene_manager, SeaderSceneFormats); consumed = true; + } else if(event.event == SeaderWorkerEventHfTeardownComplete) { + consumed = seader_hf_finish_teardown_action(seader); } } else if(event.type == SceneManagerEventTypeBack) { - scene_manager_search_and_switch_to_previous_scene( - seader->scene_manager, SeaderSceneSamPresent); - consumed = true; + consumed = seader_hf_request_teardown(seader, SeaderHfTeardownActionSamPresent); } return consumed; } @@ -162,4 +165,5 @@ void seader_scene_read_card_success_on_exit(void* context) { // Clear view widget_reset(seader->widget); + seader_wiegand_plugin_release(seader); } diff --git a/scenes/seader_scene_read_card_type.c b/scenes/seader_scene_read_card_type.c index ef9bd0f..b514f5c 100644 --- a/scenes/seader_scene_read_card_type.c +++ b/scenes/seader_scene_read_card_type.c @@ -21,10 +21,12 @@ void seader_scene_read_card_type_submenu_callback(void* context, uint32_t index) void seader_scene_read_card_type_on_enter(void* context) { Seader* seader = context; Submenu* submenu = seader->submenu; + const SeaderCredentialType* detected_types = seader_hf_mode_get_detected_types(seader); + const size_t detected_type_count = seader_hf_mode_get_detected_type_count(seader); submenu_reset(submenu); - for(size_t i = 0; i < seader->detected_card_type_count; i++) { - const SeaderCredentialType type = seader->detected_card_types[i]; + for(size_t i = 0; i < detected_type_count; i++) { + const SeaderCredentialType type = detected_types[i]; submenu_add_item( submenu, seader_scene_read_card_type_label(type), @@ -42,21 +44,18 @@ bool seader_scene_read_card_type_on_event(void* context, SceneManagerEvent event if(event.type == SceneManagerEventTypeCustom) { const SeaderCredentialType type = event.event; - if(type == SeaderCredentialType14A || type == SeaderCredentialTypeMifareClassic || - type == SeaderCredentialTypePicopass) { - seader->selected_read_type = type; - seader->detected_card_type_count = 0; - memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types)); + if(event.event == SeaderWorkerEventHfTeardownComplete) { + consumed = seader_hf_finish_teardown_action(seader); + } else if( + type == SeaderCredentialType14A || type == SeaderCredentialTypeMifareClassic || + type == SeaderCredentialTypePicopass) { + seader_hf_mode_set_selected_read_type(seader, type); + seader_hf_mode_clear_detected_types(seader); scene_manager_next_scene(seader->scene_manager, SeaderSceneRead); consumed = true; } } else if(event.type == SceneManagerEventTypeBack) { - seader->selected_read_type = SeaderCredentialTypeNone; - seader->detected_card_type_count = 0; - memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types)); - scene_manager_search_and_switch_to_previous_scene( - seader->scene_manager, SeaderSceneSamPresent); - consumed = true; + consumed = seader_hf_request_teardown(seader, SeaderHfTeardownActionSamPresent); } return consumed; diff --git a/scenes/seader_scene_read_common.c b/scenes/seader_scene_read_common.c index 0341d3a..db35e97 100644 --- a/scenes/seader_scene_read_common.c +++ b/scenes/seader_scene_read_common.c @@ -21,7 +21,6 @@ void seader_scene_read_prepare(Seader* seader) { seader->samCommand = SamCommand_PR_NOTHING; } memset(seader->read_error, 0, sizeof(seader->read_error)); - seader_worker_reset_poller_session(seader->worker); } void seader_scene_read_cleanup(Seader* seader) { @@ -33,14 +32,28 @@ void seader_scene_read_cleanup(Seader* seader) { seader->samCommand, seader->sam_state, seader->sam_intent); - seader_worker_cancel_poller_session(seader->worker); + seader_scene_read_abort_cleanup(seader); + seader_scene_read_finish_cleanup(seader); +} + +void seader_scene_read_abort_cleanup(Seader* seader) { + furi_assert(seader); + FURI_LOG_D("SceneRead", "Abort cleanup session sam=%d", seader->samCommand); if(seader_sam_has_active_card(seader)) { seader_send_no_card_detected(seader); } popup_reset(seader->popup); - seader_worker_reset_poller_session(seader->worker); + if(seader->sam_state == SeaderSamStateIdle) { + seader->samCommand = SamCommand_PR_NOTHING; + } + seader_blink_stop(seader); +} + +void seader_scene_read_finish_cleanup(Seader* seader) { + furi_assert(seader); + popup_reset(seader->popup); if(seader->sam_state == SeaderSamStateIdle) { seader->samCommand = SamCommand_PR_NOTHING; } diff --git a/scenes/seader_scene_read_common.h b/scenes/seader_scene_read_common.h index d7f17b9..580f178 100644 --- a/scenes/seader_scene_read_common.h +++ b/scenes/seader_scene_read_common.h @@ -6,4 +6,6 @@ typedef struct Seader Seader; void seader_sam_check_worker_callback(uint32_t event, void* context); void seader_scene_read_prepare(Seader* seader); +void seader_scene_read_abort_cleanup(Seader* seader); +void seader_scene_read_finish_cleanup(Seader* seader); void seader_scene_read_cleanup(Seader* seader); diff --git a/scenes/seader_scene_read_config_card.c b/scenes/seader_scene_read_config_card.c index 8687019..55da4e7 100644 --- a/scenes/seader_scene_read_config_card.c +++ b/scenes/seader_scene_read_config_card.c @@ -10,6 +10,7 @@ void seader_read_config_card_worker_callback(uint32_t event, void* context) { void seader_scene_read_config_card_on_enter(void* context) { Seader* seader = context; + seader_worker_acquire(seader); // Setup view Popup* popup = seader->popup; @@ -41,6 +42,10 @@ bool seader_scene_read_config_card_on_event(void* context, SceneManagerEvent eve if(event.event == SeaderCustomEventWorkerExit || event.event == SeaderWorkerEventSuccess) { scene_manager_next_scene(seader->scene_manager, SeaderSceneReadConfigCardSuccess); consumed = true; + } else if(event.event == SeaderWorkerEventFail) { + scene_manager_search_and_switch_to_previous_scene( + seader->scene_manager, SeaderSceneSamPresent); + consumed = true; } } else if(event.type == SceneManagerEventTypeBack) { scene_manager_search_and_switch_to_previous_scene( @@ -53,6 +58,9 @@ bool seader_scene_read_config_card_on_event(void* context, SceneManagerEvent eve void seader_scene_read_config_card_on_exit(void* context) { Seader* seader = context; - seader_worker_stop(seader->worker); + if(seader->worker) { + seader_worker_stop(seader->worker); + } seader_scene_read_cleanup(seader); + seader_worker_release(seader); } diff --git a/scenes/seader_scene_sam_info.c b/scenes/seader_scene_sam_info.c index 357d25c..6b14dcc 100644 --- a/scenes/seader_scene_sam_info.c +++ b/scenes/seader_scene_sam_info.c @@ -12,7 +12,6 @@ void seader_scene_sam_info_widget_callback(GuiButtonType result, InputType type, void seader_scene_sam_info_on_enter(void* context) { Seader* seader = context; - SeaderWorker* seader_worker = seader->worker; Widget* widget = seader->widget; // Use reusable string instead of allocating new one @@ -24,8 +23,7 @@ void seader_scene_sam_info_on_enter(void* context) { furi_string_reset(info_str); furi_string_reset(uhf_str); - furi_string_cat_printf( - fw_str, "FW %d.%d", seader_worker->sam_version[0], seader_worker->sam_version[1]); + furi_string_cat_printf(fw_str, "FW %d.%d", seader->sam_version[0], seader->sam_version[1]); furi_string_set_str(info_str, seader->sam_key_label); furi_string_set_str(uhf_str, seader->uhf_status_label); diff --git a/scenes/seader_scene_sam_missing.c b/scenes/seader_scene_sam_missing.c index ef00cc6..c75cf9e 100644 --- a/scenes/seader_scene_sam_missing.c +++ b/scenes/seader_scene_sam_missing.c @@ -42,6 +42,7 @@ bool seader_scene_sam_missing_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(seader->scene_manager, SeaderSceneFileSelect); consumed = true; } else if(event.event == SeaderWorkerEventSamPresent) { + seader->sam_present_menu_guard_active = true; scene_manager_next_scene(seader->scene_manager, SeaderSceneSamPresent); consumed = true; } diff --git a/scenes/seader_scene_sam_present.c b/scenes/seader_scene_sam_present.c index c9c3f74..3473d01 100644 --- a/scenes/seader_scene_sam_present.c +++ b/scenes/seader_scene_sam_present.c @@ -1,23 +1,17 @@ #include "../seader_i.h" enum SubmenuIndex { - SubmenuIndexSamInfo, SubmenuIndexRead, SubmenuIndexSaved, SubmenuIndexAPDURunner, SubmenuIndexReadConfigCard, + SubmenuIndexSamInfo, }; static uint8_t fwChecks = 3; -void seader_scene_sam_present_submenu_callback(void* context, uint32_t index) { - Seader* seader = context; - view_dispatcher_send_custom_event(seader->view_dispatcher, index); -} - -void seader_scene_sam_present_on_update(void* context) { - Seader* seader = context; - SeaderWorker* seader_worker = seader->worker; +void seader_scene_sam_present_submenu_callback(void* context, uint32_t index); +static void seader_scene_sam_present_rebuild_menu(Seader* seader, uint32_t selected_item) { Submenu* submenu = seader->submenu; submenu_reset(submenu); @@ -43,29 +37,32 @@ void seader_scene_sam_present_on_update(void* context) { seader_scene_sam_present_submenu_callback, seader); } - if(seader_worker->sam_version[0] != 0 && seader_worker->sam_version[1] != 0) { - submenu_add_item( - submenu, - seader->sam_key_label, - SubmenuIndexSamInfo, - seader_scene_sam_present_submenu_callback, - seader); + submenu_add_item( + submenu, + seader->sam_key_label, + SubmenuIndexSamInfo, + seader_scene_sam_present_submenu_callback, + seader); + + if(seader->sam_version[0] != 0 && seader->sam_version[1] != 0) { fwChecks = 0; - } else { - submenu_add_item( - submenu, - seader->sam_key_label, - SubmenuIndexSamInfo, - seader_scene_sam_present_submenu_callback, - seader); } - submenu_set_selected_item( - submenu, scene_manager_get_scene_state(seader->scene_manager, SeaderSceneSamPresent)); - + submenu_set_selected_item(submenu, selected_item); view_dispatcher_switch_to_view(seader->view_dispatcher, SeaderViewMenu); } +void seader_scene_sam_present_submenu_callback(void* context, uint32_t index) { + Seader* seader = context; + view_dispatcher_send_custom_event(seader->view_dispatcher, index); +} + +void seader_scene_sam_present_on_update(void* context) { + Seader* seader = context; + seader_scene_sam_present_rebuild_menu( + seader, scene_manager_get_scene_state(seader->scene_manager, SeaderSceneSamPresent)); +} + void seader_scene_sam_present_on_enter(void* context) { seader_scene_sam_present_on_update(context); } @@ -75,7 +72,13 @@ bool seader_scene_sam_present_on_event(void* context, SceneManagerEvent event) { bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { - if(event.event == SubmenuIndexRead) { + if(seader->sam_present_menu_guard_active && + (event.event == SubmenuIndexRead || event.event == SubmenuIndexSaved || + event.event == SubmenuIndexAPDURunner || event.event == SubmenuIndexReadConfigCard || + event.event == SubmenuIndexSamInfo)) { + seader->sam_present_menu_guard_active = false; + consumed = true; + } else if(event.event == SubmenuIndexRead) { scene_manager_set_scene_state( seader->scene_manager, SeaderSceneSamPresent, event.event); scene_manager_next_scene(seader->scene_manager, SeaderSceneRead); @@ -103,20 +106,23 @@ bool seader_scene_sam_present_on_event(void* context, SceneManagerEvent event) { seader->scene_manager, SeaderSceneSamPresent, event.event); scene_manager_next_scene(seader->scene_manager, SeaderSceneAPDURunner); consumed = true; + } else if(event.event == SeaderWorkerEventHfTeardownComplete) { + consumed = seader_hf_finish_teardown_action(seader); } else if(event.event == SeaderCustomEventSamStatusUpdated) { - seader_scene_sam_present_on_update(context); + seader_scene_sam_present_rebuild_menu( + seader, submenu_get_selected_item(seader->submenu)); consumed = true; } } else if(event.type == SceneManagerEventTypeBack) { - scene_manager_stop(seader->scene_manager); - view_dispatcher_stop(seader->view_dispatcher); - consumed = true; + consumed = seader_hf_request_teardown(seader, SeaderHfTeardownActionStopApp); } else if(event.type == SceneManagerEventTypeTick) { - SeaderWorker* seader_worker = seader->worker; - if(fwChecks > 0 && seader_worker->sam_version[0] != 0 && - seader_worker->sam_version[1] != 0) { + if(seader->sam_present_menu_guard_active) { + seader->sam_present_menu_guard_active = false; + } + if(fwChecks > 0 && seader->sam_version[0] != 0 && seader->sam_version[1] != 0) { fwChecks--; - seader_scene_sam_present_on_update(context); + seader_scene_sam_present_rebuild_menu( + seader, submenu_get_selected_item(seader->submenu)); } } diff --git a/scenes/seader_scene_start.c b/scenes/seader_scene_start.c index 3886c3c..7b6c520 100644 --- a/scenes/seader_scene_start.c +++ b/scenes/seader_scene_start.c @@ -8,6 +8,9 @@ enum SubmenuIndex { static void seader_scene_start_detect_callback(void* context) { Seader* seader = context; + if(!seader || !seader->start_scene_active) { + return; + } view_dispatcher_send_custom_event(seader->view_dispatcher, SeaderWorkerEventSamMissing); } @@ -18,6 +21,8 @@ void seader_scene_start_submenu_callback(void* context, uint32_t index) { void seader_scene_start_on_enter(void* context) { Seader* seader = context; + seader_worker_acquire(seader); + seader->start_scene_active = true; Popup* popup = seader->popup; @@ -43,6 +48,7 @@ bool seader_scene_start_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == SeaderWorkerEventSamPresent) { + seader->sam_present_menu_guard_active = true; scene_manager_next_scene(seader->scene_manager, SeaderSceneSamPresent); consumed = true; } else if(event.event == SeaderWorkerEventSamMissing) { @@ -67,5 +73,7 @@ bool seader_scene_start_on_event(void* context, SceneManagerEvent event) { void seader_scene_start_on_exit(void* context) { Seader* seader = context; + seader->start_scene_active = false; popup_reset(seader->popup); + seader_worker_release(seader); } diff --git a/scenes/seader_scene_virtual_credential.c b/scenes/seader_scene_virtual_credential.c index 2d72861..0bef787 100644 --- a/scenes/seader_scene_virtual_credential.c +++ b/scenes/seader_scene_virtual_credential.c @@ -8,6 +8,7 @@ void seader_virtual_credential_worker_callback(uint32_t event, void* context) { void seader_scene_virtual_credential_on_enter(void* context) { Seader* seader = context; + seader_worker_acquire(seader); // Setup view Popup* popup = seader->popup; @@ -52,4 +53,5 @@ void seader_scene_virtual_credential_on_exit(void* context) { // Clear view popup_reset(seader->popup); + seader_worker_release(seader); } diff --git a/seader.c b/seader.c index e6f5fb8..4b5cc8f 100644 --- a/seader.c +++ b/seader.c @@ -1,7 +1,435 @@ #include "seader_i.h" +#include "runtime_policy.h" +#include "hf_release_sequence.h" #include "trace_log.h" -#define TAG "Seader" +#define TAG "Seader" +#define SEADER_PLUGIN_DIR APP_ASSETS_PATH("plugins") +#define SEADER_WIEGAND_PLUGIN_PATH APP_ASSETS_PATH("plugins/plugin_wiegand.fal") +#define SEADER_HF_PLUGIN_PATH APP_ASSETS_PATH("plugins/plugin_hf.fal") + +typedef struct { + volatile bool done; + volatile bool detected; +} SeaderHfPicopassDetectContext; + +static void seader_hf_worker_event_callback(uint32_t event, void* context); +static void seader_hf_teardown_blocking(Seader* seader); + +static NfcCommand seader_hf_picopass_detect_callback(PicopassPollerEvent event, void* context) { + SeaderHfPicopassDetectContext* detect_context = context; + + if(event.type == PicopassPollerEventTypeCardDetected || + event.type == PicopassPollerEventTypeSuccess) { + detect_context->detected = true; + detect_context->done = true; + return NfcCommandStop; + } else if(event.type == PicopassPollerEventTypeFail) { + detect_context->done = true; + return NfcCommandStop; + } + + return NfcCommandContinue; +} + +static void seader_hf_plugin_notify_event(void* host_ctx, uint32_t event) { + Seader* seader = host_ctx; + if(!seader || !seader->view_dispatcher) { + FURI_LOG_W(TAG, "Drop HF plugin event %lu without dispatcher", event); + return; + } + view_dispatcher_send_custom_event(seader->view_dispatcher, event); +} + +static void seader_hf_plugin_notify_card_detected(void* host_ctx) { + seader_hf_plugin_notify_event(host_ctx, SeaderCustomEventPollerDetect); +} + +static void seader_hf_plugin_notify_worker_exit(void* host_ctx) { + seader_hf_plugin_notify_event(host_ctx, SeaderCustomEventWorkerExit); +} + +static bool seader_hf_plugin_sam_can_accept_card(void* host_ctx) { + return seader_sam_can_accept_card(host_ctx); +} + +static void seader_hf_plugin_send_card_detected( + void* host_ctx, + uint8_t sak, + const uint8_t* uid, + uint8_t uid_len, + const uint8_t* ats, + uint8_t ats_len) { + Seader* seader = host_ctx; + if(!seader || !seader->worker || !seader->credential || !uid || uid_len == 0U) { + FURI_LOG_E( + TAG, + "Drop HF cardDetected invalid state seader=%p worker=%p cred=%p uid=%p uid_len=%u", + (void*)seader, + seader ? (void*)seader->worker : NULL, + seader ? (void*)seader->credential : NULL, + (const void*)uid, + uid_len); + return; + } + FURI_LOG_D( + TAG, + "HF plugin cardDetected sak=%02x uid_len=%u ats_len=%u stage=%d", + sak, + uid_len, + ats_len, + seader->worker->stage); + seader_worker_card_detect(seader, sak, NULL, uid, uid_len, (uint8_t*)ats, ats_len); +} + +static void seader_hf_plugin_send_nfc_rx(void* host_ctx, uint8_t* buffer, size_t len) { + Seader* seader = host_ctx; + seader_send_nfc_rx(seader, buffer, len); +} + +static void seader_hf_plugin_run_conversation(void* host_ctx) { + Seader* seader = host_ctx; + if(!seader || !seader->worker) { + FURI_LOG_W(TAG, "Skip HF conversation without worker"); + return; + } + FURI_LOG_D(TAG, "HF plugin run conversation stage=%d", seader->worker->stage); + seader_worker_run_hf_conversation(seader); +} + +static void seader_hf_plugin_set_stage(void* host_ctx, PluginHfStage stage) { + Seader* seader = host_ctx; + if(seader->worker) { + switch(stage) { + case PluginHfStageCardDetect: + seader->worker->stage = SeaderPollerEventTypeCardDetect; + break; + case PluginHfStageConversation: + seader->worker->stage = SeaderPollerEventTypeConversation; + break; + case PluginHfStageComplete: + seader->worker->stage = SeaderPollerEventTypeComplete; + break; + case PluginHfStageSuccess: + seader->worker->stage = SeaderPollerEventTypeSuccess; + break; + case PluginHfStageFail: + default: + seader->worker->stage = SeaderPollerEventTypeFail; + break; + } + } +} + +static PluginHfStage seader_hf_plugin_get_stage(void* host_ctx) { + Seader* seader = host_ctx; + if(!seader->worker) { + return PluginHfStageFail; + } + + switch(seader->worker->stage) { + case SeaderPollerEventTypeCardDetect: + return PluginHfStageCardDetect; + case SeaderPollerEventTypeConversation: + return PluginHfStageConversation; + case SeaderPollerEventTypeComplete: + return PluginHfStageComplete; + case SeaderPollerEventTypeSuccess: + return PluginHfStageSuccess; + case SeaderPollerEventTypeFail: + default: + return PluginHfStageFail; + } +} + +static void seader_hf_plugin_set_credential_type(void* host_ctx, SeaderCredentialType type) { + Seader* seader = host_ctx; + seader->credential->type = type; + seader->credential->sio_len = 0U; + seader->credential->sio_start_block = 0U; + seader->credential->isDesfireEV2 = false; +} + +static SeaderCredentialType seader_hf_plugin_get_credential_type(void* host_ctx) { + Seader* seader = host_ctx; + return seader->credential->type; +} + +static bool seader_hf_plugin_get_desfire_ev2(void* host_ctx) { + Seader* seader = host_ctx; + return seader->credential->isDesfireEV2; +} + +static void seader_hf_plugin_set_desfire_ev2(void* host_ctx, bool is_desfire_ev2) { + Seader* seader = host_ctx; + seader->credential->isDesfireEV2 = is_desfire_ev2; +} + +static void seader_hf_plugin_append_picopass_sio( + void* host_ctx, + uint8_t block_num, + const uint8_t* data, + size_t len) { + Seader* seader = host_ctx; + SeaderCredential* credential = seader->credential; + + if(!data || len == 0U || credential->type != SeaderCredentialTypePicopass) { + return; + } + + if(credential->sio_len == 0U && data[0] == 0x30U) { + credential->sio_start_block = block_num; + } + + const size_t offset = (size_t)(block_num - credential->sio_start_block) * PICOPASS_BLOCK_LEN; + if(offset >= sizeof(credential->sio)) { + return; + } + + const size_t copy_len = MIN(len, sizeof(credential->sio) - offset); + memcpy(credential->sio + offset, data, copy_len); + credential->sio_len = MAX(credential->sio_len, offset + copy_len); +} + +static void seader_hf_plugin_set_14a_sio(void* host_ctx, const uint8_t* data, size_t len) { + Seader* seader = host_ctx; + SeaderCredential* credential = seader->credential; + + if(!data || credential->type != SeaderCredentialType14A) { + return; + } + + const size_t copy_len = MIN(len, sizeof(credential->sio)); + memcpy(credential->sio, data, copy_len); + credential->sio_len = copy_len; +} + +static Nfc* seader_hf_plugin_get_nfc(void* host_ctx) { + Seader* seader = host_ctx; + return seader ? seader->nfc : NULL; +} + +static NfcDevice* seader_hf_plugin_get_nfc_device(void* host_ctx) { + Seader* seader = host_ctx; + return seader ? seader->nfc_device : NULL; +} + +static bool seader_hf_plugin_picopass_detect(void* host_ctx) { + Seader* seader = host_ctx; + bool detected = false; + PicopassPoller* poller = picopass_poller_alloc(seader->nfc); + SeaderHfPicopassDetectContext detect_context = {0}; + + if(!poller) { + FURI_LOG_W(TAG, "Failed to allocate Picopass detect poller"); + return false; + } + + picopass_poller_start(poller, seader_hf_picopass_detect_callback, &detect_context); + for(uint8_t i = 0; i < 10 && !detect_context.done; i++) { + furi_delay_ms(10); + } + + picopass_poller_stop(poller); + detected = detect_context.detected; + picopass_poller_free(poller); + + return detected; +} + +static bool seader_hf_plugin_picopass_start( + void* host_ctx, + PicopassPollerCallback callback, + void* callback_ctx) { + Seader* seader = host_ctx; + + if(seader->picopass_poller) { + picopass_poller_stop(seader->picopass_poller); + picopass_poller_free(seader->picopass_poller); + seader->picopass_poller = NULL; + } + + seader->picopass_poller = picopass_poller_alloc(seader->nfc); + if(!seader->picopass_poller) { + return false; + } + + picopass_poller_start(seader->picopass_poller, callback, callback_ctx); + return true; +} + +static void seader_hf_plugin_picopass_stop(void* host_ctx) { + Seader* seader = host_ctx; + + if(seader->picopass_poller) { + picopass_poller_stop(seader->picopass_poller); + picopass_poller_free(seader->picopass_poller); + seader->picopass_poller = NULL; + } +} + +static uint8_t* seader_hf_plugin_picopass_get_csn(void* host_ctx) { + Seader* seader = host_ctx; + if(!seader->picopass_poller) { + return NULL; + } + + return picopass_poller_get_csn(seader->picopass_poller); +} + +static bool seader_hf_plugin_picopass_transmit( + void* host_ctx, + const uint8_t* tx_data, + size_t tx_len, + uint8_t* rx_data, + size_t rx_capacity, + size_t* rx_len, + uint32_t fwt_fc) { + Seader* seader = host_ctx; + if(!seader->picopass_poller || !tx_data || !rx_data || !rx_len) { + return false; + } + + BitBuffer* tx_buffer = bit_buffer_alloc(tx_len); + BitBuffer* rx_buffer = bit_buffer_alloc(rx_capacity); + bool success = false; + if(!tx_buffer || !rx_buffer) { + FURI_LOG_E(TAG, "Failed to allocate picopass host tx/rx buffers"); + if(tx_buffer) bit_buffer_free(tx_buffer); + if(rx_buffer) bit_buffer_free(rx_buffer); + return false; + } + + bit_buffer_append_bytes(tx_buffer, tx_data, tx_len); + PicopassError error = + picopass_poller_send_frame(seader->picopass_poller, tx_buffer, rx_buffer, fwt_fc); + if(error == PicopassErrorIncorrectCrc) { + error = PicopassErrorNone; + } + + if(error == PicopassErrorNone) { + *rx_len = bit_buffer_get_size_bytes(rx_buffer); + memcpy(rx_data, bit_buffer_get_data(rx_buffer), *rx_len); + success = true; + } + + bit_buffer_free(tx_buffer); + bit_buffer_free(rx_buffer); + return success; +} + +static void seader_hf_plugin_set_read_error(void* host_ctx, const char* text) { + Seader* seader = host_ctx; + if(!text) { + seader->read_error[0] = '\0'; + return; + } + strlcpy(seader->read_error, text, sizeof(seader->read_error)); +} + +static const PluginHfHostApi seader_hf_plugin_host_api = { + .notify_card_detected = seader_hf_plugin_notify_card_detected, + .notify_worker_exit = seader_hf_plugin_notify_worker_exit, + .sam_can_accept_card = seader_hf_plugin_sam_can_accept_card, + .send_card_detected = seader_hf_plugin_send_card_detected, + .send_nfc_rx = seader_hf_plugin_send_nfc_rx, + .run_conversation = seader_hf_plugin_run_conversation, + .set_stage = seader_hf_plugin_set_stage, + .get_stage = seader_hf_plugin_get_stage, + .set_credential_type = seader_hf_plugin_set_credential_type, + .get_credential_type = seader_hf_plugin_get_credential_type, + .get_desfire_ev2 = seader_hf_plugin_get_desfire_ev2, + .set_desfire_ev2 = seader_hf_plugin_set_desfire_ev2, + .append_picopass_sio = seader_hf_plugin_append_picopass_sio, + .set_14a_sio = seader_hf_plugin_set_14a_sio, + .get_nfc = seader_hf_plugin_get_nfc, + .get_nfc_device = seader_hf_plugin_get_nfc_device, + .picopass_detect = seader_hf_plugin_picopass_detect, + .picopass_start = seader_hf_plugin_picopass_start, + .picopass_stop = seader_hf_plugin_picopass_stop, + .picopass_get_csn = seader_hf_plugin_picopass_get_csn, + .picopass_transmit = seader_hf_plugin_picopass_transmit, + .set_read_error = seader_hf_plugin_set_read_error, +}; + +static void seader_hf_worker_event_callback(uint32_t event, void* context) { + Seader* seader = context; + if(!seader || !seader->view_dispatcher) { + return; + } + + view_dispatcher_send_custom_event(seader->view_dispatcher, event); +} + +static void seader_hf_session_force_unloaded(Seader* seader) { + if(!seader) { + return; + } + + seader->hf_plugin_ctx = NULL; + seader->plugin_hf = NULL; + seader->hf_plugin_manager = NULL; + seader->poller = NULL; + seader->picopass_poller = NULL; + seader->hf_session_state = SeaderHfSessionStateUnloaded; + if(seader->mode_runtime == SeaderModeRuntimeHF) { + seader->mode_runtime = SeaderModeRuntimeNone; + } +} + +static void seader_hf_release_plugin_stop(void* context) { + Seader* seader = context; + if(seader && seader->plugin_hf && seader->hf_plugin_ctx) { + seader->plugin_hf->stop(seader->hf_plugin_ctx); + } +} + +static void seader_hf_release_host_poller(void* context) { + Seader* seader = context; + if(seader && seader->poller) { + FURI_LOG_I(TAG, "Stopping host NFC poller"); + nfc_poller_stop(seader->poller); + nfc_poller_free(seader->poller); + seader->poller = NULL; + } +} + +static void seader_hf_release_host_picopass(void* context) { + Seader* seader = context; + if(seader && seader->picopass_poller) { + FURI_LOG_I(TAG, "Stopping host Picopass poller"); + picopass_poller_stop(seader->picopass_poller); + picopass_poller_free(seader->picopass_poller); + seader->picopass_poller = NULL; + } +} + +static void seader_hf_release_plugin_free(void* context) { + Seader* seader = context; + if(seader && seader->plugin_hf && seader->hf_plugin_ctx) { + seader->plugin_hf->free(seader->hf_plugin_ctx); + } + if(seader) { + seader->hf_plugin_ctx = NULL; + seader->plugin_hf = NULL; + } +} + +static void seader_hf_release_plugin_manager(void* context) { + Seader* seader = context; + if(seader && seader->hf_plugin_manager) { + FURI_LOG_I(TAG, "Unloading HF plugin"); + plugin_manager_free(seader->hf_plugin_manager); + seader->hf_plugin_manager = NULL; + } +} + +static void seader_hf_release_worker_reset(void* context) { + Seader* seader = context; + if(seader && seader->worker) { + seader_worker_reset_poller_session(seader->worker); + } +} bool seader_custom_event_callback(void* context, uint32_t event) { furi_assert(context); @@ -21,6 +449,10 @@ void seader_tick_event_callback(void* context) { scene_manager_handle_tick_event(seader->scene_manager); } +static bool seader_align_is_valid(size_t align) { + return align != 0U && ((align & (align - 1U)) == 0U); +} + Seader* seader_alloc() { Seader* seader = malloc(sizeof(Seader)); seader_trace_reset(); @@ -34,14 +466,17 @@ Seader* seader_alloc() { seader->sam_state = SeaderSamStateIdle; seader->sam_intent = SeaderSamIntentNone; seader->sam_present = false; + memset(seader->sam_version, 0, sizeof(seader->sam_version)); seader_sam_key_label_format( false, NULL, 0U, seader->sam_key_label, sizeof(seader->sam_key_label)); seader_uhf_status_label_format( false, false, false, false, seader->uhf_status_label, sizeof(seader->uhf_status_label)); - memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types)); - seader->detected_card_type_count = 0; - seader->selected_read_type = SeaderCredentialTypeNone; seader_uhf_snmp_probe_init(&seader->snmp_probe); + seader->nfc = nfc_alloc(); + seader->nfc_device = seader->nfc ? nfc_device_alloc() : NULL; + seader->scratch.offset = 0U; + seader->scratch.high_water = 0U; + seader->hf_mode = NULL; seader->worker = seader_worker_alloc(); seader->view_dispatcher = view_dispatcher_alloc(); @@ -58,8 +493,13 @@ Seader* seader_alloc() { seader->credential = seader_credential_alloc(); - seader->nfc = NULL; - seader->nfc_device = NULL; + if(!seader->nfc || !seader->nfc_device) { + FURI_LOG_W( + TAG, + "HF host NFC objects unavailable at startup nfc=%p device=%p", + seader->nfc, + seader->nfc_device); + } // Open GUI record seader->gui = furi_record_open(RECORD_GUI); @@ -106,27 +546,20 @@ Seader* seader_alloc() { seader->temp_string3 = furi_string_alloc(); seader->temp_string4 = furi_string_alloc(); - seader->plugin_manager = - plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface); - + seader->plugin_manager = NULL; seader->plugin_wiegand = NULL; - FURI_LOG_I(TAG, "Loading plugins from %s", APP_ASSETS_PATH("plugins")); - if(plugin_manager_load_all(seader->plugin_manager, APP_ASSETS_PATH("plugins")) != - PluginManagerErrorNone) { - FURI_LOG_E(TAG, "Failed to load all libs"); - } else { - uint32_t plugin_count = plugin_manager_get_count(seader->plugin_manager); - FURI_LOG_I(TAG, "Loaded %lu plugin(s)", plugin_count); + seader->hf_plugin_manager = NULL; + seader->plugin_hf = NULL; + seader->hf_plugin_ctx = NULL; + seader->mode_runtime = SeaderModeRuntimeNone; + seader->hf_session_state = SeaderHfSessionStateUnloaded; + seader->hf_teardown_action = SeaderHfTeardownActionNone; + seader->loading_popup_enabled = true; + seader->start_scene_active = false; + seader->sam_present_menu_guard_active = false; - for(uint32_t i = 0; i < plugin_count; i++) { - const PluginWiegand* plugin = plugin_manager_get_ep(seader->plugin_manager, i); - FURI_LOG_I(TAG, "plugin index %lu, name: %s", i, plugin->name); - if(strcmp(plugin->name, "Plugin Wiegand") == 0) { - FURI_LOG_I(TAG, "Wiegand plugin found and assigned"); - // Have to cast to drop "const" qualifier - seader->plugin_wiegand = (PluginWiegand*)plugin; - } - } + if(seader->nfc_device) { + nfc_device_set_loading_callback(seader->nfc_device, seader_nfc_loading_callback, seader); } return seader; @@ -139,21 +572,32 @@ void seader_free(Seader* seader) { furi_hal_power_disable_otg(); } - seader_uart_free(seader->uart); - seader->uart = NULL; + seader->loading_popup_enabled = false; + seader_hf_teardown_blocking(seader); + seader_hf_mode_deactivate(seader); + seader_worker_release(seader); + if(seader->worker) { + seader_worker_free(seader->worker); + seader->worker = NULL; + } - seader_credential_free(seader->credential); - seader->credential = NULL; + seader_wiegand_plugin_release(seader); + + if(seader->nfc_device) { + nfc_device_free(seader->nfc_device); + seader->nfc_device = NULL; + } if(seader->nfc) { nfc_free(seader->nfc); seader->nfc = NULL; } - if(seader->nfc_device) { - nfc_device_free(seader->nfc_device); - seader->nfc_device = NULL; - } + seader_uart_free(seader->uart); + seader->uart = NULL; + + seader_credential_free(seader->credential); + seader->credential = NULL; // Submenu view_dispatcher_remove_view(seader->view_dispatcher, SeaderViewMenu); @@ -186,10 +630,6 @@ void seader_free(Seader* seader) { furi_string_free(seader->temp_string3); furi_string_free(seader->temp_string4); - // Worker - seader_worker_stop(seader->worker); - seader_worker_free(seader->worker); - // View Dispatcher view_dispatcher_free(seader->view_dispatcher); @@ -204,8 +644,6 @@ void seader_free(Seader* seader) { furi_record_close(RECORD_NOTIFICATION); seader->notifications = NULL; - plugin_manager_free(seader->plugin_manager); - free(seader); } @@ -242,8 +680,20 @@ void seader_blink_stop(Seader* seader) { notification_message(seader->notifications, &seader_sequence_blink_stop); } +void seader_nfc_loading_callback(void* context, bool show) { + Seader* seader = context; + if(!seader || !seader->loading_popup_enabled || !seader->view_dispatcher) { + return; + } + + seader_show_loading_popup(seader, show); +} + void seader_show_loading_popup(void* context, bool show) { Seader* seader = context; + if(!seader || !seader->loading_popup_enabled || !seader->view_dispatcher) { + return; + } if(show) { // Raise timer priority so that animations can play @@ -255,6 +705,393 @@ void seader_show_loading_popup(void* context, bool show) { } } +bool seader_wiegand_plugin_acquire(Seader* seader) { + furi_assert(seader); + + if(seader->plugin_wiegand) { + return true; + } + + if(!seader->plugin_manager) { + seader->plugin_manager = + plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface); + if(!seader->plugin_manager) { + FURI_LOG_E(TAG, "Failed to allocate plugin manager"); + return false; + } + } + + FURI_LOG_I(TAG, "Loading Wiegand plugin from %s", SEADER_WIEGAND_PLUGIN_PATH); + if(plugin_manager_load_single(seader->plugin_manager, SEADER_WIEGAND_PLUGIN_PATH) != + PluginManagerErrorNone) { + FURI_LOG_E(TAG, "Failed to load Wiegand plugin"); + plugin_manager_free(seader->plugin_manager); + seader->plugin_manager = NULL; + return false; + } + + if(plugin_manager_get_count(seader->plugin_manager) == 0) { + FURI_LOG_E(TAG, "Wiegand plugin manager is empty after load"); + plugin_manager_free(seader->plugin_manager); + seader->plugin_manager = NULL; + return false; + } + + seader->plugin_wiegand = (PluginWiegand*)plugin_manager_get_ep(seader->plugin_manager, 0); + + if(!seader->plugin_wiegand) { + FURI_LOG_E(TAG, "Failed to resolve Wiegand plugin entry point"); + plugin_manager_free(seader->plugin_manager); + seader->plugin_manager = NULL; + return false; + } + + FURI_LOG_I(TAG, "Wiegand plugin loaded: %s", seader->plugin_wiegand->name); + return true; +} + +void seader_wiegand_plugin_release(Seader* seader) { + furi_assert(seader); + + if(!seader->plugin_manager) { + seader->plugin_wiegand = NULL; + return; + } + + FURI_LOG_I(TAG, "Unloading Wiegand plugin"); + seader->plugin_wiegand = NULL; + plugin_manager_free(seader->plugin_manager); + seader->plugin_manager = NULL; +} + +bool seader_hf_plugin_acquire(Seader* seader) { + furi_assert(seader); + + /* UHF maintenance and HF runtime are mutually exclusive mode owners. */ + if(seader->mode_runtime == SeaderModeRuntimeUHF) { + FURI_LOG_W(TAG, "Reject HF plugin acquire while UHF runtime is active"); + return false; + } + + if(seader->hf_session_state == SeaderHfSessionStateTearingDown) { + FURI_LOG_W(TAG, "Reject HF plugin acquire during teardown"); + return false; + } + + /* Re-acquire is allowed only when the live runtime is already coherent. */ + if(seader->plugin_hf && seader->hf_plugin_ctx) { + if(seader->hf_session_state == SeaderHfSessionStateUnloaded) { + seader->hf_session_state = SeaderHfSessionStateLoaded; + } + seader->mode_runtime = SeaderModeRuntimeHF; + return true; + } + + /* Partial pointer state is always a bug; normalize through the single release path + instead of trying to reason about each damaged combination inline. */ + if(seader->hf_plugin_manager || seader->plugin_hf || seader->hf_plugin_ctx) { + FURI_LOG_W( + TAG, + "Normalize partial HF session manager=%p plugin=%p ctx=%p state=%d", + (void*)seader->hf_plugin_manager, + (void*)seader->plugin_hf, + seader->hf_plugin_ctx, + seader->hf_session_state); + seader_hf_plugin_release(seader); + } + + if(!seader->nfc || !seader->nfc_device) { + FURI_LOG_E( + TAG, "Host NFC objects unavailable nfc=%p device=%p", seader->nfc, seader->nfc_device); + return false; + } + + if(!seader->hf_plugin_manager) { + seader->hf_plugin_manager = + plugin_manager_alloc(HF_PLUGIN_APP_ID, HF_PLUGIN_API_VERSION, firmware_api_interface); + if(!seader->hf_plugin_manager) { + FURI_LOG_E(TAG, "Failed to allocate HF plugin manager"); + return false; + } + } + + FURI_LOG_I(TAG, "Loading HF plugin from %s", SEADER_HF_PLUGIN_PATH); + if(plugin_manager_load_single(seader->hf_plugin_manager, SEADER_HF_PLUGIN_PATH) != + PluginManagerErrorNone) { + FURI_LOG_E(TAG, "Failed to load HF plugin"); + plugin_manager_free(seader->hf_plugin_manager); + seader_hf_session_force_unloaded(seader); + return false; + } + + seader->plugin_hf = (PluginHf*)plugin_manager_get_ep(seader->hf_plugin_manager, 0); + + if(!seader->plugin_hf) { + FURI_LOG_E(TAG, "Failed to resolve HF plugin entry point"); + plugin_manager_free(seader->hf_plugin_manager); + seader_hf_session_force_unloaded(seader); + return false; + } + + seader->hf_plugin_ctx = seader->plugin_hf->alloc(&seader_hf_plugin_host_api, seader); + if(!seader->hf_plugin_ctx) { + FURI_LOG_E(TAG, "Failed to allocate HF plugin context"); + plugin_manager_free(seader->hf_plugin_manager); + seader_hf_session_force_unloaded(seader); + return false; + } + + seader->hf_session_state = SeaderHfSessionStateLoaded; + seader->mode_runtime = SeaderModeRuntimeHF; + FURI_LOG_I(TAG, "HF plugin loaded: %s", seader->plugin_hf->name); + return true; +} + +static bool seader_hf_has_runtime(const Seader* seader) { + return seader && (seader->hf_plugin_manager || seader->plugin_hf || seader->hf_plugin_ctx || + seader->poller || seader->picopass_poller); +} + +/* App shutdown uses the same teardown primitive as normal navigation. The only difference + is that shutdown waits synchronously for the worker-owned teardown to finish. */ +static void seader_hf_teardown_blocking(Seader* seader) { + if(!seader || !seader_hf_has_runtime(seader)) { + return; + } + + seader_runtime_begin_hf_teardown(&seader->hf_session_state); + if(!seader_worker_acquire(seader) || !seader->worker || !seader->uart) { + FURI_LOG_W(TAG, "HF blocking teardown fallback"); + seader_hf_plugin_release(seader); + return; + } + + seader_worker_stop(seader->worker); + FURI_LOG_I(TAG, "HF teardown blocking"); + seader_worker_start(seader->worker, SeaderWorkerStateHfTeardown, seader->uart, NULL, seader); + seader_worker_join(seader->worker); +} + +/* All HF runtime shutdown funnels through the canonical release sequence so stop/free/unload + order cannot silently diverge between code paths. */ +void seader_hf_plugin_release(Seader* seader) { + furi_assert(seader); + SeaderHfReleaseSequence release_sequence = { + .context = seader, + .hf_session_state = &seader->hf_session_state, + .mode_runtime = &seader->mode_runtime, + .plugin_stop = seader_hf_release_plugin_stop, + .host_poller_release = seader_hf_release_host_poller, + .host_picopass_release = seader_hf_release_host_picopass, + .plugin_free = seader_hf_release_plugin_free, + .plugin_manager_unload = seader_hf_release_plugin_manager, + .worker_reset = seader_hf_release_worker_reset, + }; + seader_hf_release_sequence_run(&release_sequence); +} + +/* Teardown completion is the single place that collapses HF UI/runtime mode back into + ordinary app navigation. Scenes request teardown targets; they do not perform teardown. */ +bool seader_hf_finish_teardown_action(Seader* seader) { + if(!seader) { + return false; + } + + FURI_LOG_I(TAG, "HF teardown complete action=%d", seader->hf_teardown_action); + seader_show_loading_popup(seader, false); + seader_hf_mode_set_selected_read_type(seader, SeaderCredentialTypeNone); + seader_hf_mode_clear_detected_types(seader); + seader_hf_mode_deactivate(seader); + + const SeaderHfTeardownAction action = seader->hf_teardown_action; + seader->hf_teardown_action = SeaderHfTeardownActionNone; + + switch(action) { + case SeaderHfTeardownActionSamPresent: + return scene_manager_search_and_switch_to_another_scene( + seader->scene_manager, SeaderSceneSamPresent); + case SeaderHfTeardownActionRestartRead: + scene_manager_next_scene(seader->scene_manager, SeaderSceneRead); + return true; + case SeaderHfTeardownActionStopApp: + scene_manager_stop(seader->scene_manager); + view_dispatcher_stop(seader->view_dispatcher); + return true; + case SeaderHfTeardownActionNone: + default: + return false; + } +} + +/* Requesting teardown is intentionally cheap: record the target, handle the no-runtime and + already-tearing-down fast paths, and hand off actual release work to the worker. */ +bool seader_hf_request_teardown(Seader* seader, SeaderHfTeardownAction action) { + furi_assert(seader); + + FURI_LOG_I( + TAG, + "HF teardown requested action=%d state=%d worker_state=%d", + action, + seader->hf_session_state, + seader->worker ? seader_worker_get_state(seader->worker) : -1); + + seader->hf_teardown_action = action; + if(!seader_hf_has_runtime(seader)) { + seader->hf_session_state = SeaderHfSessionStateUnloaded; + return seader_hf_finish_teardown_action(seader); + } + + if(!seader_worker_acquire(seader)) { + return seader_hf_finish_teardown_action(seader); + } + + if(seader->hf_session_state == SeaderHfSessionStateTearingDown || + (seader->worker && + seader_worker_get_state(seader->worker) == SeaderWorkerStateHfTeardown)) { + return true; + } + + seader->hf_session_state = SeaderHfSessionStateTearingDown; + seader_worker_stop(seader->worker); + seader_worker_start( + seader->worker, + SeaderWorkerStateHfTeardown, + seader->uart, + seader_hf_worker_event_callback, + seader); + return true; +} + +bool seader_worker_acquire(Seader* seader) { + furi_assert(seader); + + if(seader->worker) { + return true; + } + + seader->worker = seader_worker_alloc(); + return seader->worker != NULL; +} + +void seader_worker_release(Seader* seader) { + furi_assert(seader); + + if(!seader->worker) { + return; + } + + seader_worker_stop(seader->worker); + seader->worker->callback = NULL; + seader->worker->context = NULL; + seader_worker_change_state(seader->worker, SeaderWorkerStateReady); +} + +void seader_scratch_reset(Seader* seader) { + furi_assert(seader); + seader->scratch.offset = 0U; +} + +void* seader_scratch_alloc(Seader* seader, size_t size, size_t align) { + furi_assert(seader); + furi_assert(seader_align_is_valid(align)); + + const size_t mask = align - 1U; + const size_t aligned_offset = (seader->scratch.offset + mask) & ~mask; + if(aligned_offset + size > sizeof(seader->scratch.arena)) { + FURI_LOG_E(TAG, "Scratch overflow: need=%zu offset=%zu", size, aligned_offset); + return NULL; + } + + void* ptr = &seader->scratch.arena[aligned_offset]; + memset(ptr, 0, size); + seader->scratch.offset = aligned_offset + size; + if(seader->scratch.offset > seader->scratch.high_water) { + seader->scratch.high_water = seader->scratch.offset; + } + + return ptr; +} + +bool seader_hf_mode_activate(Seader* seader) { + furi_assert(seader); + + if(seader->hf_mode) { + return true; + } + + seader_scratch_reset(seader); + seader->hf_mode = + seader_scratch_alloc(seader, sizeof(SeaderHfModeContext), _Alignof(SeaderHfModeContext)); + if(!seader->hf_mode) { + return false; + } + + seader->hf_mode->selected_read_type = SeaderCredentialTypeNone; + return true; +} + +void seader_hf_mode_deactivate(Seader* seader) { + furi_assert(seader); + + seader->hf_mode = NULL; + seader_scratch_reset(seader); +} + +SeaderCredentialType seader_hf_mode_get_selected_read_type(const Seader* seader) { + return seader && seader->hf_mode ? seader->hf_mode->selected_read_type : + SeaderCredentialTypeNone; +} + +void seader_hf_mode_set_selected_read_type(Seader* seader, SeaderCredentialType type) { + if(!seader || !seader->hf_mode) { + FURI_LOG_W( + TAG, + "Ignoring HF selected read type update without mode context seader=%p hf_mode=%p type=%d", + seader, + seader ? seader->hf_mode : NULL, + type); + return; + } + seader->hf_mode->selected_read_type = type; +} + +void seader_hf_mode_set_detected_types( + Seader* seader, + const SeaderCredentialType* types, + size_t count) { + if(!seader || !seader->hf_mode) { + FURI_LOG_W( + TAG, + "Ignoring HF detected types update without mode context seader=%p hf_mode=%p count=%zu", + seader, + seader ? seader->hf_mode : NULL, + count); + return; + } + + if(count > SEADER_MAX_DETECTED_CARD_TYPES) { + count = SEADER_MAX_DETECTED_CARD_TYPES; + } + + memset(seader->hf_mode->detected_card_types, 0, sizeof(seader->hf_mode->detected_card_types)); + if(types && count > 0) { + memcpy(seader->hf_mode->detected_card_types, types, count * sizeof(types[0])); + } + seader->hf_mode->detected_card_type_count = count; +} + +size_t seader_hf_mode_get_detected_type_count(const Seader* seader) { + return seader && seader->hf_mode ? seader->hf_mode->detected_card_type_count : 0U; +} + +const SeaderCredentialType* seader_hf_mode_get_detected_types(const Seader* seader) { + return seader && seader->hf_mode ? seader->hf_mode->detected_card_types : NULL; +} + +void seader_hf_mode_clear_detected_types(Seader* seader) { + seader_hf_mode_set_detected_types(seader, NULL, 0U); +} + int32_t seader_app(void* p) { UNUSED(p); Seader* seader = seader_alloc(); diff --git a/seader.h b/seader.h index 7bab59c..ab19883 100644 --- a/seader.h +++ b/seader.h @@ -1,4 +1,38 @@ #pragma once +#include +#include + typedef struct Seader Seader; typedef struct SeaderPollerContainer SeaderPollerContainer; + +typedef enum { + SeaderHfSessionStateUnloaded, + SeaderHfSessionStateLoaded, + SeaderHfSessionStateActive, + SeaderHfSessionStateTearingDown, +} SeaderHfSessionState; + +typedef enum { + SeaderModeRuntimeNone, + SeaderModeRuntimeHF, + SeaderModeRuntimeUHF, +} SeaderModeRuntime; + +typedef enum { + SeaderHfTeardownActionNone, + SeaderHfTeardownActionSamPresent, + SeaderHfTeardownActionRestartRead, + SeaderHfTeardownActionStopApp, +} SeaderHfTeardownAction; + +bool seader_worker_acquire(Seader* seader); +void seader_worker_release(Seader* seader); +void seader_scratch_reset(Seader* seader); +void* seader_scratch_alloc(Seader* seader, size_t size, size_t align); +bool seader_wiegand_plugin_acquire(Seader* seader); +void seader_wiegand_plugin_release(Seader* seader); +bool seader_hf_plugin_acquire(Seader* seader); +void seader_hf_plugin_release(Seader* seader); +bool seader_hf_request_teardown(Seader* seader, SeaderHfTeardownAction action); +bool seader_hf_finish_teardown_action(Seader* seader); diff --git a/seader_credential.h b/seader_credential.h index 05cddb3..91febac 100644 --- a/seader_credential.h +++ b/seader_credential.h @@ -5,6 +5,7 @@ #include #include #include "protocol/picopass_protocol.h" +#include "seader_credential_type.h" #include #include @@ -15,16 +16,6 @@ typedef void (*SeaderLoadingCallback)(void* context, bool state); -typedef enum { - SeaderCredentialTypeNone, - SeaderCredentialTypePicopass, - SeaderCredentialType14A, - // Might need to make 14a into "javacard" and add Desfire - SeaderCredentialTypeMifareClassic, - SeaderCredentialTypeVirtual, - SeaderCredentialTypeConfig, -} SeaderCredentialType; - typedef enum { SeaderPacsMediaTypeUnknown = 0, SeaderPacsMediaTypeDesfire = 1, diff --git a/seader_credential_type.h b/seader_credential_type.h new file mode 100644 index 0000000..d74b682 --- /dev/null +++ b/seader_credential_type.h @@ -0,0 +1,10 @@ +#pragma once + +typedef enum { + SeaderCredentialTypeNone, + SeaderCredentialTypePicopass, + SeaderCredentialType14A, + SeaderCredentialTypeMifareClassic, + SeaderCredentialTypeVirtual, + SeaderCredentialTypeConfig, +} SeaderCredentialType; diff --git a/seader_hf_mode.c b/seader_hf_mode.c new file mode 100644 index 0000000..f7881e4 --- /dev/null +++ b/seader_hf_mode.c @@ -0,0 +1,4 @@ +#include "seader_i.h" + +// The first RAM-focused step keeps HF mode state small and scratch-backed. +// Additional HF-specific session state can move here later without changing host ownership. diff --git a/seader_i.h b/seader_i.h index 2cb883e..8bfa5f9 100644 --- a/seader_i.h +++ b/seader_i.h @@ -39,7 +39,8 @@ #include #include -#include "plugin/interface.h" +#include "wiegand_interface_fal/interface.h" +#include "hf_interface_fal/hf_interface.h" #include #include #include @@ -68,6 +69,7 @@ #define SEADER_TEXT_STORE_SIZE 128 #define SEADER_MAX_ATR_SIZE 33 #define MAX_FRAME_HEADERS 32 +#define SEADER_SCRATCH_SIZE 512 #define SEADER_MAX_DETECTED_CARD_TYPES 3 enum SeaderCustomEvent { @@ -103,6 +105,18 @@ typedef struct { uint16_t current_line; } SeaderAPDURunnerContext; +typedef struct { + size_t offset; + size_t high_water; + uint8_t arena[SEADER_SCRATCH_SIZE]; +} SeaderScratch; + +typedef struct { + SeaderCredentialType detected_card_types[SEADER_MAX_DETECTED_CARD_TYPES]; + size_t detected_card_type_count; + SeaderCredentialType selected_read_type; +} SeaderHfModeContext; + typedef enum { SeaderSamStateIdle, SeaderSamStateDetectPending, @@ -135,11 +149,14 @@ struct Seader { SeaderSamState sam_state; SeaderSamIntent sam_intent; bool sam_present; + uint8_t sam_version[2]; uint8_t ATR[SEADER_MAX_ATR_SIZE]; size_t ATR_len; char sam_key_label[SEADER_SAM_KEY_LABEL_MAX_LEN]; char uhf_status_label[SEADER_UHF_STATUS_LABEL_MAX_LEN]; SeaderUhfSnmpProbe snmp_probe; + SeaderScratch scratch; + SeaderHfModeContext* hf_mode; char text_store[SEADER_TEXT_STORE_SIZE + 1]; char read_error[SEADER_TEXT_STORE_SIZE + 1]; @@ -164,12 +181,18 @@ struct Seader { PicopassPoller* picopass_poller; NfcDevice* nfc_device; - SeaderCredentialType detected_card_types[SEADER_MAX_DETECTED_CARD_TYPES]; - size_t detected_card_type_count; - SeaderCredentialType selected_read_type; PluginManager* plugin_manager; PluginWiegand* plugin_wiegand; + PluginManager* hf_plugin_manager; + PluginHf* plugin_hf; + void* hf_plugin_ctx; + SeaderModeRuntime mode_runtime; + SeaderHfSessionState hf_session_state; + SeaderHfTeardownAction hf_teardown_action; + bool loading_popup_enabled; + bool start_scene_active; + bool sam_present_menu_guard_active; APDULog* apdu_log; SeaderAPDURunnerContext apdu_runner_ctx; @@ -199,4 +222,17 @@ void seader_blink_start(Seader* seader); void seader_blink_stop(Seader* seader); +void seader_nfc_loading_callback(void* context, bool show); void seader_show_loading_popup(void* context, bool show); + +bool seader_hf_mode_activate(Seader* seader); +void seader_hf_mode_deactivate(Seader* seader); +SeaderCredentialType seader_hf_mode_get_selected_read_type(const Seader* seader); +void seader_hf_mode_set_selected_read_type(Seader* seader, SeaderCredentialType type); +void seader_hf_mode_set_detected_types( + Seader* seader, + const SeaderCredentialType* types, + size_t count); +size_t seader_hf_mode_get_detected_type_count(const Seader* seader); +const SeaderCredentialType* seader_hf_mode_get_detected_types(const Seader* seader); +void seader_hf_mode_clear_detected_types(Seader* seader); diff --git a/seader_worker.c b/seader_worker.c index 84ba3fb..4922be9 100644 --- a/seader_worker.c +++ b/seader_worker.c @@ -17,13 +17,31 @@ // Forward declaration void seader_send_card_detected(SeaderUartBridge* seader_uart, CardDetails_t* cardDetails); void seader_worker_reading(Seader* seader); -void seader_worker_poller_conversation(Seader* seader, SeaderPollerContainer* spc); + +static void seader_worker_release_hf_session(Seader* seader) { + if(!seader) { + return; + } + + seader_hf_plugin_release(seader); +} typedef struct { - bool done; - bool detected; + volatile bool done; + volatile bool detected; } SeaderPicopassDetectContext; +static void seader_worker_clear_active_card(Seader* seader, const char* reason) { + if(!seader) { + return; + } + + if(seader_sam_has_active_card(seader)) { + FURI_LOG_I(TAG, "Clear active SAM card (%s)", reason ? reason : "worker"); + seader_send_no_card_detected(seader); + } +} + static NfcCommand seader_worker_picopass_detect_callback(PicopassPollerEvent event, void* context) { SeaderPicopassDetectContext* detect_context = context; @@ -46,14 +64,19 @@ static bool seader_worker_detect_picopass(Nfc* nfc) { PicopassPoller* poller = picopass_poller_alloc(nfc); SeaderPicopassDetectContext detect_context = {0}; + if(!poller) { + FURI_LOG_W(TAG, "Failed to allocate Picopass detect poller"); + return false; + } + picopass_poller_start(poller, seader_worker_picopass_detect_callback, &detect_context); for(uint8_t i = 0; i < 10 && !detect_context.done; i++) { furi_delay_ms(10); } - detected = detect_context.detected; picopass_poller_stop(poller); + detected = detect_context.detected; picopass_poller_free(poller); return detected; @@ -75,7 +98,7 @@ static void seader_worker_add_detected_type( } } -static size_t seader_worker_detect_supported_types( +static size_t __attribute__((unused)) seader_worker_detect_supported_types( Seader* seader, SeaderCredentialType* detected_types, size_t detected_capacity) { @@ -103,7 +126,8 @@ static size_t seader_worker_detect_supported_types( return detected_type_count; } -static bool seader_worker_start_read_for_type(Seader* seader, SeaderCredentialType type) { +static bool __attribute__((unused)) +seader_worker_start_read_for_type(Seader* seader, SeaderCredentialType type) { NfcPoller* poller_detect = NULL; if(type == SeaderCredentialType14A) { @@ -161,7 +185,6 @@ SeaderWorker* seader_worker_alloc() { seader_worker->callback = NULL; seader_worker->context = NULL; seader_worker->storage = furi_record_open(RECORD_STORAGE); - memset(seader_worker->sam_version, 0, sizeof(seader_worker->sam_version)); seader_worker_change_state(seader_worker, SeaderWorkerStateReady); @@ -196,7 +219,9 @@ void seader_worker_start( seader_worker_stop(seader_worker); } - seader_worker->stage = SeaderPollerEventTypeCardDetect; + /* Worker startup owns queue/stage reset. Scene code must not pre-reset the live + poller session because the worker is the runtime owner for those objects. */ + seader_worker_reset_poller_session(seader_worker); seader_worker->callback = callback; seader_worker->context = context; seader_worker->uart = uart; @@ -214,6 +239,15 @@ void seader_worker_stop(SeaderWorker* seader_worker) { furi_thread_join(seader_worker->thread); } +void seader_worker_join(SeaderWorker* seader_worker) { + furi_assert(seader_worker); + if(furi_thread_get_state(seader_worker->thread) == FuriThreadStateStopped) { + return; + } + + furi_thread_join(seader_worker->thread); +} + void seader_worker_change_state(SeaderWorker* seader_worker, SeaderWorkerState state) { seader_worker->state = state; } @@ -247,7 +281,6 @@ void seader_worker_reset_poller_session(SeaderWorker* seader_worker) { furi_message_queue_get_count(seader_worker->messages)); furi_message_queue_reset(seader_worker->messages); - seader_worker->stage = SeaderPollerEventTypeCardDetect; } @@ -259,6 +292,8 @@ bool seader_process_success_response(Seader* seader, uint8_t* apdu, size_t len) if(seader_process_success_response_i(seader, apdu, len, false, NULL)) { // no-op, message was processed } else { + /* Outside an active conversation, an unhandled SAM message is stale noise from a + previous flow. Enqueueing it would let old maintenance/read traffic bleed forward. */ if(seader_worker->state != SeaderWorkerStateVirtualCredential && seader_worker->stage != SeaderPollerEventTypeConversation) { FURI_LOG_I( @@ -299,8 +334,11 @@ bool seader_process_success_response(Seader* seader, uint8_t* apdu, size_t len) } bool seader_worker_process_sam_message(Seader* seader, uint8_t* apdu, uint32_t len) { + furi_check(seader); SeaderWorker* seader_worker = seader->worker; + furi_check(seader_worker); SeaderUartBridge* seader_uart = seader_worker->uart; + furi_check(seader_uart); if(len < 2) { return false; } @@ -420,6 +458,12 @@ int32_t seader_worker_task(void* context) { FURI_LOG_D(TAG, "APDU Runner"); seader_apdu_runner_init(seader); return 0; + } else if(seader_worker->state == SeaderWorkerStateHfTeardown) { + FURI_LOG_I(TAG, "HF teardown started"); + seader_worker_release_hf_session(seader); + if(seader_worker->callback) { + seader_worker->callback(SeaderWorkerEventHfTeardownComplete, seader_worker->context); + } } else if(seader_worker->state == SeaderWorkerStateReading) { FURI_LOG_D(TAG, "Reading mode started"); seader_worker_reading(seader); @@ -433,26 +477,29 @@ void seader_worker_reading(Seader* seader) { SeaderWorker* seader_worker = seader->worker; FURI_LOG_I(TAG, "Reading loop started"); - seader->nfc = nfc_alloc(); - seader->nfc_device = nfc_device_alloc(); - nfc_device_set_loading_callback(seader->nfc_device, seader_show_loading_popup, seader); + if(!seader_hf_plugin_acquire(seader) || !seader->plugin_hf || !seader->hf_plugin_ctx) { + FURI_LOG_E(TAG, "HF plugin unavailable"); + strlcpy(seader->read_error, "HF plugin unavailable", sizeof(seader->read_error)); + if(seader_worker->callback) { + seader_worker->callback(SeaderWorkerEventFail, seader_worker->context); + } + return; + } while(seader_worker->state == SeaderWorkerStateReading) { bool detected = false; SeaderPollerEventType result_stage = SeaderPollerEventTypeFail; - SeaderCredentialType type_to_read = seader->selected_read_type; + SeaderCredentialType type_to_read = seader_hf_mode_get_selected_read_type(seader); + FURI_LOG_D(TAG, "HF loop selected type=%d stage=%d", type_to_read, seader_worker->stage); if(type_to_read == SeaderCredentialTypeNone) { SeaderCredentialType detected_types[SEADER_MAX_DETECTED_CARD_TYPES] = {0}; - const size_t detected_type_count = seader_worker_detect_supported_types( - seader, detected_types, COUNT_OF(detected_types)); + const size_t detected_type_count = seader->plugin_hf->detect_supported_types( + seader->hf_plugin_ctx, detected_types, COUNT_OF(detected_types)); + FURI_LOG_I(TAG, "HF plugin detected %u type(s)", detected_type_count); if(detected_type_count > 1) { - memcpy( - seader->detected_card_types, - detected_types, - sizeof(seader->detected_card_types)); - seader->detected_card_type_count = detected_type_count; + seader_hf_mode_set_detected_types(seader, detected_types, detected_type_count); if(seader_worker->callback) { seader_worker->callback( SeaderWorkerEventSelectCardType, seader_worker->context); @@ -464,7 +511,12 @@ void seader_worker_reading(Seader* seader) { } if(type_to_read != SeaderCredentialTypeNone) { - detected = seader_worker_start_read_for_type(seader, type_to_read); + FURI_LOG_I(TAG, "HF start read for type=%d", type_to_read); + detected = seader->plugin_hf->start_read_for_type(seader->hf_plugin_ctx, type_to_read); + if(detected) { + seader->hf_session_state = SeaderHfSessionStateActive; + } + FURI_LOG_I(TAG, "HF start read result=%d", detected); } if(detected) { @@ -477,18 +529,11 @@ void seader_worker_reading(Seader* seader) { furi_delay_ms(10); } result_stage = seader_worker->stage; - - // Cleanup poller - if(seader->poller) { - nfc_poller_stop(seader->poller); - nfc_poller_free(seader->poller); - seader->poller = NULL; - } - if(seader->picopass_poller) { - picopass_poller_stop(seader->picopass_poller); - picopass_poller_free(seader->picopass_poller); - seader->picopass_poller = NULL; - } + /* SAM active-card state belongs to the read lifecycle, not to the success scene. + Clear it as soon as the poller conversation reaches a terminal stage. */ + seader_worker_clear_active_card( + seader, + result_stage == SeaderPollerEventTypeComplete ? "read-complete" : "read-abort"); if(result_stage == SeaderPollerEventTypeComplete) { // Notify UI of success @@ -504,19 +549,16 @@ void seader_worker_reading(Seader* seader) { } } - nfc_free(seader->nfc); - seader->nfc = NULL; - nfc_device_free(seader->nfc_device); - seader->nfc_device = NULL; - FURI_LOG_I(TAG, "Reading loop stopped"); } -void seader_worker_poller_conversation(Seader* seader, SeaderPollerContainer* spc) { +void seader_worker_run_hf_conversation(Seader* seader) { SeaderWorker* seader_worker = seader->worker; furi_thread_set_current_priority(FuriThreadPriorityHighest); + /* The NFC callback thread stays in this loop while the SAM drives the conversation. + The worker queue is the bridge between SAM APDUs and the poller callback thread. */ while(seader_worker->stage == SeaderPollerEventTypeConversation && seader_worker->state == SeaderWorkerStateReading) { SeaderAPDU seaderApdu = {}; @@ -526,7 +568,7 @@ void seader_worker_poller_conversation(Seader* seader, SeaderPollerContainer* sp if(status == FuriStatusOk) { FURI_LOG_D(TAG, "Dequeue SAM message [%d bytes]", seaderApdu.len); if(seader_process_success_response_i( - seader, seaderApdu.buf, seaderApdu.len, true, spc)) { + seader, seaderApdu.buf, seaderApdu.len, true, NULL)) { // message was processed, loop again to see if SAM has more to say } else { FURI_LOG_I(TAG, "Response false, ending conversation"); @@ -547,15 +589,17 @@ void seader_worker_poller_conversation(Seader* seader, SeaderPollerContainer* sp } NfcCommand seader_worker_poller_callback_iso14443_4a(NfcGenericEvent event, void* context) { - furi_assert(event.protocol == NfcProtocolIso14443_4a); + if(event.protocol != NfcProtocolIso14443_4a || !event.event_data) { + FURI_LOG_W(TAG, "Ignore invalid host 14A callback"); + return NfcCommandStop; + } + furi_check(context); NfcCommand ret = NfcCommandContinue; Seader* seader = context; SeaderWorker* seader_worker = seader->worker; const Iso14443_4aPollerEvent* iso14443_4a_event = event.event_data; - SeaderPollerContainer spc = {.iso14443_4a_poller = event.instance}; - if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeReady) { if(seader_worker->stage == SeaderPollerEventTypeCardDetect) { FURI_LOG_D(TAG, "14a stage CardDetect -> Conversation"); @@ -592,7 +636,11 @@ NfcCommand seader_worker_poller_callback_iso14443_4a(NfcGenericEvent event, void uint8_t ats_len = 0; uint8_t* ats = malloc(4 + t1_tk_size); - furi_assert(ats); + if(!ats) { + FURI_LOG_E(TAG, "Failed to allocate host ATS buffer"); + seader_worker->stage = SeaderPollerEventTypeFail; + return NfcCommandStop; + } if(iso14443_4a_data->ats_data.tl > 1) { ats[ats_len++] = iso14443_4a_data->ats_data.t0; @@ -633,7 +681,7 @@ NfcCommand seader_worker_poller_callback_iso14443_4a(NfcGenericEvent event, void seader_worker->stage = SeaderPollerEventTypeConversation; } else if(seader_worker->stage == SeaderPollerEventTypeConversation) { seader_trace(TAG, "14a ready in Conversation"); - seader_worker_poller_conversation(seader, &spc); + seader_worker_run_hf_conversation(seader); } else if(seader_worker->stage == SeaderPollerEventTypeComplete) { seader_trace(TAG, "14a ready in Complete"); ret = NfcCommandStop; @@ -667,15 +715,17 @@ NfcCommand seader_worker_poller_callback_iso14443_4a(NfcGenericEvent event, void } NfcCommand seader_worker_poller_callback_mfc(NfcGenericEvent event, void* context) { - furi_assert(event.protocol == NfcProtocolMfClassic); + if(event.protocol != NfcProtocolMfClassic || !event.event_data) { + FURI_LOG_W(TAG, "Ignore invalid host MFC callback"); + return NfcCommandStop; + } + furi_check(context); NfcCommand ret = NfcCommandContinue; Seader* seader = context; SeaderWorker* seader_worker = seader->worker; MfClassicPollerEvent* mfc_event = event.event_data; - SeaderPollerContainer spc = {.mfc_poller = event.instance}; - if(mfc_event->type == MfClassicPollerEventTypeSuccess) { if(seader_worker->stage == SeaderPollerEventTypeCardDetect) { FURI_LOG_D(TAG, "MFC stage CardDetect -> Conversation"); @@ -706,7 +756,7 @@ NfcCommand seader_worker_poller_callback_mfc(NfcGenericEvent event, void* contex furi_thread_set_current_priority(FuriThreadPriorityLowest); seader_worker->stage = SeaderPollerEventTypeConversation; } else if(seader_worker->stage == SeaderPollerEventTypeConversation) { - seader_worker_poller_conversation(seader, &spc); + seader_worker_run_hf_conversation(seader); } else if(seader_worker->stage == SeaderPollerEventTypeComplete) { ret = NfcCommandStop; } else if(seader_worker->stage == SeaderPollerEventTypeFail) { @@ -725,15 +775,13 @@ NfcCommand seader_worker_poller_callback_mfc(NfcGenericEvent event, void* contex } NfcCommand seader_worker_poller_callback_picopass(PicopassPollerEvent event, void* context) { - furi_assert(context); + furi_check(context); NfcCommand ret = NfcCommandContinue; Seader* seader = context; SeaderWorker* seader_worker = seader->worker; // I know this is is passing the same thing that is on seader all the way down, but I prefer the symmetry between the 15a and iso15 stuff PicopassPoller* instance = seader->picopass_poller; - SeaderPollerContainer spc = {.picopass_poller = instance}; - if(event.type == PicopassPollerEventTypeCardDetected) { seader_worker->stage = SeaderPollerEventTypeCardDetect; } else if(event.type == PicopassPollerEventTypeSuccess) { @@ -761,7 +809,7 @@ NfcCommand seader_worker_poller_callback_picopass(PicopassPollerEvent event, voi furi_thread_set_current_priority(FuriThreadPriorityLowest); seader_worker->stage = SeaderPollerEventTypeConversation; } else if(seader_worker->stage == SeaderPollerEventTypeConversation) { - seader_worker_poller_conversation(seader, &spc); + seader_worker_run_hf_conversation(seader); } else if(seader_worker->stage == SeaderPollerEventTypeComplete) { ret = NfcCommandStop; } else if(seader_worker->stage == SeaderPollerEventTypeFail) { diff --git a/seader_worker.h b/seader_worker.h index d52e335..ce9e1b3 100644 --- a/seader_worker.h +++ b/seader_worker.h @@ -3,6 +3,8 @@ #include #include +#include "protocol/picopass_poller.h" +#include "seader.h" #include "sam_api.h" #include "seader_credential.h" #include "seader_bridge.h" @@ -22,6 +24,7 @@ typedef enum { SeaderWorkerStateVirtualCredential, SeaderWorkerStateAPDURunner, SeaderWorkerStateReading, + SeaderWorkerStateHfTeardown, // Transition SeaderWorkerStateStop, } SeaderWorkerState; @@ -42,6 +45,7 @@ typedef enum { SeaderWorkerEventAPDURunnerUpdate, SeaderWorkerEventAPDURunnerSuccess, SeaderWorkerEventAPDURunnerError, + SeaderWorkerEventHfTeardownComplete, } SeaderWorkerEvent; typedef enum { @@ -69,10 +73,12 @@ void seader_worker_start( void* context); void seader_worker_stop(SeaderWorker* seader_worker); +void seader_worker_join(SeaderWorker* seader_worker); bool seader_worker_process_sam_message(Seader* seader, uint8_t* apdu, uint32_t len); void seader_worker_send_version(Seader* seader); void seader_worker_cancel_poller_session(SeaderWorker* seader_worker); void seader_worker_reset_poller_session(SeaderWorker* seader_worker); +void seader_worker_run_hf_conversation(Seader* seader); NfcCommand seader_worker_poller_callback_iso14443_4a(NfcGenericEvent event, void* context); NfcCommand seader_worker_poller_callback_mfc(NfcGenericEvent event, void* context); diff --git a/seader_worker_i.h b/seader_worker_i.h index 62df0c2..a5a539b 100644 --- a/seader_worker_i.h +++ b/seader_worker_i.h @@ -26,7 +26,6 @@ struct SeaderWorker { FuriThread* thread; Storage* storage; - uint8_t sam_version[2]; FuriMessageQueue* messages; SeaderUartBridge* uart; SeaderWorkerCallback callback; diff --git a/t_1.c b/t_1.c index df92fd7..50e0d8c 100644 --- a/t_1.c +++ b/t_1.c @@ -19,6 +19,13 @@ static uint8_t seader_next_dpcb(SeaderUartBridge* seader_uart) { return t1->send_pcb; } +static SeaderUartBridge* seader_t1_active_uart(Seader* seader) { + furi_check(seader); + furi_check(seader->worker); + furi_check(seader->worker->uart); + return seader->worker->uart; +} + void seader_t_1_reset(SeaderUartBridge* seader_uart) { SeaderT1State* t1 = seader_t1_state(seader_uart); t1->nad = 0x00; @@ -31,8 +38,7 @@ void seader_t_1_reset(SeaderUartBridge* seader_uart) { } void seader_t_1_set_IFSD(Seader* seader) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_t1_active_uart(seader); SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t frame[5]; uint8_t frame_len = 0; @@ -51,8 +57,7 @@ void seader_t_1_set_IFSD(Seader* seader) { } static void seader_t_1_IFSD_response(Seader* seader, uint8_t ifs_value) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_t1_active_uart(seader); SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t frame[5]; uint8_t frame_len = 0; @@ -68,8 +73,7 @@ static void seader_t_1_IFSD_response(Seader* seader, uint8_t ifs_value) { } static void seader_t_1_WTX_response(Seader* seader, uint8_t multiplier) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_t1_active_uart(seader); SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t frame[5]; uint8_t frame_len = 0; @@ -85,8 +89,7 @@ static void seader_t_1_WTX_response(Seader* seader, uint8_t multiplier) { } static void seader_t_1_resynch_response(Seader* seader) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_t1_active_uart(seader); SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t frame[4]; uint8_t frame_len = 0; @@ -101,8 +104,7 @@ static void seader_t_1_resynch_response(Seader* seader) { } void seader_t_1_send_ack(Seader* seader) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_t1_active_uart(seader); SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t frame[4]; uint8_t frame_len = 0; @@ -117,8 +119,7 @@ void seader_t_1_send_ack(Seader* seader) { } static void seader_t_1_send_nak(Seader* seader) { - SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderUartBridge* seader_uart = seader_t1_active_uart(seader); SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t frame[4]; uint8_t frame_len = 0; @@ -211,8 +212,8 @@ void seader_send_t1(SeaderUartBridge* seader_uart, uint8_t* apdu, size_t len) { } bool seader_recv_t1(Seader* seader, CCID_Message* message) { + SeaderUartBridge* seader_uart = seader_t1_active_uart(seader); SeaderWorker* seader_worker = seader->worker; - SeaderUartBridge* seader_uart = seader_worker->uart; SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t* apdu = NULL; size_t apdu_len = 0; diff --git a/uart.c b/uart.c index 7f32edd..fa8cdc4 100644 --- a/uart.c +++ b/uart.c @@ -104,7 +104,15 @@ int32_t seader_uart_worker(void* context) { while(1) { uint32_t events = furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever); - furi_check(!(events & FuriFlagError)); + if(events & FuriFlagError) { + FURI_LOG_E( + TAG, + "RX worker flag error events=0x%08lx thread=%p tx_thread=%p", + (unsigned long)events, + (void*)seader_uart->thread, + (void*)seader_uart->tx_thread); + break; + } if(events & WorkerEvtStop) { memset(cmd, 0, cmd_len); cmd_len = 0; @@ -171,7 +179,14 @@ int32_t seader_uart_tx_thread(void* context) { while(1) { uint32_t events = furi_thread_flags_wait(WORKER_ALL_TX_EVENTS, FuriFlagWaitAny, FuriWaitForever); - furi_check(!(events & FuriFlagError)); + if(events & FuriFlagError) { + FURI_LOG_E( + TAG, + "TX worker flag error events=0x%08lx serial_handle=%p", + (unsigned long)events, + (void*)seader_uart->serial_handle); + break; + } if(events & WorkerEvtTxStop) break; if(events & WorkerEvtSamRx) { if(seader_uart->tx_len > 0) { diff --git a/uhf_status_label.c b/uhf_status_label.c index e4919fe..73c3cf4 100644 --- a/uhf_status_label.c +++ b/uhf_status_label.c @@ -10,16 +10,35 @@ static size_t seader_uhf_append_family( bool* wrote_any, const char* name, bool key_present) { - if(*wrote_any) { - pos += (size_t)snprintf(out + pos, out_size - pos, "/"); - } else { - pos += (size_t)snprintf(out + pos, out_size - pos, "UHF: "); - *wrote_any = true; + int written = 0; + + if(pos >= out_size) { + return out_size - 1U; + } + + if(*wrote_any) { + written = snprintf(out + pos, out_size - pos, "/"); + } else { + written = snprintf(out + pos, out_size - pos, "UHF: "); + *wrote_any = true; + } + pos += (size_t)written; + if(pos >= out_size) { + return out_size - 1U; + } + + written = snprintf(out + pos, out_size - pos, "%s", name); + pos += (size_t)written; + if(pos >= out_size) { + return out_size - 1U; } - pos += (size_t)snprintf(out + pos, out_size - pos, "%s", name); if(!key_present) { - pos += (size_t)snprintf(out + pos, out_size - pos, " [no key]"); + written = snprintf(out + pos, out_size - pos, " [no key]"); + pos += (size_t)written; + if(pos >= out_size) { + return out_size - 1U; + } } return pos; } diff --git a/wiegand_interface_fal/README.md b/wiegand_interface_fal/README.md new file mode 100644 index 0000000..a38e05e --- /dev/null +++ b/wiegand_interface_fal/README.md @@ -0,0 +1,9 @@ +# Seader embedded Wiegand plugin sources + +This directory is part of the main Seader repository. + +It contains the embedded Wiegand `.fal` plugin sources used by Seader: +- `wiegand.c` +- `interface.h` + +The Wiegand plugin source path in `application.fam` must point at `wiegand_interface_fal/wiegand.c`. diff --git a/wiegand_interface_fal/interface.h b/wiegand_interface_fal/interface.h new file mode 100644 index 0000000..58021f9 --- /dev/null +++ b/wiegand_interface_fal/interface.h @@ -0,0 +1,20 @@ +/** + * @file plugin_interface.h + * @brief Example plugin interface. + * + * Common interface between a plugin and host application + */ +#pragma once + +#include +#include +#include + +#define PLUGIN_APP_ID "plugin_wiegand" +#define PLUGIN_API_VERSION 1 + +typedef struct { + const char* name; + int (*count)(uint8_t, uint64_t); + void (*description)(uint8_t, uint64_t, size_t, FuriString*); +} PluginWiegand; diff --git a/wiegand_interface_fal/wiegand.c b/wiegand_interface_fal/wiegand.c new file mode 100644 index 0000000..a362f2b --- /dev/null +++ b/wiegand_interface_fal/wiegand.c @@ -0,0 +1,228 @@ + +#include "interface.h" + +#include +#include + +/* + * Huge thanks to the proxmark codebase: + * https://github.com/RfidResearchGroup/proxmark3/blob/master/client/src/wiegand_formats.c + */ + +// Structure for packed wiegand messages +// Always align lowest value (last transmitted) bit to ordinal position 0 (lowest valued bit bottom) +typedef struct { + uint8_t Length; // Number of encoded bits in wiegand message (excluding headers and preamble) + uint32_t Top; // Bits in x<<64 positions + uint32_t Mid; // Bits in x<<32 positions + uint32_t Bot; // Lowest ordinal positions +} wiegand_message_t; + +static inline uint8_t oddparity32(uint32_t x) { + return bit_lib_test_parity_32(x, BitLibParityOdd); +} + +static inline uint8_t evenparity32(uint32_t x) { + return bit_lib_test_parity_32(x, BitLibParityEven); +} + +uint8_t get_bit_by_position(wiegand_message_t* data, uint8_t pos) { + if(pos >= data->Length) return false; + pos = (data->Length - pos) - + 1; // invert ordering; Indexing goes from 0 to 1. Subtract 1 for weight of bit. + uint8_t result = 0; + if(pos > 95) + result = 0; + else if(pos > 63) + result = (data->Top >> (pos - 64)) & 1; + else if(pos > 31) + result = (data->Mid >> (pos - 32)) & 1; + else + result = (data->Bot >> pos) & 1; + return result; +} + +uint64_t get_linear_field(wiegand_message_t* data, uint8_t firstBit, uint8_t length) { + uint64_t result = 0; + for(uint8_t i = 0; i < length; i++) { + result = (result << 1) | get_bit_by_position(data, firstBit + i); + } + return result; +} + +static int wiegand_C1k35s_parse(uint8_t bit_length, uint64_t bits, FuriString* description) { + wiegand_message_t value; + value.Length = bit_length; + value.Mid = bits >> 32; + value.Bot = bits; + wiegand_message_t* packed = &value; + + if(packed->Length != 35) return false; // Wrong length? Stop here. + + uint32_t cn = (packed->Bot >> 1) & 0x000FFFFF; + uint32_t fc = ((packed->Mid & 1) << 11) | ((packed->Bot >> 21)); + bool valid = (evenparity32((packed->Mid & 0x1) ^ (packed->Bot & 0xB6DB6DB6)) == + ((packed->Mid >> 1) & 1)) && + (oddparity32((packed->Mid & 0x3) ^ (packed->Bot & 0x6DB6DB6C)) == + ((packed->Bot >> 0) & 1)) && + (oddparity32((packed->Mid & 0x3) ^ (packed->Bot & 0xFFFFFFFF)) == + ((packed->Mid >> 2) & 1)); + + if(valid) { + furi_string_cat_printf(description, "C1k35s\nFC: %ld CN: %ld\n", fc, cn); + return 1; + } else { + FURI_LOG_D(PLUGIN_APP_ID, "C1k35s invalid"); + } + + return 0; +} + +static int wiegand_h10301_parse(uint8_t bit_length, uint64_t bits, FuriString* description) { + if(bit_length != 26) { + return 0; + } + + //E XXXX XXXX XXXX + //XXXX XXXX XXXX O + uint32_t eBitMask = 0x02000000; + uint32_t oBitMask = 0x00000001; + uint32_t eParityMask = 0x01FFE000; + uint32_t oParityMask = 0x00001FFE; + uint8_t eBit = (eBitMask & bits) >> 25; + uint8_t oBit = (oBitMask & bits) >> 0; + + bool eParity = bit_lib_test_parity_32((bits & eParityMask) >> 13, BitLibParityEven) == + (eBit == 1); + bool oParity = bit_lib_test_parity_32((bits & oParityMask) >> 1, BitLibParityOdd) == + (oBit == 1); + + FURI_LOG_D( + PLUGIN_APP_ID, + "eBit: %d, oBit: %d, eParity: %d, oParity: %d", + eBit, + oBit, + eParity, + oParity); + + if(eParity && oParity) { + uint32_t cnMask = 0x1FFFE; + uint16_t cn = ((bits & cnMask) >> 1); + + uint32_t fcMask = 0x1FE0000; + uint16_t fc = ((bits & fcMask) >> 17); + + furi_string_cat_printf(description, "H10301\nFC: %d CN: %d\n", fc, cn); + return 1; + } else { + FURI_LOG_D(PLUGIN_APP_ID, "H10301 invalid"); + } + + return 0; +} + +static int wiegand_H10304_parse(uint8_t bit_length, uint64_t bits, FuriString* description) { + wiegand_message_t value; + value.Length = bit_length; + value.Mid = bits >> 32; + value.Bot = bits; + wiegand_message_t* packed = &value; + + if(packed->Length != 37) return false; // Wrong length? Stop here. + + uint32_t fc = get_linear_field(packed, 1, 16); + uint32_t cn = get_linear_field(packed, 17, 19); + bool valid = + (get_bit_by_position(packed, 0) == evenparity32(get_linear_field(packed, 1, 18))) && + (get_bit_by_position(packed, 36) == oddparity32(get_linear_field(packed, 18, 18))); + + if(valid) { + furi_string_cat_printf(description, "H10304\nFC: %ld CN: %ld\n", fc, cn); + return 1; + } else { + FURI_LOG_D(PLUGIN_APP_ID, "H10304 invalid"); + } + + return 0; +} + +static int wiegand_H10302_parse(uint8_t bit_length, uint64_t bits, FuriString* description) { + wiegand_message_t value; + value.Length = bit_length; + value.Mid = bits >> 32; + value.Bot = bits; + wiegand_message_t* packed = &value; + + if(packed->Length != 37) return false; // Wrong length? Stop here. + + uint64_t cn = get_linear_field(packed, 1, 35); + bool valid = + (get_bit_by_position(packed, 0) == evenparity32(get_linear_field(packed, 1, 18))) && + (get_bit_by_position(packed, 36) == oddparity32(get_linear_field(packed, 18, 18))); + + if(valid) { + furi_string_cat_printf(description, "H10302\nCN: %lld\n", cn); + return 1; + } else { + FURI_LOG_D(PLUGIN_APP_ID, "H10302 invalid"); + } + + return 0; +} + +static int wiegand_format_count(uint8_t bit_length, uint64_t bits) { + UNUSED(bit_length); + UNUSED(bits); + int count = 0; + FuriString* ignore = furi_string_alloc(); + + // NOTE: Always update the `total` and add to the wiegand_format_description function + // TODO: Make this into a function pointer array + count += wiegand_h10301_parse(bit_length, bits, ignore); + count += wiegand_C1k35s_parse(bit_length, bits, ignore); + count += wiegand_H10302_parse(bit_length, bits, ignore); + count += wiegand_H10304_parse(bit_length, bits, ignore); + int total = 4; + + furi_string_free(ignore); + + FURI_LOG_I(PLUGIN_APP_ID, "count: %i/%i", count, total); + return count; +} + +static void wiegand_format_description( + uint8_t bit_length, + uint64_t bits, + size_t index, + FuriString* description) { + FURI_LOG_I(PLUGIN_APP_ID, "description %d", index); + + // Turns out I did this wrong and trying to use the index means the results get repeated. Instead, just return the results for index == 0 + if(index != 0) { + return; + } + + wiegand_h10301_parse(bit_length, bits, description); + wiegand_C1k35s_parse(bit_length, bits, description); + wiegand_H10302_parse(bit_length, bits, description); + wiegand_H10304_parse(bit_length, bits, description); +} + +/* Actual implementation of app<>plugin interface */ +static const PluginWiegand plugin_wiegand = { + .name = "Plugin Wiegand", + .count = &wiegand_format_count, + .description = &wiegand_format_description, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor plugin_wiegand_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &plugin_wiegand, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* plugin_wiegand_ep(void) { + return &plugin_wiegand_descriptor; +}