From e7634d7563cd807caf5f522806e83c1d7393340b Mon Sep 17 00:00:00 2001 From: Nathan N Date: Mon, 29 Sep 2025 13:05:06 -0400 Subject: [PATCH 1/3] NFC: Ultralight C App Key Management, Dictionary Attack (#4271) * Upstream Ultralight C dictionary attack (squashed) * linter: formatting * unit_tests: nfc: split nfc data to named var * Fix mf_ultralight_poller_sync_read_card * linter: suppressed warnings on TODOs --------- Co-authored-by: hedger Co-authored-by: hedger --- .../debug/unit_tests/tests/nfc/nfc_test.c | 6 +- .../mf_ultralight/mf_ultralight.c | 51 ++- applications/main/nfc/nfc_app_i.h | 16 +- .../nfc/assets/mf_ultralight_c_dict.nfc | 55 ++++ .../main/nfc/scenes/nfc_scene_config.h | 7 + .../nfc/scenes/nfc_scene_delete_success.c | 4 + .../main/nfc/scenes/nfc_scene_extra_actions.c | 10 + .../nfc/scenes/nfc_scene_mf_classic_keys.c | 2 - .../scenes/nfc_scene_mf_classic_keys_add.c | 2 +- .../nfc_scene_mf_ultralight_c_dict_attack.c | 238 ++++++++++++++ .../scenes/nfc_scene_mf_ultralight_c_keys.c | 96 ++++++ .../nfc_scene_mf_ultralight_c_keys_add.c | 63 ++++ .../nfc_scene_mf_ultralight_c_keys_delete.c | 108 +++++++ .../nfc_scene_mf_ultralight_c_keys_list.c | 66 ++++ ...cene_mf_ultralight_c_keys_warn_duplicate.c | 49 +++ .../main/nfc/scenes/nfc_scene_save_success.c | 4 + applications/main/nfc/views/dict_attack.c | 299 +++++++++++------- applications/main/nfc/views/dict_attack.h | 13 + .../services/dolphin/helpers/dolphin_deed.c | 2 +- .../services/dolphin/helpers/dolphin_deed.h | 2 +- documentation/file_formats/NfcFileFormats.md | 26 +- .../mf_ultralight/mf_ultralight_poller.c | 94 +++++- .../mf_ultralight/mf_ultralight_poller.h | 21 +- .../mf_ultralight/mf_ultralight_poller_i.c | 6 +- lib/toolbox/keys_dict.c | 19 +- 25 files changed, 1094 insertions(+), 165 deletions(-) create mode 100644 applications/main/nfc/resources/nfc/assets/mf_ultralight_c_dict.nfc create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_dict_attack.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_add.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_delete.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_list.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_warn_duplicate.c diff --git a/applications/debug/unit_tests/tests/nfc/nfc_test.c b/applications/debug/unit_tests/tests/nfc/nfc_test.c index e028b8041..caeb3b9d7 100644 --- a/applications/debug/unit_tests/tests/nfc/nfc_test.c +++ b/applications/debug/unit_tests/tests/nfc/nfc_test.c @@ -268,9 +268,9 @@ static void mf_ultralight_reader_test(const char* path) { nfc_listener_stop(mfu_listener); nfc_listener_free(mfu_listener); - mu_assert( - mf_ultralight_is_equal(mfu_data, nfc_device_get_data(nfc_device, NfcProtocolMfUltralight)), - "Data not matches"); + MfUltralightData* mfu_other_data = + (MfUltralightData*)nfc_device_get_data(nfc_device, NfcProtocolMfUltralight); + mu_assert(mf_ultralight_is_equal(mfu_data, mfu_other_data), "Data mismatch"); mf_ultralight_free(mfu_data); nfc_device_free(nfc_device); diff --git a/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c b/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c index 3adf2a1f5..5ed564897 100644 --- a/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c +++ b/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c @@ -15,6 +15,7 @@ enum { SubmenuIndexUnlockByReader, SubmenuIndexUnlockByPassword, SubmenuIndexWrite, + SubmenuIndexDictAttack }; enum { @@ -150,7 +151,15 @@ static NfcCommand } if(!mf_ultralight_event->data->auth_context.skip_auth) { mf_ultralight_event->data->auth_context.password = instance->mf_ul_auth->password; - mf_ultralight_event->data->auth_context.tdes_key = instance->mf_ul_auth->tdes_key; + + // Only set tdes_key for Manual/Reader auth types, not for dictionary attacks + if(instance->mf_ul_auth->type == MfUltralightAuthTypeManual || + instance->mf_ul_auth->type == MfUltralightAuthTypeReader) { + mf_ultralight_event->data->key_request_data.key = instance->mf_ul_auth->tdes_key; + mf_ultralight_event->data->key_request_data.key_provided = true; + } else { + mf_ultralight_event->data->key_request_data.key_provided = false; + } } } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeAuthSuccess) { instance->mf_ul_auth->pack = mf_ultralight_event->data->auth_context.pack; @@ -166,15 +175,31 @@ static void nfc_scene_read_on_enter_mf_ultralight(NfcApp* instance) { bool nfc_scene_read_on_event_mf_ultralight(NfcApp* instance, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { - if(event.event == NfcCustomEventCardDetected) { - nfc_unlock_helper_card_detected_handler(instance); - } else if(event.event == NfcCustomEventPollerIncomplete) { - notification_message(instance->notifications, &sequence_semi_success); + if(event.event == NfcCustomEventPollerSuccess) { + notification_message(instance->notifications, &sequence_success); scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); dolphin_deed(DolphinDeedNfcReadSuccess); + return true; + } else if(event.event == NfcCustomEventPollerIncomplete) { + const MfUltralightData* data = + nfc_device_get_data(instance->nfc_device, NfcProtocolMfUltralight); + if(data->type == MfUltralightTypeMfulC && + instance->mf_ul_auth->type == MfUltralightAuthTypeNone) { + // Start dict attack for MFUL C cards only if no specific auth was attempted + scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCDictAttack); + } else { + if(data->pages_read == data->pages_total) { + notification_message(instance->notifications, &sequence_success); + } else { + notification_message(instance->notifications, &sequence_semi_success); + } + scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); + dolphin_deed(DolphinDeedNfcReadSuccess); + } + return true; } } - return true; + return false; } static void nfc_scene_read_and_saved_menu_on_enter_mf_ultralight(NfcApp* instance) { @@ -190,6 +215,14 @@ static void nfc_scene_read_and_saved_menu_on_enter_mf_ultralight(NfcApp* instanc SubmenuIndexUnlock, nfc_protocol_support_common_submenu_callback, instance); + if(data->type == MfUltralightTypeMfulC) { + submenu_add_item( + submenu, + "Unlock with Dictionary", + SubmenuIndexDictAttack, + nfc_protocol_support_common_submenu_callback, + instance); + } } else if( data->type == MfUltralightTypeNTAG213 || data->type == MfUltralightTypeNTAG215 || data->type == MfUltralightTypeNTAG216 || data->type == MfUltralightTypeUL11 || @@ -258,6 +291,12 @@ static bool nfc_scene_read_and_saved_menu_on_event_mf_ultralight( } else if(event.event == SubmenuIndexCommonEdit) { scene_manager_next_scene(instance->scene_manager, NfcSceneSetUid); consumed = true; + } else if(event.event == SubmenuIndexDictAttack) { + if(!scene_manager_search_and_switch_to_previous_scene( + instance->scene_manager, NfcSceneMfUltralightCDictAttack)) { + scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCDictAttack); + } + consumed = true; } } return consumed; diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index 920127fef..f7af85ea8 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -64,7 +65,7 @@ #define NFC_NAME_SIZE 22 #define NFC_TEXT_STORE_SIZE 128 -#define NFC_BYTE_INPUT_STORE_SIZE 10 +#define NFC_BYTE_INPUT_STORE_SIZE 16 #define NFC_LOG_SIZE_MAX (1024) #define NFC_APP_FOLDER EXT_PATH("nfc") #define NFC_APP_EXTENSION ".nfc" @@ -80,6 +81,10 @@ #define NFC_APP_MF_CLASSIC_DICT_SYSTEM_PATH (NFC_APP_FOLDER "/assets/mf_classic_dict.nfc") #define NFC_APP_MF_CLASSIC_DICT_SYSTEM_NESTED_PATH \ (NFC_APP_FOLDER "/assets/mf_classic_dict_nested.nfc") +#define NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH \ + (NFC_APP_FOLDER "/assets/mf_ultralight_c_dict_user.nfc") +#define NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH \ + (NFC_APP_FOLDER "/assets/mf_ultralight_c_dict.nfc") #define NFC_MFKEY32_APP_PATH (EXT_PATH("apps/NFC/mfkey.fap")) @@ -107,6 +112,14 @@ typedef struct { bool enhanced_dict; } NfcMfClassicDictAttackContext; +typedef struct { + KeysDict* dict; + bool auth_success; + bool is_card_present; + size_t dict_keys_total; + size_t dict_keys_current; +} NfcMfUltralightCDictContext; + struct NfcApp { DialogsApp* dialogs; Storage* storage; @@ -145,6 +158,7 @@ struct NfcApp { MfUltralightAuth* mf_ul_auth; SlixUnlock* slix_unlock; NfcMfClassicDictAttackContext nfc_dict_context; + NfcMfUltralightCDictContext mf_ultralight_c_dict_context; Mfkey32Logger* mfkey32_logger; MfUserDict* mf_user_dict; MfClassicKeyCache* mfc_key_cache; diff --git a/applications/main/nfc/resources/nfc/assets/mf_ultralight_c_dict.nfc b/applications/main/nfc/resources/nfc/assets/mf_ultralight_c_dict.nfc new file mode 100644 index 000000000..fa5dbb1fb --- /dev/null +++ b/applications/main/nfc/resources/nfc/assets/mf_ultralight_c_dict.nfc @@ -0,0 +1,55 @@ +# Sample Key (BREAKMEIFYOUCAN!) +425245414B4D454946594F5543414E21 +# Hexadecimal-Reversed Sample Key +12E4143455F495649454D4B414542524 +# Byte-Reversed Sample Key (!NACUOYFIEMKAERB) +214E4143554F594649454D4B41455242 +# Semnox Key (IEMKAERB!NACUOY ) +49454D4B41455242214E4143554F5900 +# Modified Semnox Key (IEMKAERB!NACUOYF) +49454D4B41455242214E4143554F5946 + +# Mix of Proxmark and ChameleonMiniLiveDebugger +00000000000000000000000000000000 +000102030405060708090A0B0C0D0E0F +01010101010101010101010101010101 +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +00112233445566778899AABBCCDDEEFF +47454D5850524553534F53414D504C45 +79702553797025537970255379702553 +4E617468616E2E4C6920546564647920 +43464F494D48504E4C4359454E528841 +6AC292FAA1315B4D858AB3A3D7D5933A +404142434445464748494A4B4C4D4E4F +2B7E151628AED2A6ABF7158809CF4F3C +FBEED618357133667C85E08F7236A8DE +F7DDAC306AE266CCF90BC11EE46D513B +54686973206973206D79206B65792020 +A0A1A2A3A4A5A6A7A0A1A2A3A4A5A6A7 +B0B1B2B3B4B5B6B7B0B1B2B3B4B5B6B7 +B0B1B2B3B4B5B6B7B8B9BABBBCBDBEBF +D3F7D3F7D3F7D3F7D3F7D3F7D3F7D3F7 +11111111111111111111111111111111 +22222222222222222222222222222222 +33333333333333333333333333333333 +44444444444444444444444444444444 +55555555555555555555555555555555 +66666666666666666666666666666666 +77777777777777777777777777777777 +88888888888888888888888888888888 +99999999999999999999999999999999 +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD +EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE +0102030405060708090A0B0C0D0E0F10 +00010203040506070809101112131415 +01020304050607080910111213141516 +16151413121110090807060504030201 +15141312111009080706050403020100 +0F0E0D0C0B0A09080706050403020100 +100F0E0D0C0B0A090807060504030201 +303132333435363738393A3B3C3D3E3F +9CABF398358405AE2F0E2B3D31C99A8A +605F5E5D5C5B5A59605F5E5D5C5B5A59 diff --git a/applications/main/nfc/scenes/nfc_scene_config.h b/applications/main/nfc/scenes/nfc_scene_config.h index 83c8ffeed..399d59b92 100644 --- a/applications/main/nfc/scenes/nfc_scene_config.h +++ b/applications/main/nfc/scenes/nfc_scene_config.h @@ -25,6 +25,7 @@ ADD_SCENE(nfc, retry_confirm, RetryConfirm) ADD_SCENE(nfc, exit_confirm, ExitConfirm) ADD_SCENE(nfc, save_confirm, SaveConfirm) +ADD_SCENE(nfc, mf_ultralight_c_dict_attack, MfUltralightCDictAttack) ADD_SCENE(nfc, mf_ultralight_write, MfUltralightWrite) ADD_SCENE(nfc, mf_ultralight_write_success, MfUltralightWriteSuccess) ADD_SCENE(nfc, mf_ultralight_write_fail, MfUltralightWriteFail) @@ -57,6 +58,12 @@ ADD_SCENE(nfc, mf_classic_keys_delete, MfClassicKeysDelete) ADD_SCENE(nfc, mf_classic_keys_add, MfClassicKeysAdd) ADD_SCENE(nfc, mf_classic_keys_warn_duplicate, MfClassicKeysWarnDuplicate) +ADD_SCENE(nfc, mf_ultralight_c_keys, MfUltralightCKeys) +ADD_SCENE(nfc, mf_ultralight_c_keys_list, MfUltralightCKeysList) +ADD_SCENE(nfc, mf_ultralight_c_keys_delete, MfUltralightCKeysDelete) +ADD_SCENE(nfc, mf_ultralight_c_keys_add, MfUltralightCKeysAdd) +ADD_SCENE(nfc, mf_ultralight_c_keys_warn_duplicate, MfUltralightCKeysWarnDuplicate) + ADD_SCENE(nfc, set_type, SetType) ADD_SCENE(nfc, set_sak, SetSak) ADD_SCENE(nfc, set_atqa, SetAtqa) diff --git a/applications/main/nfc/scenes/nfc_scene_delete_success.c b/applications/main/nfc/scenes/nfc_scene_delete_success.c index d41e52549..d8308addd 100644 --- a/applications/main/nfc/scenes/nfc_scene_delete_success.c +++ b/applications/main/nfc/scenes/nfc_scene_delete_success.c @@ -28,6 +28,10 @@ bool nfc_scene_delete_success_on_event(void* context, SceneManagerEvent event) { if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneMfClassicKeys)) { consumed = scene_manager_search_and_switch_to_previous_scene( nfc->scene_manager, NfcSceneMfClassicKeys); + } else if(scene_manager_has_previous_scene( + nfc->scene_manager, NfcSceneMfUltralightCKeys)) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneMfUltralightCKeys); } else { consumed = scene_manager_search_and_switch_to_previous_scene( nfc->scene_manager, NfcSceneFileSelect); diff --git a/applications/main/nfc/scenes/nfc_scene_extra_actions.c b/applications/main/nfc/scenes/nfc_scene_extra_actions.c index 2943c0c55..6720b2d7b 100644 --- a/applications/main/nfc/scenes/nfc_scene_extra_actions.c +++ b/applications/main/nfc/scenes/nfc_scene_extra_actions.c @@ -3,6 +3,7 @@ enum SubmenuIndex { SubmenuIndexReadCardType, SubmenuIndexMfClassicKeys, + SubmenuIndexMfUltralightCKeys, SubmenuIndexMfUltralightUnlock, SubmenuIndexSlixUnlock, }; @@ -29,6 +30,12 @@ void nfc_scene_extra_actions_on_enter(void* context) { SubmenuIndexMfClassicKeys, nfc_scene_extra_actions_submenu_callback, instance); + submenu_add_item( + submenu, + "MIFARE Ultralight C Keys", + SubmenuIndexMfUltralightCKeys, + nfc_scene_extra_actions_submenu_callback, + instance); submenu_add_item( submenu, "Unlock NTAG/Ultralight", @@ -54,6 +61,9 @@ bool nfc_scene_extra_actions_on_event(void* context, SceneManagerEvent event) { if(event.event == SubmenuIndexMfClassicKeys) { scene_manager_next_scene(instance->scene_manager, NfcSceneMfClassicKeys); consumed = true; + } else if(event.event == SubmenuIndexMfUltralightCKeys) { + scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCKeys); + consumed = true; } else if(event.event == SubmenuIndexMfUltralightUnlock) { mf_ultralight_auth_reset(instance->mf_ul_auth); scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightUnlockMenu); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_keys.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_keys.c index eaa054149..7ee203285 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_keys.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_keys.c @@ -1,7 +1,5 @@ #include "../nfc_app_i.h" -#define NFC_SCENE_MF_CLASSIC_KEYS_MAX (100) - void nfc_scene_mf_classic_keys_widget_callback(GuiButtonType result, InputType type, void* context) { NfcApp* instance = context; if(type == InputTypeShort) { diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_keys_add.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_keys_add.c index a963f44ac..131b5e230 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_keys_add.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_keys_add.c @@ -39,7 +39,7 @@ bool nfc_scene_mf_classic_keys_add_on_event(void* context, SceneManagerEvent eve instance->scene_manager, NfcSceneMfClassicKeysWarnDuplicate); } else if(keys_dict_add_key(dict, key.data, sizeof(MfClassicKey))) { scene_manager_next_scene(instance->scene_manager, NfcSceneSaveSuccess); - dolphin_deed(DolphinDeedNfcMfcAdd); + dolphin_deed(DolphinDeedNfcKeyAdd); } else { scene_manager_previous_scene(instance->scene_manager); } diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_dict_attack.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_dict_attack.c new file mode 100644 index 000000000..843261142 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_dict_attack.c @@ -0,0 +1,238 @@ +#include "../nfc_app_i.h" +#include + +#define TAG "NfcMfUlCDictAttack" + +// TODO: Support card_detected properly -nofl + +enum { + DictAttackStateUserDictInProgress, + DictAttackStateSystemDictInProgress, +}; + +NfcCommand nfc_mf_ultralight_c_dict_attack_worker_callback(NfcGenericEvent event, void* context) { + furi_assert(context); + furi_assert(event.event_data); + furi_assert(event.protocol == NfcProtocolMfUltralight); + NfcCommand command = NfcCommandContinue; + NfcApp* instance = context; + MfUltralightPollerEvent* poller_event = event.event_data; + + if(poller_event->type == MfUltralightPollerEventTypeRequestMode) { + poller_event->data->poller_mode = MfUltralightPollerModeDictAttack; + command = NfcCommandContinue; + } else if(poller_event->type == MfUltralightPollerEventTypeRequestKey) { + MfUltralightC3DesAuthKey key = {}; + if(keys_dict_get_next_key( + instance->mf_ultralight_c_dict_context.dict, + key.data, + sizeof(MfUltralightC3DesAuthKey))) { + poller_event->data->key_request_data.key = key; + poller_event->data->key_request_data.key_provided = true; + instance->mf_ultralight_c_dict_context.dict_keys_current++; + + if(instance->mf_ultralight_c_dict_context.dict_keys_current % 10 == 0) { + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventDictAttackDataUpdate); + } + } else { + poller_event->data->key_request_data.key_provided = false; + } + } else if(poller_event->type == MfUltralightPollerEventTypeReadSuccess) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolMfUltralight, nfc_poller_get_data(instance->poller)); + // Check if this is a successful authentication by looking at the poller's auth context + const MfUltralightData* data = nfc_poller_get_data(instance->poller); + + // Update page information + dict_attack_set_pages_read(instance->dict_attack, data->pages_read); + dict_attack_set_pages_total(instance->dict_attack, data->pages_total); + + if(data->pages_read == data->pages_total) { + // Full read indicates successful authentication in dict attack mode + instance->mf_ultralight_c_dict_context.auth_success = true; + dict_attack_set_key_found(instance->dict_attack, true); + } + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventDictAttackComplete); + command = NfcCommandStop; + } + return command; +} + +void nfc_scene_mf_ultralight_c_dict_attack_dict_attack_result_callback( + DictAttackEvent event, + void* context) { + furi_assert(context); + NfcApp* instance = context; + if(event == DictAttackEventSkipPressed) { + view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventDictAttackSkip); + } +} + +void nfc_scene_mf_ultralight_c_dict_attack_prepare_view(NfcApp* instance) { + uint32_t state = + scene_manager_get_scene_state(instance->scene_manager, NfcSceneMfUltralightCDictAttack); + + // Set attack type to Ultralight C + dict_attack_set_type(instance->dict_attack, DictAttackTypeMfUltralightC); + + if(state == DictAttackStateUserDictInProgress) { + do { + if(!keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) { + state = DictAttackStateSystemDictInProgress; + break; + } + instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH, + KeysDictModeOpenAlways, + sizeof(MfUltralightC3DesAuthKey)); + if(keys_dict_get_total_keys(instance->mf_ultralight_c_dict_context.dict) == 0) { + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + state = DictAttackStateSystemDictInProgress; + break; + } + dict_attack_set_header(instance->dict_attack, "MFUL C User Dictionary"); + } while(false); + } + if(state == DictAttackStateSystemDictInProgress) { + instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH, + KeysDictModeOpenExisting, + sizeof(MfUltralightC3DesAuthKey)); + dict_attack_set_header(instance->dict_attack, "MFUL C System Dictionary"); + } + + instance->mf_ultralight_c_dict_context.dict_keys_total = + keys_dict_get_total_keys(instance->mf_ultralight_c_dict_context.dict); + dict_attack_set_total_dict_keys( + instance->dict_attack, instance->mf_ultralight_c_dict_context.dict_keys_total); + instance->mf_ultralight_c_dict_context.dict_keys_current = 0; + dict_attack_set_current_dict_key( + instance->dict_attack, instance->mf_ultralight_c_dict_context.dict_keys_current); + + // Set initial Ultralight C specific values + dict_attack_set_key_found(instance->dict_attack, false); + dict_attack_set_pages_total(instance->dict_attack, 48); // Ultralight C page count + dict_attack_set_pages_read(instance->dict_attack, 0); + + dict_attack_set_callback( + instance->dict_attack, + nfc_scene_mf_ultralight_c_dict_attack_dict_attack_result_callback, + instance); + scene_manager_set_scene_state(instance->scene_manager, NfcSceneMfUltralightCDictAttack, state); +} + +void nfc_scene_mf_ultralight_c_dict_attack_on_enter(void* context) { + NfcApp* instance = context; + + scene_manager_set_scene_state( + instance->scene_manager, + NfcSceneMfUltralightCDictAttack, + DictAttackStateUserDictInProgress); + nfc_scene_mf_ultralight_c_dict_attack_prepare_view(instance); + + // Setup and start worker + instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfUltralight); + nfc_poller_start(instance->poller, nfc_mf_ultralight_c_dict_attack_worker_callback, instance); + + dict_attack_set_card_state(instance->dict_attack, true); + view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewDictAttack); + nfc_blink_read_start(instance); +} + +void nfc_scene_mf_ul_c_dict_attack_update_view(NfcApp* instance) { + dict_attack_set_card_state( + instance->dict_attack, instance->mf_ultralight_c_dict_context.is_card_present); + dict_attack_set_current_dict_key( + instance->dict_attack, instance->mf_ultralight_c_dict_context.dict_keys_current); +} + +bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerEvent event) { + NfcApp* instance = context; + bool consumed = false; + + uint32_t state = + scene_manager_get_scene_state(instance->scene_manager, NfcSceneMfUltralightCDictAttack); + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventDictAttackComplete) { + if(state == DictAttackStateUserDictInProgress) { + if(instance->mf_ultralight_c_dict_context.auth_success) { + notification_message(instance->notifications, &sequence_success); + scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); + dolphin_deed(DolphinDeedNfcReadSuccess); + consumed = true; + } else { + nfc_poller_stop(instance->poller); + nfc_poller_free(instance->poller); + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + scene_manager_set_scene_state( + instance->scene_manager, + NfcSceneMfUltralightCDictAttack, + DictAttackStateSystemDictInProgress); + nfc_scene_mf_ultralight_c_dict_attack_prepare_view(instance); + instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfUltralight); + nfc_poller_start( + instance->poller, + nfc_mf_ultralight_c_dict_attack_worker_callback, + instance); + consumed = true; + } + } else { + // Could check if card is fully read here like MFC dict attack, but found key means fully read + if(instance->mf_ultralight_c_dict_context.auth_success) { + notification_message(instance->notifications, &sequence_success); + } else { + notification_message(instance->notifications, &sequence_semi_success); + } + scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); + dolphin_deed(DolphinDeedNfcReadSuccess); + consumed = true; + } + } else if(event.event == NfcCustomEventDictAttackDataUpdate) { + dict_attack_set_current_dict_key( + instance->dict_attack, instance->mf_ultralight_c_dict_context.dict_keys_current); + consumed = true; + } else if(event.event == NfcCustomEventDictAttackSkip) { + if(state == DictAttackStateUserDictInProgress) { + nfc_poller_stop(instance->poller); + nfc_poller_free(instance->poller); + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + scene_manager_set_scene_state( + instance->scene_manager, + NfcSceneMfUltralightCDictAttack, + DictAttackStateSystemDictInProgress); + nfc_scene_mf_ultralight_c_dict_attack_prepare_view(instance); + instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfUltralight); + nfc_poller_start( + instance->poller, nfc_mf_ultralight_c_dict_attack_worker_callback, instance); + } else { + notification_message(instance->notifications, &sequence_semi_success); + scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); + dolphin_deed(DolphinDeedNfcReadSuccess); + } + consumed = true; + } + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_next_scene(instance->scene_manager, NfcSceneExitConfirm); + consumed = true; + } + return consumed; +} + +void nfc_scene_mf_ultralight_c_dict_attack_on_exit(void* context) { + NfcApp* instance = context; + nfc_poller_stop(instance->poller); + nfc_poller_free(instance->poller); + scene_manager_set_scene_state( + instance->scene_manager, + NfcSceneMfUltralightCDictAttack, + DictAttackStateUserDictInProgress); + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + instance->mf_ultralight_c_dict_context.dict_keys_total = 0; + instance->mf_ultralight_c_dict_context.dict_keys_current = 0; + instance->mf_ultralight_c_dict_context.auth_success = false; + instance->mf_ultralight_c_dict_context.is_card_present = false; + nfc_blink_stop(instance); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys.c new file mode 100644 index 000000000..9bf96f0b4 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys.c @@ -0,0 +1,96 @@ +#include "../nfc_app_i.h" + +void nfc_scene_mf_ultralight_c_keys_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + NfcApp* instance = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(instance->view_dispatcher, result); + } +} + +void nfc_scene_mf_ultralight_c_keys_on_enter(void* context) { + NfcApp* instance = context; + + // Load flipper dict keys total + uint32_t flipper_dict_keys_total = 0; + KeysDict* dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH, + KeysDictModeOpenExisting, + sizeof(MfUltralightC3DesAuthKey)); + flipper_dict_keys_total = keys_dict_get_total_keys(dict); + keys_dict_free(dict); + + // Load user dict keys total + uint32_t user_dict_keys_total = 0; + dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH, + KeysDictModeOpenAlways, + sizeof(MfUltralightC3DesAuthKey)); + user_dict_keys_total = keys_dict_get_total_keys(dict); + keys_dict_free(dict); + + FuriString* temp_str = furi_string_alloc(); + widget_add_string_element( + instance->widget, 0, 0, AlignLeft, AlignTop, FontPrimary, "MIFARE Ultralight C Keys"); + furi_string_printf(temp_str, "System dict: %lu", flipper_dict_keys_total); + widget_add_string_element( + instance->widget, + 0, + 20, + AlignLeft, + AlignTop, + FontSecondary, + furi_string_get_cstr(temp_str)); + furi_string_printf(temp_str, "User dict: %lu", user_dict_keys_total); + widget_add_string_element( + instance->widget, + 0, + 32, + AlignLeft, + AlignTop, + FontSecondary, + furi_string_get_cstr(temp_str)); + widget_add_icon_element(instance->widget, 87, 13, &I_Keychain_39x36); + widget_add_button_element( + instance->widget, + GuiButtonTypeCenter, + "Add", + nfc_scene_mf_ultralight_c_keys_widget_callback, + instance); + if(user_dict_keys_total > 0) { + widget_add_button_element( + instance->widget, + GuiButtonTypeRight, + "List", + nfc_scene_mf_ultralight_c_keys_widget_callback, + instance); + } + furi_string_free(temp_str); + + view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewWidget); +} + +bool nfc_scene_mf_ultralight_c_keys_on_event(void* context, SceneManagerEvent event) { + NfcApp* instance = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeCenter) { + scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCKeysAdd); + consumed = true; + } else if(event.event == GuiButtonTypeRight) { + scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCKeysList); + consumed = true; + } + } + + return consumed; +} + +void nfc_scene_mf_ultralight_c_keys_on_exit(void* context) { + NfcApp* instance = context; + + widget_reset(instance->widget); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_add.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_add.c new file mode 100644 index 000000000..63fdaed49 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_add.c @@ -0,0 +1,63 @@ +#include "../nfc_app_i.h" + +void nfc_scene_mf_ultralight_c_keys_add_byte_input_callback(void* context) { + NfcApp* instance = context; + + view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventByteInputDone); +} + +void nfc_scene_mf_ultralight_c_keys_add_on_enter(void* context) { + NfcApp* instance = context; + + // Setup view + ByteInput* byte_input = instance->byte_input; + byte_input_set_header_text(byte_input, "Enter the key in hex"); + byte_input_set_result_callback( + byte_input, + nfc_scene_mf_ultralight_c_keys_add_byte_input_callback, + NULL, + instance, + instance->byte_input_store, + sizeof(MfUltralightC3DesAuthKey)); + view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewByteInput); +} + +bool nfc_scene_mf_ultralight_c_keys_add_on_event(void* context, SceneManagerEvent event) { + NfcApp* instance = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventByteInputDone) { + // Add key to dict + KeysDict* dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH, + KeysDictModeOpenAlways, + sizeof(MfUltralightC3DesAuthKey)); + + MfUltralightC3DesAuthKey key = {}; + memcpy(key.data, instance->byte_input_store, sizeof(MfUltralightC3DesAuthKey)); + if(keys_dict_is_key_present(dict, key.data, sizeof(MfUltralightC3DesAuthKey))) { + scene_manager_next_scene( + instance->scene_manager, NfcSceneMfUltralightCKeysWarnDuplicate); + } else if(keys_dict_add_key(dict, key.data, sizeof(MfUltralightC3DesAuthKey))) { + scene_manager_next_scene(instance->scene_manager, NfcSceneSaveSuccess); + dolphin_deed(DolphinDeedNfcKeyAdd); + } else { + scene_manager_previous_scene(instance->scene_manager); + } + + keys_dict_free(dict); + consumed = true; + } + } + + return consumed; +} + +void nfc_scene_mf_ultralight_c_keys_add_on_exit(void* context) { + NfcApp* instance = context; + + // Clear view + byte_input_set_result_callback(instance->byte_input, NULL, NULL, NULL, NULL, 0); + byte_input_set_header_text(instance->byte_input, ""); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_delete.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_delete.c new file mode 100644 index 000000000..db2903939 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_delete.c @@ -0,0 +1,108 @@ +#include "../nfc_app_i.h" + +void nfc_scene_mf_ultralight_c_keys_delete_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + NfcApp* instance = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(instance->view_dispatcher, result); + } +} + +void nfc_scene_mf_ultralight_c_keys_delete_on_enter(void* context) { + NfcApp* instance = context; + + uint32_t key_index = + scene_manager_get_scene_state(instance->scene_manager, NfcSceneMfUltralightCKeysDelete); + FuriString* key_str = furi_string_alloc(); + + widget_add_string_element( + instance->widget, 64, 0, AlignCenter, AlignTop, FontPrimary, "Delete this key?"); + widget_add_button_element( + instance->widget, + GuiButtonTypeLeft, + "Cancel", + nfc_scene_mf_ultralight_c_keys_delete_widget_callback, + instance); + widget_add_button_element( + instance->widget, + GuiButtonTypeRight, + "Delete", + nfc_scene_mf_ultralight_c_keys_delete_widget_callback, + instance); + + KeysDict* mf_ultralight_c_user_dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH, + KeysDictModeOpenAlways, + sizeof(MfUltralightC3DesAuthKey)); + size_t dict_keys_num = keys_dict_get_total_keys(mf_ultralight_c_user_dict); + furi_assert(key_index < dict_keys_num); + MfUltralightC3DesAuthKey stack_key; + for(size_t i = 0; i < (key_index + 1); i++) { + bool key_loaded = keys_dict_get_next_key( + mf_ultralight_c_user_dict, stack_key.data, sizeof(MfUltralightC3DesAuthKey)); + furi_assert(key_loaded); + } + furi_string_reset(key_str); + for(size_t i = 0; i < sizeof(MfUltralightC3DesAuthKey); i++) { + furi_string_cat_printf(key_str, "%02X", stack_key.data[i]); + } + + widget_add_string_element( + instance->widget, + 64, + 32, + AlignCenter, + AlignCenter, + FontSecondary, + furi_string_get_cstr(key_str)); + + keys_dict_free(mf_ultralight_c_user_dict); + furi_string_free(key_str); + + view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewWidget); +} + +bool nfc_scene_mf_ultralight_c_keys_delete_on_event(void* context, SceneManagerEvent event) { + NfcApp* instance = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeRight) { + uint32_t key_index = scene_manager_get_scene_state( + instance->scene_manager, NfcSceneMfUltralightCKeysDelete); + KeysDict* mf_ultralight_c_user_dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH, + KeysDictModeOpenAlways, + sizeof(MfUltralightC3DesAuthKey)); + size_t dict_keys_num = keys_dict_get_total_keys(mf_ultralight_c_user_dict); + furi_assert(key_index < dict_keys_num); + MfUltralightC3DesAuthKey stack_key; + for(size_t i = 0; i < (key_index + 1); i++) { + bool key_loaded = keys_dict_get_next_key( + mf_ultralight_c_user_dict, stack_key.data, sizeof(MfUltralightC3DesAuthKey)); + furi_assert(key_loaded); + } + bool key_delete_success = keys_dict_delete_key( + mf_ultralight_c_user_dict, stack_key.data, sizeof(MfUltralightC3DesAuthKey)); + keys_dict_free(mf_ultralight_c_user_dict); + if(key_delete_success) { + scene_manager_next_scene(instance->scene_manager, NfcSceneDeleteSuccess); + } else { + scene_manager_previous_scene(instance->scene_manager); + } + } else if(event.event == GuiButtonTypeLeft) { + scene_manager_previous_scene(instance->scene_manager); + } + consumed = true; + } + + return consumed; +} + +void nfc_scene_mf_ultralight_c_keys_delete_on_exit(void* context) { + NfcApp* instance = context; + + widget_reset(instance->widget); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_list.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_list.c new file mode 100644 index 000000000..e2fda3aea --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_list.c @@ -0,0 +1,66 @@ +#include "../nfc_app_i.h" + +#define NFC_SCENE_MF_ULTRALIGHT_C_KEYS_LIST_MAX (100) + +void nfc_scene_mf_ultralight_c_keys_list_submenu_callback(void* context, uint32_t index) { + NfcApp* instance = context; + + view_dispatcher_send_custom_event(instance->view_dispatcher, index); +} + +void nfc_scene_mf_ultralight_c_keys_list_on_enter(void* context) { + NfcApp* instance = context; + + KeysDict* mf_ultralight_c_user_dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH, + KeysDictModeOpenAlways, + sizeof(MfUltralightC3DesAuthKey)); + + submenu_set_header(instance->submenu, "Select key to delete:"); + FuriString* temp_str = furi_string_alloc(); + + size_t dict_keys_num = keys_dict_get_total_keys(mf_ultralight_c_user_dict); + size_t keys_num = MIN((size_t)NFC_SCENE_MF_ULTRALIGHT_C_KEYS_LIST_MAX, dict_keys_num); + MfUltralightC3DesAuthKey stack_key; + + if(keys_num > 0) { + for(size_t i = 0; i < keys_num; i++) { + bool key_loaded = keys_dict_get_next_key( + mf_ultralight_c_user_dict, stack_key.data, sizeof(MfUltralightC3DesAuthKey)); + furi_assert(key_loaded); + furi_string_reset(temp_str); + for(size_t i = 0; i < sizeof(MfUltralightC3DesAuthKey); i++) { + furi_string_cat_printf(temp_str, "%02X", stack_key.data[i]); + } + submenu_add_item( + instance->submenu, + furi_string_get_cstr(temp_str), + i, + nfc_scene_mf_ultralight_c_keys_list_submenu_callback, + instance); + } + } + keys_dict_free(mf_ultralight_c_user_dict); + furi_string_free(temp_str); + + view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewMenu); +} + +bool nfc_scene_mf_ultralight_c_keys_list_on_event(void* context, SceneManagerEvent event) { + NfcApp* instance = context; + + bool consumed = false; + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state( + instance->scene_manager, NfcSceneMfUltralightCKeysDelete, event.event); + scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCKeysDelete); + } + + return consumed; +} + +void nfc_scene_mf_ultralight_c_keys_list_on_exit(void* context) { + NfcApp* instance = context; + + submenu_reset(instance->submenu); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_warn_duplicate.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_warn_duplicate.c new file mode 100644 index 000000000..c8881e5d4 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_keys_warn_duplicate.c @@ -0,0 +1,49 @@ +#include "../nfc_app_i.h" + +void nfc_scene_mf_ultralight_c_keys_warn_duplicate_popup_callback(void* context) { + NfcApp* instance = context; + + view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventViewExit); +} + +void nfc_scene_mf_ultralight_c_keys_warn_duplicate_on_enter(void* context) { + NfcApp* instance = context; + + // Setup view + Popup* popup = instance->popup; + popup_set_icon(popup, 83, 22, &I_WarningDolphinFlip_45x42); + popup_set_header(popup, "Key Already Exists!", 64, 3, AlignCenter, AlignTop); + popup_set_text( + popup, + "Please enter a\n" + "different key.", + 4, + 24, + AlignLeft, + AlignTop); + popup_set_timeout(popup, 1500); + popup_set_context(popup, instance); + popup_set_callback(popup, nfc_scene_mf_ultralight_c_keys_warn_duplicate_popup_callback); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewPopup); +} + +bool nfc_scene_mf_ultralight_c_keys_warn_duplicate_on_event(void* context, SceneManagerEvent event) { + NfcApp* instance = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventViewExit) { + consumed = scene_manager_search_and_switch_to_previous_scene( + instance->scene_manager, NfcSceneMfUltralightCKeysAdd); + } + } + + return consumed; +} + +void nfc_scene_mf_ultralight_c_keys_warn_duplicate_on_exit(void* context) { + NfcApp* instance = context; + + popup_reset(instance->popup); +} diff --git a/applications/main/nfc/scenes/nfc_scene_save_success.c b/applications/main/nfc/scenes/nfc_scene_save_success.c index 5f812ba9c..06999fe9f 100644 --- a/applications/main/nfc/scenes/nfc_scene_save_success.c +++ b/applications/main/nfc/scenes/nfc_scene_save_success.c @@ -28,6 +28,10 @@ bool nfc_scene_save_success_on_event(void* context, SceneManagerEvent event) { if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneMfClassicKeys)) { consumed = scene_manager_search_and_switch_to_previous_scene( nfc->scene_manager, NfcSceneMfClassicKeys); + } else if(scene_manager_has_previous_scene( + nfc->scene_manager, NfcSceneMfUltralightCKeys)) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneMfUltralightCKeys); } else if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSaveConfirm)) { NfcSceneSaveConfirmState scene_state = scene_manager_get_scene_state(nfc->scene_manager, NfcSceneSaveConfirm); diff --git a/applications/main/nfc/views/dict_attack.c b/applications/main/nfc/views/dict_attack.c index 726076972..a71e466f8 100644 --- a/applications/main/nfc/views/dict_attack.c +++ b/applications/main/nfc/views/dict_attack.c @@ -13,12 +13,13 @@ struct DictAttack { typedef struct { FuriString* header; bool card_detected; + DictAttackType attack_type; + + // MIFARE Classic specific uint8_t sectors_total; uint8_t sectors_read; uint8_t current_sector; uint8_t keys_found; - size_t dict_keys_total; - size_t dict_keys_current; bool is_key_attack; uint8_t key_attack_current_sector; MfClassicNestedPhase nested_phase; @@ -26,8 +27,150 @@ typedef struct { MfClassicBackdoor backdoor; uint16_t nested_target_key; uint16_t msb_count; + + // Ultralight C specific + uint8_t pages_total; + uint8_t pages_read; + bool key_found; + + // Common + size_t dict_keys_total; + size_t dict_keys_current; } DictAttackViewModel; +static void dict_attack_draw_mf_classic(Canvas* canvas, DictAttackViewModel* m) { + char draw_str[32] = {}; + canvas_set_font(canvas, FontSecondary); + + switch(m->nested_phase) { + case MfClassicNestedPhaseAnalyzePRNG: + furi_string_set(m->header, "PRNG Analysis"); + break; + case MfClassicNestedPhaseDictAttack: + case MfClassicNestedPhaseDictAttackVerify: + case MfClassicNestedPhaseDictAttackResume: + furi_string_set(m->header, "Nested Dictionary"); + break; + case MfClassicNestedPhaseCalibrate: + case MfClassicNestedPhaseRecalibrate: + furi_string_set(m->header, "Calibration"); + break; + case MfClassicNestedPhaseCollectNtEnc: + furi_string_set(m->header, "Nonce Collection"); + break; + default: + break; + } + + if(m->prng_type == MfClassicPrngTypeHard) { + furi_string_cat(m->header, " (Hard)"); + } + + if(m->backdoor != MfClassicBackdoorNone && m->backdoor != MfClassicBackdoorUnknown) { + if(m->nested_phase != MfClassicNestedPhaseNone) { + furi_string_cat(m->header, " (Backdoor)"); + } else { + furi_string_set(m->header, "Backdoor Read"); + } + } + + canvas_draw_str_aligned(canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(m->header)); + if(m->nested_phase == MfClassicNestedPhaseCollectNtEnc) { + uint8_t nonce_sector = + m->nested_target_key / (m->prng_type == MfClassicPrngTypeWeak ? 4 : 2); + snprintf(draw_str, sizeof(draw_str), "Collecting from sector: %d", nonce_sector); + canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str); + } else if(m->is_key_attack) { + snprintf( + draw_str, + sizeof(draw_str), + "Reuse key check for sector: %d", + m->key_attack_current_sector); + } else { + snprintf(draw_str, sizeof(draw_str), "Unlocking sector: %d", m->current_sector); + } + canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str); + float dict_progress = 0; + if(m->nested_phase == MfClassicNestedPhaseAnalyzePRNG || + m->nested_phase == MfClassicNestedPhaseDictAttack || + m->nested_phase == MfClassicNestedPhaseDictAttackVerify || + m->nested_phase == MfClassicNestedPhaseDictAttackResume) { + // Phase: Nested dictionary attack + uint8_t target_sector = + m->nested_target_key / (m->prng_type == MfClassicPrngTypeWeak ? 2 : 16); + dict_progress = (float)(target_sector) / (float)(m->sectors_total); + snprintf(draw_str, sizeof(draw_str), "%d/%d", target_sector, m->sectors_total); + } else if( + m->nested_phase == MfClassicNestedPhaseCalibrate || + m->nested_phase == MfClassicNestedPhaseRecalibrate || + m->nested_phase == MfClassicNestedPhaseCollectNtEnc) { + // Phase: Nonce collection + if(m->prng_type == MfClassicPrngTypeWeak) { + uint8_t target_sector = m->nested_target_key / 4; + dict_progress = (float)(target_sector) / (float)(m->sectors_total); + snprintf(draw_str, sizeof(draw_str), "%d/%d", target_sector, m->sectors_total); + } else { + uint16_t max_msb = UINT8_MAX + 1; + dict_progress = (float)(m->msb_count) / (float)(max_msb); + snprintf(draw_str, sizeof(draw_str), "%d/%d", m->msb_count, max_msb); + } + } else { + dict_progress = m->dict_keys_total == 0 ? + 0 : + (float)(m->dict_keys_current) / (float)(m->dict_keys_total); + if(m->dict_keys_current == 0) { + // Cause when people see 0 they think it's broken + snprintf(draw_str, sizeof(draw_str), "%d/%zu", 1, m->dict_keys_total); + } else { + snprintf( + draw_str, sizeof(draw_str), "%zu/%zu", m->dict_keys_current, m->dict_keys_total); + } + } + if(dict_progress > 1.0f) { + dict_progress = 1.0f; + } + elements_progress_bar_with_text(canvas, 0, 20, 128, dict_progress, draw_str); + canvas_set_font(canvas, FontSecondary); + snprintf( + draw_str, + sizeof(draw_str), + "Keys found: %d/%d", + m->keys_found, + m->sectors_total * NFC_CLASSIC_KEYS_PER_SECTOR); + canvas_draw_str_aligned(canvas, 0, 33, AlignLeft, AlignTop, draw_str); + snprintf(draw_str, sizeof(draw_str), "Sectors Read: %d/%d", m->sectors_read, m->sectors_total); + canvas_draw_str_aligned(canvas, 0, 43, AlignLeft, AlignTop, draw_str); +} + +static void dict_attack_draw_mf_ultralight_c(Canvas* canvas, DictAttackViewModel* m) { + char draw_str[32] = {}; + canvas_set_font(canvas, FontSecondary); + + canvas_draw_str_aligned(canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(m->header)); + + snprintf(draw_str, sizeof(draw_str), "Trying keys"); + canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str); + + float dict_progress = + m->dict_keys_total == 0 ? 0 : (float)(m->dict_keys_current) / (float)(m->dict_keys_total); + if(m->dict_keys_current == 0) { + snprintf(draw_str, sizeof(draw_str), "%d/%zu", 1, m->dict_keys_total); + } else { + snprintf(draw_str, sizeof(draw_str), "%zu/%zu", m->dict_keys_current, m->dict_keys_total); + } + if(dict_progress > 1.0f) { + dict_progress = 1.0f; + } + elements_progress_bar_with_text(canvas, 0, 20, 128, dict_progress, draw_str); + + canvas_set_font(canvas, FontSecondary); + snprintf(draw_str, sizeof(draw_str), "Key found: %s", m->key_found ? "Yes" : "No"); + canvas_draw_str_aligned(canvas, 0, 33, AlignLeft, AlignTop, draw_str); + + snprintf(draw_str, sizeof(draw_str), "Pages read: %d/%d", m->pages_read, m->pages_total); + canvas_draw_str_aligned(canvas, 0, 43, AlignLeft, AlignTop, draw_str); +} + static void dict_attack_draw_callback(Canvas* canvas, void* model) { DictAttackViewModel* m = model; if(!m->card_detected) { @@ -37,113 +180,11 @@ static void dict_attack_draw_callback(Canvas* canvas, void* model) { elements_multiline_text_aligned( canvas, 64, 23, AlignCenter, AlignTop, "Make sure the tag is\npositioned correctly."); } else { - char draw_str[32] = {}; - canvas_set_font(canvas, FontSecondary); - - switch(m->nested_phase) { - case MfClassicNestedPhaseAnalyzePRNG: - furi_string_set(m->header, "PRNG Analysis"); - break; - case MfClassicNestedPhaseDictAttack: - case MfClassicNestedPhaseDictAttackVerify: - case MfClassicNestedPhaseDictAttackResume: - furi_string_set(m->header, "Nested Dictionary"); - break; - case MfClassicNestedPhaseCalibrate: - case MfClassicNestedPhaseRecalibrate: - furi_string_set(m->header, "Calibration"); - break; - case MfClassicNestedPhaseCollectNtEnc: - furi_string_set(m->header, "Nonce Collection"); - break; - default: - break; + if(m->attack_type == DictAttackTypeMfClassic) { + dict_attack_draw_mf_classic(canvas, m); + } else if(m->attack_type == DictAttackTypeMfUltralightC) { + dict_attack_draw_mf_ultralight_c(canvas, m); } - - if(m->prng_type == MfClassicPrngTypeHard) { - furi_string_cat(m->header, " (Hard)"); - } - - if(m->backdoor != MfClassicBackdoorNone && m->backdoor != MfClassicBackdoorUnknown) { - if(m->nested_phase != MfClassicNestedPhaseNone) { - furi_string_cat(m->header, " (Backdoor)"); - } else { - furi_string_set(m->header, "Backdoor Read"); - } - } - - canvas_draw_str_aligned( - canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(m->header)); - if(m->nested_phase == MfClassicNestedPhaseCollectNtEnc) { - uint8_t nonce_sector = - m->nested_target_key / (m->prng_type == MfClassicPrngTypeWeak ? 4 : 2); - snprintf(draw_str, sizeof(draw_str), "Collecting from sector: %d", nonce_sector); - canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str); - } else if(m->is_key_attack) { - snprintf( - draw_str, - sizeof(draw_str), - "Reuse key check for sector: %d", - m->key_attack_current_sector); - } else { - snprintf(draw_str, sizeof(draw_str), "Unlocking sector: %d", m->current_sector); - } - canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str); - float dict_progress = 0; - if(m->nested_phase == MfClassicNestedPhaseAnalyzePRNG || - m->nested_phase == MfClassicNestedPhaseDictAttack || - m->nested_phase == MfClassicNestedPhaseDictAttackVerify || - m->nested_phase == MfClassicNestedPhaseDictAttackResume) { - // Phase: Nested dictionary attack - uint8_t target_sector = - m->nested_target_key / (m->prng_type == MfClassicPrngTypeWeak ? 2 : 16); - dict_progress = (float)(target_sector) / (float)(m->sectors_total); - snprintf(draw_str, sizeof(draw_str), "%d/%d", target_sector, m->sectors_total); - } else if( - m->nested_phase == MfClassicNestedPhaseCalibrate || - m->nested_phase == MfClassicNestedPhaseRecalibrate || - m->nested_phase == MfClassicNestedPhaseCollectNtEnc) { - // Phase: Nonce collection - if(m->prng_type == MfClassicPrngTypeWeak) { - uint8_t target_sector = m->nested_target_key / 4; - dict_progress = (float)(target_sector) / (float)(m->sectors_total); - snprintf(draw_str, sizeof(draw_str), "%d/%d", target_sector, m->sectors_total); - } else { - uint16_t max_msb = UINT8_MAX + 1; - dict_progress = (float)(m->msb_count) / (float)(max_msb); - snprintf(draw_str, sizeof(draw_str), "%d/%d", m->msb_count, max_msb); - } - } else { - dict_progress = m->dict_keys_total == 0 ? - 0 : - (float)(m->dict_keys_current) / (float)(m->dict_keys_total); - if(m->dict_keys_current == 0) { - // Cause when people see 0 they think it's broken - snprintf(draw_str, sizeof(draw_str), "%d/%zu", 1, m->dict_keys_total); - } else { - snprintf( - draw_str, - sizeof(draw_str), - "%zu/%zu", - m->dict_keys_current, - m->dict_keys_total); - } - } - if(dict_progress > 1.0f) { - dict_progress = 1.0f; - } - elements_progress_bar_with_text(canvas, 0, 20, 128, dict_progress, draw_str); - canvas_set_font(canvas, FontSecondary); - snprintf( - draw_str, - sizeof(draw_str), - "Keys found: %d/%d", - m->keys_found, - m->sectors_total * NFC_CLASSIC_KEYS_PER_SECTOR); - canvas_draw_str_aligned(canvas, 0, 33, AlignLeft, AlignTop, draw_str); - snprintf( - draw_str, sizeof(draw_str), "Sectors Read: %d/%d", m->sectors_read, m->sectors_total); - canvas_draw_str_aligned(canvas, 0, 43, AlignLeft, AlignTop, draw_str); } elements_button_center(canvas, "Skip"); } @@ -195,18 +236,28 @@ void dict_attack_reset(DictAttack* instance) { instance->view, DictAttackViewModel * model, { + model->attack_type = DictAttackTypeMfClassic; + + // MIFARE Classic fields model->sectors_total = 0; model->sectors_read = 0; model->current_sector = 0; model->keys_found = 0; - model->dict_keys_total = 0; - model->dict_keys_current = 0; model->is_key_attack = false; model->nested_phase = MfClassicNestedPhaseNone; model->prng_type = MfClassicPrngTypeUnknown; model->backdoor = MfClassicBackdoorUnknown; model->nested_target_key = 0; model->msb_count = 0; + + // Ultralight C fields + model->pages_total = 0; + model->pages_read = 0; + model->key_found = false; + + // Common fields + model->dict_keys_total = 0; + model->dict_keys_current = 0; furi_string_reset(model->header); }, false); @@ -355,3 +406,31 @@ void dict_attack_set_msb_count(DictAttack* instance, uint16_t msb_count) { with_view_model( instance->view, DictAttackViewModel * model, { model->msb_count = msb_count; }, true); } + +void dict_attack_set_type(DictAttack* instance, DictAttackType type) { + furi_assert(instance); + + with_view_model( + instance->view, DictAttackViewModel * model, { model->attack_type = type; }, true); +} + +void dict_attack_set_pages_total(DictAttack* instance, uint8_t pages_total) { + furi_assert(instance); + + with_view_model( + instance->view, DictAttackViewModel * model, { model->pages_total = pages_total; }, true); +} + +void dict_attack_set_pages_read(DictAttack* instance, uint8_t pages_read) { + furi_assert(instance); + + with_view_model( + instance->view, DictAttackViewModel * model, { model->pages_read = pages_read; }, true); +} + +void dict_attack_set_key_found(DictAttack* instance, bool key_found) { + furi_assert(instance); + + with_view_model( + instance->view, DictAttackViewModel * model, { model->key_found = key_found; }, true); +} diff --git a/applications/main/nfc/views/dict_attack.h b/applications/main/nfc/views/dict_attack.h index b6c6fdbdc..70709f86e 100644 --- a/applications/main/nfc/views/dict_attack.h +++ b/applications/main/nfc/views/dict_attack.h @@ -8,6 +8,11 @@ extern "C" { #endif +typedef enum { + DictAttackTypeMfClassic, + DictAttackTypeMfUltralightC, +} DictAttackType; + typedef struct DictAttack DictAttack; typedef enum { @@ -56,6 +61,14 @@ void dict_attack_set_nested_target_key(DictAttack* instance, uint16_t target_key void dict_attack_set_msb_count(DictAttack* instance, uint16_t msb_count); +void dict_attack_set_type(DictAttack* instance, DictAttackType type); + +void dict_attack_set_pages_total(DictAttack* instance, uint8_t pages_total); + +void dict_attack_set_pages_read(DictAttack* instance, uint8_t pages_read); + +void dict_attack_set_key_found(DictAttack* instance, bool key_found); + #ifdef __cplusplus } #endif diff --git a/applications/services/dolphin/helpers/dolphin_deed.c b/applications/services/dolphin/helpers/dolphin_deed.c index f1f42b770..43f30dced 100644 --- a/applications/services/dolphin/helpers/dolphin_deed.c +++ b/applications/services/dolphin/helpers/dolphin_deed.c @@ -20,7 +20,7 @@ static const DolphinDeedWeight dolphin_deed_weights[] = { {3, DolphinAppNfc}, // DolphinDeedNfcSave {1, DolphinAppNfc}, // DolphinDeedNfcDetectReader {2, DolphinAppNfc}, // DolphinDeedNfcEmulate - {2, DolphinAppNfc}, // DolphinDeedNfcMfcAdd + {2, DolphinAppNfc}, // DolphinDeedNfcKeyAdd {1, DolphinAppNfc}, // DolphinDeedNfcAddSave {1, DolphinAppNfc}, // DolphinDeedNfcAddEmulate diff --git a/applications/services/dolphin/helpers/dolphin_deed.h b/applications/services/dolphin/helpers/dolphin_deed.h index c9cd18f31..7202dcf07 100644 --- a/applications/services/dolphin/helpers/dolphin_deed.h +++ b/applications/services/dolphin/helpers/dolphin_deed.h @@ -36,7 +36,7 @@ typedef enum { DolphinDeedNfcSave, DolphinDeedNfcDetectReader, DolphinDeedNfcEmulate, - DolphinDeedNfcMfcAdd, + DolphinDeedNfcKeyAdd, DolphinDeedNfcAddSave, DolphinDeedNfcAddEmulate, diff --git a/documentation/file_formats/NfcFileFormats.md b/documentation/file_formats/NfcFileFormats.md index da0b0a19d..d89483390 100644 --- a/documentation/file_formats/NfcFileFormats.md +++ b/documentation/file_formats/NfcFileFormats.md @@ -36,7 +36,7 @@ Version differences: ATQA: 00 44 SAK: 00 -### Description +### Description This file format is used to store the UID, SAK and ATQA of an ISO14443-3A device. UID must be either 4 or 7 bytes long. ATQA is 2 bytes long. SAK is 1 byte long. @@ -56,7 +56,7 @@ None, there are no versions yet. Application data: 00 12 34 FF Protocol info: 11 81 E1 -### Description +### Description This file format is used to store the UID, Application data and Protocol info of a ISO14443-3B device. UID must be 4 bytes long. Application data is 4 bytes long. Protocol info is 3 bytes long. @@ -80,7 +80,7 @@ None, there are no versions yet. # ISO14443-4A specific data ATS: 06 75 77 81 02 80 -### Description +### Description This file format is used to store the UID, SAK and ATQA of a ISO14443-4A device. It also stores the Answer to Select (ATS) data of the card. ATS must be no less than 5 bytes long. @@ -303,6 +303,26 @@ None, there are no versions yet. This file contains a list of Mifare Classic keys. Each key is represented as a hex string. Lines starting with '#' are ignored as comments. Blank lines are ignored as well. +## Mifare Ultralight C Dictionary + +### Example + + # Hexadecimal-Reversed Sample Key + 12E4143455F495649454D4B414542524 + # Byte-Reversed Sample Key (!NACUOYFIEMKAERB) + 214E4143554F594649454D4B41455242 + # Sample Key (BREAKMEIFYOUCAN!) + 425245414B4D454946594F5543414E21 + # Semnox Key (IEMKAERB!NACUOY ) + 49454D4B41455242214E4143554F5900 + # Modified Semnox Key (IEMKAERB!NACUOYF) + 49454D4B41455242214E4143554F5946 + ... + +### Description + +This file contains a list of Mifare Ultralight C keys. Each key is represented as a hex string. Lines starting with '#' are ignored as comments. Blank lines are ignored as well. + ## EMV resources ### Example diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c index 7d51f6c6e..5f872952e 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c @@ -251,7 +251,7 @@ static NfcCommand mf_ultralight_poller_handler_read_version(MfUltralightPoller* instance->data->type = mf_ultralight_get_type_by_version(&instance->data->version); instance->state = MfUltralightPollerStateGetFeatureSet; } else { - FURI_LOG_D(TAG, "Didn't response. Check Ultralight C"); + FURI_LOG_D(TAG, "Didn't respond. Check Ultralight C"); iso14443_3a_poller_halt(instance->iso14443_3a_poller); instance->state = MfUltralightPollerStateDetectMfulC; } @@ -266,7 +266,7 @@ static NfcCommand mf_ultralight_poller_handler_check_ultralight_c(MfUltralightPo instance->data->type = MfUltralightTypeMfulC; instance->state = MfUltralightPollerStateGetFeatureSet; } else { - FURI_LOG_D(TAG, "Didn't response. Check NTAG 203"); + FURI_LOG_D(TAG, "Didn't respond. Check NTAG 203"); instance->state = MfUltralightPollerStateDetectNtag203; } iso14443_3a_poller_halt(instance->iso14443_3a_poller); @@ -452,7 +452,48 @@ static NfcCommand mf_ultralight_poller_handler_auth_ultralight_c(MfUltralightPol command = instance->callback(instance->general_event, instance->context); if(!instance->mfu_event.data->auth_context.skip_auth) { FURI_LOG_D(TAG, "Trying to authenticate with 3des key"); - instance->auth_context.tdes_key = instance->mfu_event.data->auth_context.tdes_key; + // Only use the key if it was actually provided + if(instance->mfu_event.data->key_request_data.key_provided) { + instance->auth_context.tdes_key = instance->mfu_event.data->key_request_data.key; + } else if(instance->mode == MfUltralightPollerModeDictAttack) { + // TODO: -nofl Can logic be rearranged to request this key + // before reaching mf_ultralight_poller_handler_auth_ultralight_c in poller? + FURI_LOG_D(TAG, "No initial key provided, requesting key from dictionary"); + // Trigger dictionary key request + instance->mfu_event.type = MfUltralightPollerEventTypeRequestKey; + command = instance->callback(instance->general_event, instance->context); + if(!instance->mfu_event.data->key_request_data.key_provided) { + instance->state = MfUltralightPollerStateReadPages; + return command; + } else { + instance->auth_context.tdes_key = + instance->mfu_event.data->key_request_data.key; + } + } else { + // Fallback: use key from auth context (for sync poller compatibility) + instance->auth_context.tdes_key = instance->mfu_event.data->auth_context.tdes_key; + } + instance->auth_context.auth_success = false; + // For debugging + FURI_LOG_D( + "TAG", + "Key data: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + instance->auth_context.tdes_key.data[0], + instance->auth_context.tdes_key.data[1], + instance->auth_context.tdes_key.data[2], + instance->auth_context.tdes_key.data[3], + instance->auth_context.tdes_key.data[4], + instance->auth_context.tdes_key.data[5], + instance->auth_context.tdes_key.data[6], + instance->auth_context.tdes_key.data[7], + instance->auth_context.tdes_key.data[8], + instance->auth_context.tdes_key.data[9], + instance->auth_context.tdes_key.data[10], + instance->auth_context.tdes_key.data[11], + instance->auth_context.tdes_key.data[12], + instance->auth_context.tdes_key.data[13], + instance->auth_context.tdes_key.data[14], + instance->auth_context.tdes_key.data[15]); do { uint8_t output[MF_ULTRALIGHT_C_AUTH_DATA_SIZE]; uint8_t RndA[MF_ULTRALIGHT_C_AUTH_RND_BLOCK_SIZE] = {0}; @@ -469,20 +510,40 @@ static NfcCommand mf_ultralight_poller_handler_auth_ultralight_c(MfUltralightPol mf_ultralight_3des_shift_data(RndA); instance->auth_context.auth_success = (memcmp(RndA, decoded_shifted_RndA, sizeof(decoded_shifted_RndA)) == 0); - if(instance->auth_context.auth_success) { - FURI_LOG_D(TAG, "Auth success"); + FURI_LOG_E(TAG, "Auth success"); + if(instance->mode == MfUltralightPollerModeDictAttack) { + memcpy( + &instance->data->page[44], + instance->auth_context.tdes_key.data, + MF_ULTRALIGHT_C_AUTH_DES_KEY_SIZE); + // Continue to read pages after successful authentication + instance->state = MfUltralightPollerStateReadPages; + } } } while(false); - if(instance->error != MfUltralightErrorNone || !instance->auth_context.auth_success) { - FURI_LOG_D(TAG, "Auth failed"); + FURI_LOG_E(TAG, "Auth failed"); iso14443_3a_poller_halt(instance->iso14443_3a_poller); + if(instance->mode == MfUltralightPollerModeDictAttack) { + // Not needed? We already do a callback earlier? + instance->mfu_event.type = MfUltralightPollerEventTypeRequestKey; + command = instance->callback(instance->general_event, instance->context); + if(!instance->mfu_event.data->key_request_data.key_provided) { + instance->state = MfUltralightPollerStateReadPages; + } else { + instance->auth_context.tdes_key = + instance->mfu_event.data->key_request_data.key; + instance->state = MfUltralightPollerStateAuthMfulC; + } + } } } } - instance->state = MfUltralightPollerStateReadPages; - + // Regression review + if(instance->mode != MfUltralightPollerModeDictAttack) { + instance->state = MfUltralightPollerStateReadPages; + } return command; } @@ -505,12 +566,16 @@ static NfcCommand mf_ultralight_poller_handler_read_pages(MfUltralightPoller* in instance->error = mf_ultralight_poller_read_page(instance, start_page, &data); } + // Regression review + const uint8_t read_cnt = instance->data->type == MfUltralightTypeMfulC ? 1 : 4; if(instance->error == MfUltralightErrorNone) { - if(start_page < instance->pages_total) { - FURI_LOG_D(TAG, "Read page %d success", start_page); - instance->data->page[start_page] = data.page[0]; - instance->pages_read++; - instance->data->pages_read = instance->pages_read; + for(size_t i = 0; i < read_cnt; i++) { + if(start_page + i < instance->pages_total) { + FURI_LOG_D(TAG, "Read page %d success", start_page + i); + instance->data->page[start_page + i] = data.page[i]; + instance->pages_read++; + instance->data->pages_read = instance->pages_read; + } } if(instance->pages_read == instance->pages_total) { @@ -753,7 +818,6 @@ static const MfUltralightPollerReadHandler [MfUltralightPollerStateWritePages] = mf_ultralight_poller_handler_write_pages, [MfUltralightPollerStateWriteFail] = mf_ultralight_poller_handler_write_fail, [MfUltralightPollerStateWriteSuccess] = mf_ultralight_poller_handler_write_success, - }; static NfcCommand mf_ultralight_poller_run(NfcGenericEvent event, void* context) { diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.h b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.h index e50017324..2552abeb5 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.h +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.h @@ -27,6 +27,7 @@ typedef enum { MfUltralightPollerEventTypeCardLocked, /**< Presented card is locked by password, AUTH0 or lock bytes. */ MfUltralightPollerEventTypeWriteSuccess, /**< Poller wrote card successfully. */ MfUltralightPollerEventTypeWriteFail, /**< Poller failed to write card. */ + MfUltralightPollerEventTypeRequestKey, /**< Poller requests key for dict attack. */ } MfUltralightPollerEventType; /** @@ -35,6 +36,7 @@ typedef enum { typedef enum { MfUltralightPollerModeRead, /**< Poller will only read card. It's a default mode. */ MfUltralightPollerModeWrite, /**< Poller will write already saved card to another presented card. */ + MfUltralightPollerModeDictAttack, /**< Poller will perform dictionary attack against card. */ } MfUltralightPollerMode; /** @@ -42,20 +44,29 @@ typedef enum { */ typedef struct { MfUltralightAuthPassword password; /**< Password to be used for authentication. */ - MfUltralightC3DesAuthKey tdes_key; - MfUltralightAuthPack pack; /**< Pack received on successfull authentication. */ + MfUltralightC3DesAuthKey tdes_key; /**< 3DES key to be used for authentication. */ + MfUltralightAuthPack pack; /**< Pack received on successful authentication. */ bool auth_success; /**< Set to true if authentication succeeded, false otherwise. */ bool skip_auth; /**< Set to true if authentication should be skipped, false otherwise. */ } MfUltralightPollerAuthContext; +/** + * @brief MfUltralight poller key request data. + */ +typedef struct { + MfUltralightC3DesAuthKey key; /**< Key to try. */ + bool key_provided; /**< Set to true if key was provided, false to stop attack. */ +} MfUltralightPollerKeyRequestData; + /** * @brief MfUltralight poller event data. */ typedef union { MfUltralightPollerAuthContext auth_context; /**< Authentication context. */ MfUltralightError error; /**< Error code indicating reading fail reason. */ - const MfUltralightData* write_data; - MfUltralightPollerMode poller_mode; + const MfUltralightData* write_data; /**< Data to be written to card. */ + MfUltralightPollerMode poller_mode; /**< Mode to operate in. */ + MfUltralightPollerKeyRequestData key_request_data; /**< Key request data. */ } MfUltralightPollerEventData; /** @@ -64,7 +75,7 @@ typedef union { * Upon emission of an event, an instance of this struct will be passed to the callback. */ typedef struct { - MfUltralightPollerEventType type; /**< Type of emmitted event. */ + MfUltralightPollerEventType type; /**< Type of emitted event. */ MfUltralightPollerEventData* data; /**< Pointer to event specific data. */ } MfUltralightPollerEvent; diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.c index d84377612..82e647da8 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.c @@ -134,7 +134,7 @@ MfUltralightError mf_ultralight_poller_authenticate_start( uint8_t* RndB = output + MF_ULTRALIGHT_C_AUTH_RND_B_BLOCK_OFFSET; mf_ultralight_3des_decrypt( &instance->des_context, - instance->mfu_event.data->auth_context.tdes_key.data, + instance->auth_context.tdes_key.data, iv, encRndB, sizeof(encRndB), @@ -145,7 +145,7 @@ MfUltralightError mf_ultralight_poller_authenticate_start( mf_ultralight_3des_encrypt( &instance->des_context, - instance->mfu_event.data->auth_context.tdes_key.data, + instance->auth_context.tdes_key.data, encRndB, output, MF_ULTRALIGHT_C_AUTH_DATA_SIZE, @@ -179,7 +179,7 @@ MfUltralightError mf_ultralight_poller_authenticate_end( mf_ultralight_3des_decrypt( &instance->des_context, - instance->mfu_event.data->auth_context.tdes_key.data, + instance->auth_context.tdes_key.data, RndB, bit_buffer_get_data(instance->rx_buffer) + 1, MF_ULTRALIGHT_C_AUTH_RND_BLOCK_SIZE, diff --git a/lib/toolbox/keys_dict.c b/lib/toolbox/keys_dict.c index 602653e8f..c26e9c1e7 100644 --- a/lib/toolbox/keys_dict.c +++ b/lib/toolbox/keys_dict.c @@ -134,22 +134,21 @@ static void keys_dict_int_to_str(KeysDict* instance, const uint8_t* key_int, Fur furi_string_cat_printf(key_str, "%02X", key_int[i]); } -static void keys_dict_str_to_int(KeysDict* instance, FuriString* key_str, uint64_t* key_int) { +static void keys_dict_str_to_int(KeysDict* instance, FuriString* key_str, uint8_t* key_out) { furi_assert(instance); furi_assert(key_str); - furi_assert(key_int); + furi_assert(key_out); uint8_t key_byte_tmp; char h, l; - *key_int = 0ULL; - + // Process two hex characters at a time to create each byte for(size_t i = 0; i < instance->key_size_symbols - 1; i += 2) { h = furi_string_get_char(key_str, i); l = furi_string_get_char(key_str, i + 1); args_char_to_hex(h, l, &key_byte_tmp); - *key_int |= (uint64_t)key_byte_tmp << (8 * (instance->key_size - 1 - i / 2)); + key_out[i / 2] = key_byte_tmp; } } @@ -193,15 +192,7 @@ bool keys_dict_get_next_key(KeysDict* instance, uint8_t* key, size_t key_size) { bool key_read = keys_dict_get_next_key_str(instance, temp_key); if(key_read) { - size_t tmp_len = key_size; - uint64_t key_int = 0; - - keys_dict_str_to_int(instance, temp_key, &key_int); - - while(tmp_len--) { - key[tmp_len] = (uint8_t)key_int; - key_int >>= 8; - } + keys_dict_str_to_int(instance, temp_key, key); } furi_string_free(temp_key); From f78a8328d190bd651d7065038697e604841d7316 Mon Sep 17 00:00:00 2001 From: Leptopt1los <53914086+Leptopt1los@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:13:33 +0300 Subject: [PATCH 2/3] tm01x dallas write support (#4230) Co-authored-by: hedger --- lib/ibutton/protocols/blanks/rw1990.c | 4 ++ lib/ibutton/protocols/blanks/tm01x.c | 58 +++++++++++++++++++ lib/ibutton/protocols/blanks/tm01x.h | 6 ++ lib/ibutton/protocols/blanks/tm2004.c | 2 + .../protocols/dallas/protocol_ds1990.c | 5 +- lib/one_wire/one_wire_host.c | 46 +++++++++++---- lib/one_wire/one_wire_host.h | 4 ++ targets/f18/api_symbols.csv | 2 + targets/f7/api_symbols.csv | 2 + 9 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 lib/ibutton/protocols/blanks/tm01x.c create mode 100644 lib/ibutton/protocols/blanks/tm01x.h diff --git a/lib/ibutton/protocols/blanks/rw1990.c b/lib/ibutton/protocols/blanks/rw1990.c index d8017ca83..6edb4777c 100644 --- a/lib/ibutton/protocols/blanks/rw1990.c +++ b/lib/ibutton/protocols/blanks/rw1990.c @@ -38,6 +38,8 @@ static bool rw1990_read_and_compare(OneWireHost* host, const uint8_t* data, size } bool rw1990_write_v1(OneWireHost* host, const uint8_t* data, size_t data_size) { + onewire_host_set_timings_default(host); + // Unlock sequence onewire_host_reset(host); onewire_host_write(host, RW1990_1_CMD_WRITE_RECORD_FLAG); @@ -67,6 +69,8 @@ bool rw1990_write_v1(OneWireHost* host, const uint8_t* data, size_t data_size) { } bool rw1990_write_v2(OneWireHost* host, const uint8_t* data, size_t data_size) { + onewire_host_set_timings_default(host); + // Unlock sequence onewire_host_reset(host); onewire_host_write(host, RW1990_2_CMD_WRITE_RECORD_FLAG); diff --git a/lib/ibutton/protocols/blanks/tm01x.c b/lib/ibutton/protocols/blanks/tm01x.c new file mode 100644 index 000000000..6bdcb43d0 --- /dev/null +++ b/lib/ibutton/protocols/blanks/tm01x.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include "tm01x.h" + +// Commands for TM01x +#define TM01X_CMD_WRITE_FLAG 0xC1 +#define TM01X_CMD_WRITE_ROM 0xC5 +#define TM01X_CMD_READ_ROM 0x33 + +#define TM01X_CMD_FINALIZE_CYFRAL 0xCA +#define TM01X_CMD_FINALIZE_METAKOM 0xCB + +static void tm01x_write_byte(OneWireHost* host, uint8_t value) { + for(uint8_t bitMask = 0x01; bitMask; bitMask <<= 1) { + onewire_host_write_bit(host, (bool)(bitMask & value)); + furi_delay_us(5000); // 5ms pause after each bit + } +} + +// Helper function to read and verify written data +static bool tm01x_read_and_verify(OneWireHost* host, const uint8_t* data, size_t data_size) { + bool success = false; + + if(onewire_host_reset(host)) { + success = true; + onewire_host_write(host, TM01X_CMD_READ_ROM); + + for(size_t i = 0; i < data_size; ++i) { + if(data[i] != onewire_host_read(host)) { + success = false; + break; + } + } + } + + return success; +} + +bool tm01x_write_dallas(OneWireHost* host, const uint8_t* data, size_t data_size) { + // Set TM01x specific timings + onewire_host_set_timings_tm01x(host); + + // Write sequence + onewire_host_reset(host); + onewire_host_write(host, TM01X_CMD_WRITE_FLAG); + onewire_host_write_bit(host, true); + furi_delay_us(5000); + + onewire_host_reset(host); + onewire_host_write(host, TM01X_CMD_WRITE_ROM); + + for(size_t i = 0; i < data_size; ++i) { + tm01x_write_byte(host, data[i]); + } + + return tm01x_read_and_verify(host, data, data_size); +} diff --git a/lib/ibutton/protocols/blanks/tm01x.h b/lib/ibutton/protocols/blanks/tm01x.h new file mode 100644 index 000000000..6c7840f6d --- /dev/null +++ b/lib/ibutton/protocols/blanks/tm01x.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +bool tm01x_write_dallas(OneWireHost* host, const uint8_t* data, size_t data_size); diff --git a/lib/ibutton/protocols/blanks/tm2004.c b/lib/ibutton/protocols/blanks/tm2004.c index a275dda0a..a93b69410 100644 --- a/lib/ibutton/protocols/blanks/tm2004.c +++ b/lib/ibutton/protocols/blanks/tm2004.c @@ -9,6 +9,8 @@ #define TM2004_ANSWER_READ_MEMORY 0xF5 bool tm2004_write(OneWireHost* host, const uint8_t* data, size_t data_size) { + onewire_host_set_timings_default(host); + onewire_host_reset(host); onewire_host_write(host, TM2004_CMD_WRITE_ROM); // Starting writing from address 0x0000 diff --git a/lib/ibutton/protocols/dallas/protocol_ds1990.c b/lib/ibutton/protocols/dallas/protocol_ds1990.c index 5ed2171c6..44fd60192 100644 --- a/lib/ibutton/protocols/dallas/protocol_ds1990.c +++ b/lib/ibutton/protocols/dallas/protocol_ds1990.c @@ -7,7 +7,7 @@ #include "../blanks/rw1990.h" #include "../blanks/tm2004.h" - +#include "../blanks/tm01x.h" #define DS1990_FAMILY_CODE 0x01U #define DS1990_FAMILY_NAME "DS1990" @@ -66,7 +66,8 @@ bool dallas_ds1990_write_id(OneWireHost* host, iButtonProtocolData* protocol_dat return rw1990_write_v1(host, data->rom_data.bytes, sizeof(DallasCommonRomData)) || rw1990_write_v2(host, data->rom_data.bytes, sizeof(DallasCommonRomData)) || - tm2004_write(host, data->rom_data.bytes, sizeof(DallasCommonRomData)); + tm2004_write(host, data->rom_data.bytes, sizeof(DallasCommonRomData)) || + tm01x_write_dallas(host, data->rom_data.bytes, sizeof(DallasCommonRomData)); } static bool dallas_ds1990_reset_callback(bool is_short, void* context) { diff --git a/lib/one_wire/one_wire_host.c b/lib/one_wire/one_wire_host.c index f3f3d953e..62d325cf5 100644 --- a/lib/one_wire/one_wire_host.c +++ b/lib/one_wire/one_wire_host.c @@ -8,16 +8,16 @@ #include "one_wire_host.h" typedef struct { - uint16_t a; - uint16_t b; - uint16_t c; - uint16_t d; - uint16_t e; - uint16_t f; - uint16_t g; - uint16_t h; - uint16_t i; - uint16_t j; + uint16_t a; // Write 1 low time + uint16_t b; // Write 1 high time + uint16_t c; // Write 0 low time + uint16_t d; // Write 0 high time + uint16_t e; // Read low time + uint16_t f; // Read high time + uint16_t g; // Reset pre-delay + uint16_t h; // Reset pulse + uint16_t i; // Presence detect + uint16_t j; // Reset post-delay } OneWireHostTimings; static const OneWireHostTimings onewire_host_timings_normal = { @@ -46,6 +46,20 @@ static const OneWireHostTimings onewire_host_timings_overdrive = { .j = 40, }; +// TM01x specific timings +static const OneWireHostTimings onewire_host_timings_tm01x = { + .a = 5, + .b = 80, + .c = 70, + .d = 10, + .e = 5, + .f = 70, + .g = 0, + .h = 740, + .i = 140, + .j = 410, +}; + struct OneWireHost { const GpioPin* gpio_pin; const OneWireHostTimings* timings; @@ -354,3 +368,15 @@ void onewire_host_set_overdrive(OneWireHost* host, bool set) { host->timings = set ? &onewire_host_timings_overdrive : &onewire_host_timings_normal; } + +void onewire_host_set_timings_default(OneWireHost* host) { + furi_check(host); + + host->timings = &onewire_host_timings_normal; +} + +void onewire_host_set_timings_tm01x(OneWireHost* host) { + furi_check(host); + + host->timings = &onewire_host_timings_tm01x; +} diff --git a/lib/one_wire/one_wire_host.h b/lib/one_wire/one_wire_host.h index 9f9bd4ffd..e61dc63e2 100644 --- a/lib/one_wire/one_wire_host.h +++ b/lib/one_wire/one_wire_host.h @@ -125,6 +125,10 @@ bool onewire_host_search(OneWireHost* host, uint8_t* new_addr, OneWireHostSearch */ void onewire_host_set_overdrive(OneWireHost* host, bool set); +void onewire_host_set_timings_default(OneWireHost* host); + +void onewire_host_set_timings_tm01x(OneWireHost* host); + #ifdef __cplusplus } #endif diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 0590a16b8..a89625080 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -2283,6 +2283,8 @@ Function,+,onewire_host_reset,_Bool,OneWireHost* Function,+,onewire_host_reset_search,void,OneWireHost* Function,+,onewire_host_search,_Bool,"OneWireHost*, uint8_t*, OneWireHostSearchMode" Function,+,onewire_host_set_overdrive,void,"OneWireHost*, _Bool" +Function,+,onewire_host_set_timings_default,void,OneWireHost* +Function,+,onewire_host_set_timings_tm01x,void,OneWireHost* Function,+,onewire_host_start,void,OneWireHost* Function,+,onewire_host_stop,void,OneWireHost* Function,+,onewire_host_target_search,void,"OneWireHost*, uint8_t" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 62f0245df..ef938ab27 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -2935,6 +2935,8 @@ Function,+,onewire_host_reset,_Bool,OneWireHost* Function,+,onewire_host_reset_search,void,OneWireHost* Function,+,onewire_host_search,_Bool,"OneWireHost*, uint8_t*, OneWireHostSearchMode" Function,+,onewire_host_set_overdrive,void,"OneWireHost*, _Bool" +Function,+,onewire_host_set_timings_default,void,OneWireHost* +Function,+,onewire_host_set_timings_tm01x,void,OneWireHost* Function,+,onewire_host_start,void,OneWireHost* Function,+,onewire_host_stop,void,OneWireHost* Function,+,onewire_host_target_search,void,"OneWireHost*, uint8_t" From 1e0f3a606f654bd9047b271aadaa6d028199100c Mon Sep 17 00:00:00 2001 From: Ivan Barsukov Date: Mon, 29 Sep 2025 20:53:10 +0300 Subject: [PATCH 3/3] cli: Buzzer command (#4006) * Add args_read_float_and_trim function * Add args_read_duration function * Add notes_frequency_from_name function * Add cli_sleep function and sleep CLI command * Update CLI top command to use cli_sleep * Add buzzer CLI command * toolbox: make args_read_duration less convoluted * notification: make notification_messages_notes_frequency_from_name less convoluted * unit_tests: better float checking * fix formatting and f18 --------- Co-authored-by: Anna Antonenko Co-authored-by: hedger --- applications/debug/unit_tests/application.fam | 17 ++ .../debug/unit_tests/tests/args/args_test.c | 211 ++++++++++++++++++ applications/debug/unit_tests/tests/minunit.h | 2 +- .../tests/notification/notes_test.c | 165 ++++++++++++++ applications/services/cli/application.fam | 8 + applications/services/cli/cli_main_commands.c | 49 +++- applications/services/cli/commands/buzzer.c | 135 +++++++++++ .../notification_messages_notes.c | 33 +++ .../notification_messages_notes.h | 11 + lib/toolbox/args.c | 54 +++++ lib/toolbox/args.h | 39 +++- lib/toolbox/cli/cli_command.c | 15 ++ lib/toolbox/cli/cli_command.h | 23 +- targets/f18/api_symbols.csv | 4 + targets/f7/api_symbols.csv | 4 + 15 files changed, 747 insertions(+), 23 deletions(-) create mode 100644 applications/debug/unit_tests/tests/args/args_test.c create mode 100644 applications/debug/unit_tests/tests/notification/notes_test.c create mode 100644 applications/services/cli/commands/buzzer.c diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index 72b8cafcb..252eb57c5 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -244,3 +244,20 @@ App( entry_point="get_api", requires=["unit_tests"], ) + +App( + appid="test_args", + sources=["tests/common/*.c", "tests/args/*.c"], + apptype=FlipperAppType.PLUGIN, + entry_point="get_api", + requires=["unit_tests"], +) + + +App( + appid="test_notification", + sources=["tests/common/*.c", "tests/notification/*.c"], + apptype=FlipperAppType.PLUGIN, + entry_point="get_api", + requires=["unit_tests"], +) diff --git a/applications/debug/unit_tests/tests/args/args_test.c b/applications/debug/unit_tests/tests/args/args_test.c new file mode 100644 index 000000000..9b1887f0b --- /dev/null +++ b/applications/debug/unit_tests/tests/args/args_test.c @@ -0,0 +1,211 @@ + +#include "../test.h" // IWYU pragma: keep +#include + +const uint32_t one_ms = 1; +const uint32_t one_s = 1000 * one_ms; +const uint32_t one_m = 60 * one_s; +const uint32_t one_h = 60 * one_m; + +MU_TEST(args_read_duration_default_values_test) { + FuriString* args_string; + uint32_t value = 0; + + // Check default == NULL (ms) + args_string = furi_string_alloc_set_str("1"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, one_ms); + furi_string_free(args_string); + value = 0; + + // Check default == ms + args_string = furi_string_alloc_set_str("1"); + mu_check(args_read_duration(args_string, &value, "ms")); + mu_assert_int_eq(value, one_ms); + furi_string_free(args_string); + value = 0; + + // Check default == s + args_string = furi_string_alloc_set_str("1"); + mu_check(args_read_duration(args_string, &value, "s")); + mu_assert_int_eq(value, one_s); + furi_string_free(args_string); + value = 0; + + // Check default == m + args_string = furi_string_alloc_set_str("1"); + mu_check(args_read_duration(args_string, &value, "m")); + mu_assert_int_eq(value, one_m); + furi_string_free(args_string); + value = 0; + + // Check default == h + args_string = furi_string_alloc_set_str("1"); + mu_check(args_read_duration(args_string, &value, "h")); + mu_assert_int_eq(value, one_h); + furi_string_free(args_string); + value = 0; +} + +MU_TEST(args_read_duration_suffix_values_test) { + FuriString* args_string; + uint32_t value = 0; + + // Check ms + args_string = furi_string_alloc_set_str("1ms"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, one_ms); + furi_string_free(args_string); + value = 0; + + // Check s + args_string = furi_string_alloc_set_str("1s"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, one_s); + furi_string_free(args_string); + value = 0; + + // Check m + args_string = furi_string_alloc_set_str("1m"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, one_m); + furi_string_free(args_string); + value = 0; + + // Check h + args_string = furi_string_alloc_set_str("1h"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, one_h); + furi_string_free(args_string); + value = 0; +} + +MU_TEST(args_read_duration_values_test) { + FuriString* args_string; + uint32_t value = 0; + + // Check for ms + args_string = furi_string_alloc_set_str("4294967295ms"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 4294967295U); + furi_string_free(args_string); + + // Check for s + args_string = furi_string_alloc_set_str("4294967s"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 4294967U * one_s); + furi_string_free(args_string); + + // Check for m + args_string = furi_string_alloc_set_str("71582m"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 71582U * one_m); + furi_string_free(args_string); + + // Check for h + args_string = furi_string_alloc_set_str("1193h"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 1193U * one_h); + furi_string_free(args_string); + + // Check for ms in float + args_string = furi_string_alloc_set_str("4.2ms"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 4); + furi_string_free(args_string); + + // Check for s in float + args_string = furi_string_alloc_set_str("1.5s"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, (uint32_t)(1.5 * one_s)); + furi_string_free(args_string); + + // Check for m in float + args_string = furi_string_alloc_set_str("1.5m"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, (uint32_t)(1.5 * one_m)); + furi_string_free(args_string); + + // Check for h in float + args_string = furi_string_alloc_set_str("1.5h"); + mu_check(args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, (uint32_t)(1.5 * one_h)); + furi_string_free(args_string); +} + +MU_TEST(args_read_duration_errors_test) { + FuriString* args_string; + uint32_t value = 0; + + // Check wrong suffix + args_string = furi_string_alloc_set_str("1x"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); + + // Check wrong suffix + args_string = furi_string_alloc_set_str("1xs"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); + + // Check negative value + args_string = furi_string_alloc_set_str("-1s"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); + + // Check wrong values + + // Check only suffix + args_string = furi_string_alloc_set_str("s"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); + + // Check doubled point + args_string = furi_string_alloc_set_str("0.1.1"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); + + // Check overflow values + + // Check for ms + args_string = furi_string_alloc_set_str("4294967296ms"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); + + // Check for s + args_string = furi_string_alloc_set_str("4294968s"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); + + // Check for m + args_string = furi_string_alloc_set_str("71583m"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); + + // Check for h + args_string = furi_string_alloc_set_str("1194h"); + mu_check(!args_read_duration(args_string, &value, NULL)); + mu_assert_int_eq(value, 0); + furi_string_free(args_string); +} + +MU_TEST_SUITE(toolbox_args_read_duration_suite) { + MU_RUN_TEST(args_read_duration_default_values_test); + MU_RUN_TEST(args_read_duration_suffix_values_test); + MU_RUN_TEST(args_read_duration_values_test); + MU_RUN_TEST(args_read_duration_errors_test); +} + +int run_minunit_test_toolbox_args(void) { + MU_RUN_SUITE(toolbox_args_read_duration_suite); + return MU_EXIT_CODE; +} + +TEST_API_DEFINE(run_minunit_test_toolbox_args) diff --git a/applications/debug/unit_tests/tests/minunit.h b/applications/debug/unit_tests/tests/minunit.h index c854c4673..943ed3c67 100644 --- a/applications/debug/unit_tests/tests/minunit.h +++ b/applications/debug/unit_tests/tests/minunit.h @@ -389,8 +389,8 @@ void minunit_printf_warning(const char* format, ...); __func__, \ __FILE__, \ __LINE__, \ - minunit_tmp_e, \ minunit_tmp_r, \ + minunit_tmp_e, \ minunit_tmp_m); \ minunit_status = 1; \ return; \ diff --git a/applications/debug/unit_tests/tests/notification/notes_test.c b/applications/debug/unit_tests/tests/notification/notes_test.c new file mode 100644 index 000000000..2b6d25c13 --- /dev/null +++ b/applications/debug/unit_tests/tests/notification/notes_test.c @@ -0,0 +1,165 @@ +#include "../test.h" // IWYU pragma: keep +#include +#include + +void frequency_assert(const char* note_name, const NotificationMessage* message) { + double a = notification_messages_notes_frequency_from_name(note_name); + double b = message->data.sound.frequency; + const double epsilon = message->data.sound.frequency > 5000 ? 0.02f : 0.01f; + mu_assert_double_between(b - epsilon, b + epsilon, a); +} + +MU_TEST(notification_messages_notes_frequency_from_name_test) { + // Upper case + mu_check(float_is_equal( + notification_messages_notes_frequency_from_name("C0"), + notification_messages_notes_frequency_from_name("c0"))); + + // Mixed case + mu_check(float_is_equal( + notification_messages_notes_frequency_from_name("Cs0"), + notification_messages_notes_frequency_from_name("cs0"))); + + // Check errors + mu_check( + float_is_equal(notification_messages_notes_frequency_from_name("0"), 0.0)); // Without note + mu_check(float_is_equal( + notification_messages_notes_frequency_from_name("C"), 0.0)); // Without octave + mu_check(float_is_equal( + notification_messages_notes_frequency_from_name("C9"), 0.0)); // Unsupported octave + mu_check(float_is_equal( + notification_messages_notes_frequency_from_name("C10"), 0.0)); // Unsupported octave + mu_check(float_is_equal( + notification_messages_notes_frequency_from_name("X0"), 0.0)); // Unknown note + mu_check(float_is_equal( + notification_messages_notes_frequency_from_name("CCC0"), 0.0)); // Note name overflow + + // Notes and structures + frequency_assert("c0", &message_note_c0); + frequency_assert("cs0", &message_note_cs0); + frequency_assert("d0", &message_note_d0); + frequency_assert("ds0", &message_note_ds0); + frequency_assert("e0", &message_note_e0); + frequency_assert("f0", &message_note_f0); + frequency_assert("fs0", &message_note_fs0); + frequency_assert("g0", &message_note_g0); + frequency_assert("gs0", &message_note_gs0); + frequency_assert("a0", &message_note_a0); + frequency_assert("as0", &message_note_as0); + frequency_assert("b0", &message_note_b0); + + frequency_assert("c1", &message_note_c1); + frequency_assert("cs1", &message_note_cs1); + frequency_assert("d1", &message_note_d1); + frequency_assert("ds1", &message_note_ds1); + frequency_assert("e1", &message_note_e1); + frequency_assert("f1", &message_note_f1); + frequency_assert("fs1", &message_note_fs1); + frequency_assert("g1", &message_note_g1); + frequency_assert("gs1", &message_note_gs1); + frequency_assert("a1", &message_note_a1); + frequency_assert("as1", &message_note_as1); + frequency_assert("b1", &message_note_b1); + + frequency_assert("c2", &message_note_c2); + frequency_assert("cs2", &message_note_cs2); + frequency_assert("d2", &message_note_d2); + frequency_assert("ds2", &message_note_ds2); + frequency_assert("e2", &message_note_e2); + frequency_assert("f2", &message_note_f2); + frequency_assert("fs2", &message_note_fs2); + frequency_assert("g2", &message_note_g2); + frequency_assert("gs2", &message_note_gs2); + frequency_assert("a2", &message_note_a2); + frequency_assert("as2", &message_note_as2); + frequency_assert("b2", &message_note_b2); + + frequency_assert("c3", &message_note_c3); + frequency_assert("cs3", &message_note_cs3); + frequency_assert("d3", &message_note_d3); + frequency_assert("ds3", &message_note_ds3); + frequency_assert("e3", &message_note_e3); + frequency_assert("f3", &message_note_f3); + frequency_assert("fs3", &message_note_fs3); + frequency_assert("g3", &message_note_g3); + frequency_assert("gs3", &message_note_gs3); + frequency_assert("a3", &message_note_a3); + frequency_assert("as3", &message_note_as3); + frequency_assert("b3", &message_note_b3); + + frequency_assert("c4", &message_note_c4); + frequency_assert("cs4", &message_note_cs4); + frequency_assert("d4", &message_note_d4); + frequency_assert("ds4", &message_note_ds4); + frequency_assert("e4", &message_note_e4); + frequency_assert("f4", &message_note_f4); + frequency_assert("fs4", &message_note_fs4); + frequency_assert("g4", &message_note_g4); + frequency_assert("gs4", &message_note_gs4); + frequency_assert("a4", &message_note_a4); + frequency_assert("as4", &message_note_as4); + frequency_assert("b4", &message_note_b4); + + frequency_assert("c5", &message_note_c5); + frequency_assert("cs5", &message_note_cs5); + frequency_assert("d5", &message_note_d5); + frequency_assert("ds5", &message_note_ds5); + frequency_assert("e5", &message_note_e5); + frequency_assert("f5", &message_note_f5); + frequency_assert("fs5", &message_note_fs5); + frequency_assert("g5", &message_note_g5); + frequency_assert("gs5", &message_note_gs5); + frequency_assert("a5", &message_note_a5); + frequency_assert("as5", &message_note_as5); + frequency_assert("b5", &message_note_b5); + + frequency_assert("c6", &message_note_c6); + frequency_assert("cs6", &message_note_cs6); + frequency_assert("d6", &message_note_d6); + frequency_assert("ds6", &message_note_ds6); + frequency_assert("e6", &message_note_e6); + frequency_assert("f6", &message_note_f6); + frequency_assert("fs6", &message_note_fs6); + frequency_assert("g6", &message_note_g6); + frequency_assert("gs6", &message_note_gs6); + frequency_assert("a6", &message_note_a6); + frequency_assert("as6", &message_note_as6); + frequency_assert("b6", &message_note_b6); + + frequency_assert("c7", &message_note_c7); + frequency_assert("cs7", &message_note_cs7); + frequency_assert("d7", &message_note_d7); + frequency_assert("ds7", &message_note_ds7); + frequency_assert("e7", &message_note_e7); + frequency_assert("f7", &message_note_f7); + frequency_assert("fs7", &message_note_fs7); + frequency_assert("g7", &message_note_g7); + frequency_assert("gs7", &message_note_gs7); + frequency_assert("a7", &message_note_a7); + frequency_assert("as7", &message_note_as7); + frequency_assert("b7", &message_note_b7); + + frequency_assert("c8", &message_note_c8); + frequency_assert("cs8", &message_note_cs8); + frequency_assert("d8", &message_note_d8); + frequency_assert("ds8", &message_note_ds8); + frequency_assert("e8", &message_note_e8); + frequency_assert("f8", &message_note_f8); + frequency_assert("fs8", &message_note_fs8); + frequency_assert("g8", &message_note_g8); + frequency_assert("gs8", &message_note_gs8); + frequency_assert("a8", &message_note_a8); + frequency_assert("as8", &message_note_as8); + frequency_assert("b8", &message_note_b8); +} + +MU_TEST_SUITE(notes_suite) { + MU_RUN_TEST(notification_messages_notes_frequency_from_name_test); +} + +int run_minunit_test_notes(void) { + MU_RUN_SUITE(notes_suite); + return MU_EXIT_CODE; +} + +TEST_API_DEFINE(run_minunit_test_notes) diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index b305fb6b0..d5acf7752 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -49,3 +49,11 @@ App( requires=["cli"], sources=["commands/subshell_demo.c"], ) + +App( + appid="cli_buzzer", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_buzzer_ep", + requires=["cli"], + sources=["commands/buzzer.c"], +) diff --git a/applications/services/cli/cli_main_commands.c b/applications/services/cli/cli_main_commands.c index 508a650de..a478512d5 100644 --- a/applications/services/cli/cli_main_commands.c +++ b/applications/services/cli/cli_main_commands.c @@ -356,11 +356,13 @@ void cli_command_led(PipeSide* pipe, FuriString* args, void* context) { static void cli_command_top(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); - int interval = 1000; - args_read_int_and_trim(args, &interval); + uint32_t interval; + if(!args_read_duration(args, &interval, NULL)) { + interval = 1000; + } FuriThreadList* thread_list = furi_thread_list_alloc(); - while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { + do { uint32_t tick = furi_get_tick(); furi_thread_enumerate(thread_list); @@ -416,12 +418,8 @@ static void cli_command_top(PipeSide* pipe, FuriString* args, void* context) { printf(ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END)); fflush(stdout); - if(interval > 0) { - furi_delay_ms(interval); - } else { - break; - } - } + } while(interval > 0 && cli_sleep(pipe, interval)); + furi_thread_list_free(thread_list); } @@ -491,6 +489,37 @@ void cli_command_echo(PipeSide* pipe, FuriString* args, void* context) { } } +/** + * @brief Pause for a specified duration or until Ctrl+C is pressed or the + * session is terminated. + * + * The duration can be specified in various units such as milliseconds (ms), + * seconds (s), minutes (m), or hours (h). If the unit is not specified, the + * second is used by default. + * + * Example: + * sleep 5s + */ +void cli_command_sleep(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(context); + FuriString* duration_string; + duration_string = furi_string_alloc(); + + do { + uint32_t duration_in_ms = 0; + if(!args_read_string_and_trim(args, duration_string) || + !args_read_duration(duration_string, &duration_in_ms, "s")) { + cli_print_usage("sleep", "[<0-...>[]]", furi_string_get_cstr(args)); + break; + } + + cli_sleep(pipe, duration_in_ms); + + } while(false); + + furi_string_free(duration_string); +} + void cli_main_commands_init(CliRegistry* registry) { cli_registry_add_command( registry, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); @@ -508,6 +537,8 @@ void cli_main_commands_init(CliRegistry* registry) { cli_registry_add_command( registry, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); cli_registry_add_command(registry, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); + cli_registry_add_command( + registry, "sleep", CliCommandFlagParallelSafe, cli_command_sleep, NULL); cli_registry_add_command(registry, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); cli_registry_add_command(registry, "led", CliCommandFlagDefault, cli_command_led, NULL); diff --git a/applications/services/cli/commands/buzzer.c b/applications/services/cli/commands/buzzer.c new file mode 100644 index 000000000..3c1673149 --- /dev/null +++ b/applications/services/cli/commands/buzzer.c @@ -0,0 +1,135 @@ +#include "../cli_main_commands.h" +#include +#include +#include + +void cli_command_buzzer_print_usage(bool is_freq_subcommand, FuriString* args) { + if(is_freq_subcommand) { + cli_print_usage( + "buzzer freq", " [<0-...>[]]", furi_string_get_cstr(args)); + + } else { + cli_print_usage("buzzer note", " [<0-...>[]]", furi_string_get_cstr(args)); + } +} + +float cli_command_buzzer_read_frequency(bool is_freq_subcommand, FuriString* args) { + float frequency = 0.0f; + + if(is_freq_subcommand) { + args_read_float_and_trim(args, &frequency); + return frequency; + } + + // Extract note frequency from name + + FuriString* note_name_string; + note_name_string = furi_string_alloc(); + + do { + if(!args_read_string_and_trim(args, note_name_string)) { + break; + } + const char* note_name = furi_string_get_cstr(note_name_string); + frequency = notification_messages_notes_frequency_from_name(note_name); + } while(false); + + furi_string_free(note_name_string); + + return frequency; +} + +void cli_command_buzzer_play( + PipeSide* pipe, + NotificationApp* notification, + bool is_freq_subcommand, + FuriString* args) { + FuriString* duration_string; + duration_string = furi_string_alloc(); + + do { + float frequency = cli_command_buzzer_read_frequency(is_freq_subcommand, args); + if(frequency <= 0.0f) { + cli_command_buzzer_print_usage(is_freq_subcommand, args); + break; + } + + const NotificationMessage notification_buzzer_message = { + .type = NotificationMessageTypeSoundOn, + .data.sound.frequency = frequency, + .data.sound.volume = 1.0, + }; + + // Optional duration + uint32_t duration_ms = 100; + if(args_read_string_and_trim(args, duration_string)) { + if(!args_read_duration(duration_string, &duration_ms, NULL)) { + cli_command_buzzer_print_usage(is_freq_subcommand, args); + break; + } + } + + const NotificationSequence sound_on_sequence = { + ¬ification_buzzer_message, + &message_do_not_reset, + NULL, + }; + + // Play sound + notification_message_block(notification, &sound_on_sequence); + + cli_sleep(pipe, duration_ms); + + // Stop sound + const NotificationSequence sound_off_sequence = { + &message_sound_off, + NULL, + }; + notification_message_block(notification, &sound_off_sequence); + + } while(false); + + furi_string_free(duration_string); +} + +void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(context); + + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); + + FuriString* command_string; + command_string = furi_string_alloc(); + + do { + if(!args_read_string_and_trim(args, command_string)) { + cli_print_usage("buzzer", "", furi_string_get_cstr(args)); + break; + } + + // Check volume + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { + printf("Flipper is in stealth mode. Unmute the device to control buzzer."); + break; + } + if(notification->settings.speaker_volume == 0.0f) { + printf("Sound is disabled in settings. Increase volume to control buzzer."); + break; + } + + if(furi_string_cmp(command_string, "freq") == 0) { + cli_command_buzzer_play(pipe, notification, true, args); + break; + } else if(furi_string_cmp(command_string, "note") == 0) { + cli_command_buzzer_play(pipe, notification, false, args); + break; + } + + cli_print_usage("buzzer", "", furi_string_get_cstr(args)); + + } while(false); + + furi_string_free(command_string); + furi_record_close(RECORD_NOTIFICATION); +} + +CLI_COMMAND_INTERFACE(buzzer, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/services/notification/notification_messages_notes.c b/applications/services/notification/notification_messages_notes.c index 18ff94aaf..4a13fe0e6 100644 --- a/applications/services/notification/notification_messages_notes.c +++ b/applications/services/notification/notification_messages_notes.c @@ -1,4 +1,5 @@ #include "notification.h" +#include /* Python script for note messages generation @@ -571,3 +572,35 @@ const NotificationMessage message_note_b8 = { .data.sound.frequency = 7902.13f, .data.sound.volume = 1.0f, }; + +float notification_messages_notes_frequency_from_name(const char* note_name) { + const float base_note = 16.3515979f; // C0 + + const char* note_names[] = {"c", "cs", "d", "ds", "e", "f", "fs", "g", "gs", "a", "as", "b"}; + const size_t notes_count = COUNT_OF(note_names); + + char note_wo_octave[3] = {0}; + for(size_t i = 0; i < sizeof(note_wo_octave) - 1; i++) { + char in = *note_name; + if(!in) break; + if(!isalpha(in)) break; + note_wo_octave[i] = in; + note_name++; + } + + int note_index = -1; + for(size_t i = 0; i < notes_count; i++) { + if(strcasecmp(note_wo_octave, note_names[i]) == 0) note_index = i; + } + if(note_index < 0) return 0.0; + + uint16_t octave; + StrintParseError error = strint_to_uint16(note_name, NULL, &octave, 10); + if(error != StrintParseNoError) return 0.0; + if(octave > 8) return 0.0; + + int semitone_index = octave * notes_count + note_index; + float frequency = base_note * powf(2.0f, semitone_index / 12.0f); + + return roundf(frequency * 100) / 100.0f; +} diff --git a/applications/services/notification/notification_messages_notes.h b/applications/services/notification/notification_messages_notes.h index b1040a01e..dffa2519e 100644 --- a/applications/services/notification/notification_messages_notes.h +++ b/applications/services/notification/notification_messages_notes.h @@ -115,6 +115,17 @@ extern const NotificationMessage message_note_a8; extern const NotificationMessage message_note_as8; extern const NotificationMessage message_note_b8; +/** + * @brief Returns the frequency of the given note + * + * This function calculates and returns the frequency (in Hz) of the specified note. + * If the input note name is invalid, the function returns 0.0. + * + * @param [in] note_name The name of the note (e.g., "A4", cs5") + * @return The frequency of the note in Hz, or 0.0 if the note name is invalid + */ +extern float notification_messages_notes_frequency_from_name(const char* note_name); + #ifdef __cplusplus } #endif diff --git a/lib/toolbox/args.c b/lib/toolbox/args.c index 914b093ba..f6a1cfda5 100644 --- a/lib/toolbox/args.c +++ b/lib/toolbox/args.c @@ -2,6 +2,7 @@ #include "hex.h" #include "strint.h" #include "m-core.h" +#include size_t args_get_first_word_length(FuriString* args) { size_t ws = furi_string_search_char(args, ' '); @@ -34,6 +35,24 @@ bool args_read_int_and_trim(FuriString* args, int* value) { return false; } +bool args_read_float_and_trim(FuriString* args, float* value) { + size_t cmd_length = args_get_first_word_length(args); + if(cmd_length == 0) { + return false; + } + + char* end_ptr; + float temp = strtof(furi_string_get_cstr(args), &end_ptr); + if(end_ptr == furi_string_get_cstr(args)) { + return false; + } + + *value = temp; + furi_string_right(args, cmd_length); + furi_string_trim(args); + return true; +} + bool args_read_string_and_trim(FuriString* args, FuriString* word) { size_t cmd_length = args_get_first_word_length(args); @@ -97,3 +116,38 @@ bool args_read_hex_bytes(FuriString* args, uint8_t* bytes, size_t bytes_count) { return result; } + +bool args_read_duration(FuriString* args, uint32_t* value, const char* default_unit) { + const char* args_cstr = furi_string_get_cstr(args); + + const char* unit; + errno = 0; + double duration_ms = strtod(args_cstr, (char**)&unit); + if(errno) return false; + if(duration_ms < 0) return false; + if(unit == args_cstr) return false; + + if(strcmp(unit, "") == 0) { + unit = default_unit; + if(!unit) unit = "ms"; + } + + uint32_t multiplier; + if(strcasecmp(unit, "ms") == 0) { + multiplier = 1; + } else if(strcasecmp(unit, "s") == 0) { + multiplier = 1000; + } else if(strcasecmp(unit, "m") == 0) { + multiplier = 60 * 1000; + } else if(strcasecmp(unit, "h") == 0) { + multiplier = 60 * 60 * 1000; + } else { + return false; + } + + const uint32_t max_pre_multiplication = UINT32_MAX / multiplier; + if(duration_ms > max_pre_multiplication) return false; + + *value = round(duration_ms * multiplier); + return true; +} diff --git a/lib/toolbox/args.h b/lib/toolbox/args.h index 556fd4a72..fecf33599 100644 --- a/lib/toolbox/args.h +++ b/lib/toolbox/args.h @@ -9,17 +9,26 @@ extern "C" { #endif /** Extract int value and trim arguments string - * - * @param args - arguments string - * @param word first argument, output + * + * @param args - arguments string + * @param value first argument, output * @return true - success * @return false - arguments string does not contain int */ bool args_read_int_and_trim(FuriString* args, int* value); +/** Extract float value and trim arguments string + * + * @param [in, out] args arguments string + * @param [out] value first argument + * @return true - success + * @return false - arguments string does not contain float + */ +bool args_read_float_and_trim(FuriString* args, float* value); + /** * @brief Extract first argument from arguments string and trim arguments string - * + * * @param args arguments string * @param word first argument, output * @return true - success @@ -29,7 +38,7 @@ bool args_read_string_and_trim(FuriString* args, FuriString* word); /** * @brief Extract the first quoted argument from the argument string and trim the argument string. If the argument is not quoted, calls args_read_string_and_trim. - * + * * @param args arguments string * @param word first argument, output, without quotes * @return true - success @@ -39,7 +48,7 @@ bool args_read_probably_quoted_string_and_trim(FuriString* args, FuriString* wor /** * @brief Convert hex ASCII values to byte array - * + * * @param args arguments string * @param bytes byte array pointer, output * @param bytes_count needed bytes count @@ -48,11 +57,23 @@ bool args_read_probably_quoted_string_and_trim(FuriString* args, FuriString* wor */ bool args_read_hex_bytes(FuriString* args, uint8_t* bytes, size_t bytes_count); +/** + * @brief Parses a duration value from a given string and converts it to milliseconds + * + * @param [in] args the input string containing the duration value. The string may include units (e.g., "10s", "0.5m"). + * @param [out] value pointer to store the parsed value in milliseconds + * @param [in] default_unit A default unit to be used if the input string does not contain a valid suffix. + * Supported units: `"ms"`, `"s"`, `"m"`, `"h"` + * If NULL, the function will assume milliseconds by default. + * @return `true` if the parsing and conversion succeeded, `false` otherwise. + */ +bool args_read_duration(FuriString* args, uint32_t* value, const char* default_unit); + /************************************ HELPERS ***************************************/ /** * @brief Get length of first word from arguments string - * + * * @param args arguments string * @return size_t length of first word */ @@ -60,7 +81,7 @@ size_t args_get_first_word_length(FuriString* args); /** * @brief Get length of arguments string - * + * * @param args arguments string * @return size_t length of arguments string */ @@ -68,7 +89,7 @@ size_t args_length(FuriString* args); /** * @brief Convert ASCII hex values to byte - * + * * @param hi_nibble ASCII hi nibble character * @param low_nibble ASCII low nibble character * @param byte byte pointer, output diff --git a/lib/toolbox/cli/cli_command.c b/lib/toolbox/cli/cli_command.c index a3c9ff292..60aa351e7 100644 --- a/lib/toolbox/cli/cli_command.c +++ b/lib/toolbox/cli/cli_command.c @@ -15,3 +15,18 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) { printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); } + +bool cli_sleep(PipeSide* side, uint32_t duration_in_ms) { + uint32_t passed_time = 0; + bool is_interrupted = false; + + do { + uint32_t left_time = duration_in_ms - passed_time; + uint32_t check_interval = left_time >= 100 ? 100 : left_time; + furi_delay_ms(check_interval); + passed_time += check_interval; + is_interrupted = cli_is_pipe_broken_or_is_etx_next_char(side); + } while(!is_interrupted && passed_time < duration_in_ms); + + return !is_interrupted; +} diff --git a/lib/toolbox/cli/cli_command.h b/lib/toolbox/cli/cli_command.h index 2d1d851d6..9d341f6d2 100644 --- a/lib/toolbox/cli/cli_command.h +++ b/lib/toolbox/cli/cli_command.h @@ -29,14 +29,14 @@ typedef enum { CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */ } CliCommandFlag; -/** +/** * @brief CLI command execution callback pointer - * + * * This callback will be called from a separate thread spawned just for your * command. The pipe will be installed as the thread's stdio, so you can use * `printf`, `getchar` and other standard functions to communicate with the * user. - * + * * @param [in] pipe Pipe that can be used to send and receive data. If * `CliCommandFlagDontAttachStdio` was not set, you can * also use standard C functions (printf, getc, etc.) to @@ -64,7 +64,7 @@ typedef struct { /** * @brief Detects if Ctrl+C has been pressed or session has been terminated - * + * * @param [in] side Pointer to pipe side given to the command thread * @warning This function also assumes that the pipe is installed as the * thread's stdio @@ -80,6 +80,21 @@ bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side); */ void cli_print_usage(const char* cmd, const char* usage, const char* arg); +/** + * @brief Pause for a specified duration or until Ctrl+C is pressed or the + * session is terminated. + * + * @param [in] side Pointer to pipe side given to the command thread. + * @param [in] duration_in_ms Duration of sleep in milliseconds. + * @return `true` if the sleep completed without interruption. + * @return `false` if interrupted. + * + * @warning This function also assumes that the pipe is installed as the + * thread's stdio. + * @warning This function will consume 0 or 1 bytes from the pipe. + */ +bool cli_sleep(PipeSide* side, uint32_t duration_in_ms); + #define CLI_COMMAND_INTERFACE(name, execute_callback, flags, stack_depth, app_id) \ static const CliCommandDescriptor cli_##name##_desc = { \ #name, \ diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index a89625080..27b65e202 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -546,6 +546,8 @@ Function,-,arc4random_uniform,__uint32_t,__uint32_t Function,+,args_char_to_hex,_Bool,"char, char, uint8_t*" Function,+,args_get_first_word_length,size_t,FuriString* Function,+,args_length,size_t,FuriString* +Function,+,args_read_duration,_Bool,"FuriString*, uint32_t*, const char*" +Function,+,args_read_float_and_trim,_Bool,"FuriString*, float*" Function,+,args_read_hex_bytes,_Bool,"FuriString*, uint8_t*, size_t" Function,+,args_read_int_and_trim,_Bool,"FuriString*, int*" Function,+,args_read_probably_quoted_string_and_trim,_Bool,"FuriString*, FuriString*" @@ -802,6 +804,7 @@ Function,+,cli_shell_free,void,CliShell* Function,+,cli_shell_join,void,CliShell* Function,+,cli_shell_set_prompt,void,"CliShell*, const char*" Function,+,cli_shell_start,void,CliShell* +Function,+,cli_sleep,_Bool,"PipeSide*, uint32_t" Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" @@ -2267,6 +2270,7 @@ Function,+,notification_internal_message,void,"NotificationApp*, const Notificat Function,+,notification_internal_message_block,void,"NotificationApp*, const NotificationSequence*" Function,+,notification_message,void,"NotificationApp*, const NotificationSequence*" Function,+,notification_message_block,void,"NotificationApp*, const NotificationSequence*" +Function,+,notification_messages_notes_frequency_from_name,float,const char* Function,-,nrand48,long,unsigned short[3] Function,+,number_input_alloc,NumberInput*, Function,+,number_input_free,void,NumberInput* diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index ef938ab27..de385684d 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -625,6 +625,8 @@ Function,-,arc4random_uniform,__uint32_t,__uint32_t Function,+,args_char_to_hex,_Bool,"char, char, uint8_t*" Function,+,args_get_first_word_length,size_t,FuriString* Function,+,args_length,size_t,FuriString* +Function,+,args_read_float_and_trim,_Bool,"FuriString*, float*" +Function,+,args_read_duration,_Bool,"FuriString*, uint32_t*, const char*" Function,+,args_read_hex_bytes,_Bool,"FuriString*, uint8_t*, size_t" Function,+,args_read_int_and_trim,_Bool,"FuriString*, int*" Function,+,args_read_probably_quoted_string_and_trim,_Bool,"FuriString*, FuriString*" @@ -881,6 +883,7 @@ Function,+,cli_shell_free,void,CliShell* Function,+,cli_shell_join,void,CliShell* Function,+,cli_shell_set_prompt,void,"CliShell*, const char*" Function,+,cli_shell_start,void,CliShell* +Function,+,cli_sleep,_Bool,"PipeSide*, uint32_t" Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" @@ -2919,6 +2922,7 @@ Function,+,notification_internal_message,void,"NotificationApp*, const Notificat Function,+,notification_internal_message_block,void,"NotificationApp*, const NotificationSequence*" Function,+,notification_message,void,"NotificationApp*, const NotificationSequence*" Function,+,notification_message_block,void,"NotificationApp*, const NotificationSequence*" +Function,+,notification_messages_notes_frequency_from_name,float,const char* Function,-,nrand48,long,unsigned short[3] Function,+,number_input_alloc,NumberInput*, Function,+,number_input_free,void,NumberInput*