From eea53491de6180710438ab7c19e84c4f98564329 Mon Sep 17 00:00:00 2001 From: RebornedBrain Date: Mon, 29 Sep 2025 13:34:49 +0300 Subject: [PATCH 1/4] [FL-3569] NFC CLI commands (#4158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: FuriThread stdin * ci: fix f18 * feat: stdio callback context * feat: FuriPipe * POTENTIALLY EXPLOSIVE pipe welding * fix: non-explosive welding * Revert welding * docs: furi_pipe * feat: pipe event loop integration * update f18 sdk * f18 * docs: make doxygen happy * fix: event loop not triggering when pipe attached to stdio * fix: partial stdout in pipe * allow simultaneous in and out subscription in event loop * feat: vcp i/o * feat: cli ansi stuffs and history * feat: more line editing * working but slow cli rewrite * restore previous speed after 4 days of debugging 🥲 * fix: cli_app_should_stop * fix: cli and event_loop memory leaks * style: remove commented out code * ci: fix pvs warnings * fix: unit tests, event_loop crash * ci: fix build * ci: silence pvs warning * feat: cli gpio * ci: fix formatting * Fix memory leak during event loop unsubscription * Event better memory leak fix * feat: cli completions * Merge remote-tracking branch 'origin/dev' into portasynthinca3/3928-cli-threads * merge fixups * temporarily exclude speaker_debug app * pvs and unit tests fixups * feat: commands in fals * move commands out of flash, code cleanup * ci: fix errors * fix: run commands in buffer when stopping session * speedup cli file transfer * fix f18 * separate cli_shell into modules * fix pvs warning * fix qflipper refusing to connect * remove temp debug logs * remove erroneous conclusion * Fix memory leak during event loop unsubscription * Event better memory leak fix * unit test for the fix * improve thread stdio callback signatures * pipe stdout timeout * update api symbols * fix f18, formatting * fix pvs warnings * increase stack size, hope to fix unit tests * cli completions * more key combos * commands in fals * move commands out of flash * ci: fix errors * speedup cli file transfer * merge fixups * fix f18 * cli: revert flag changes * cli: fix formatting * cli, fbt: loopback perf benchmark * thread, event_loop: subscribing to thread flags * cli: signal internal events using thread flags, improve performance * fix f18, formatting * event_loop: fix crash * storage_cli: increase write_chunk buffer size again * cli: explanation for order=0 * thread, event_loop: thread flags callback refactor * cli: increase stack size * cli: rename cli_app_should_stop -> cli_is_pipe_broken_or_is_etx_next_char * cli: use plain array instead of mlib for history * cli: prepend file name to static fns * cli: fix formatting * cli_shell: increase stack size * Now cli_shell can be customized with another motd and another command set * Added custom motd callback definition * Now user can alloc and free his own cli command set * cli_vcp can now restart shell with another command set * Help command modified to show available commands from different command sets * Api adjustement * Reworked nfc_cli to start new shell with another command set * Revert custom shell changes from vcp * Custom motd callback moved to cli_shell * Cli Shell now can be started from ongoing cli command * Help command moved to a separate function so it can be used for custom shell * Now nfc command spawns separate shell for further nfc commands * cli_shell: give up pipe to command thread * fix formatting * cli_shell: separate into toolbox * speaker_debug: fix * fix: format * Merge branch 'portasynthinca3/3928-3929-cli-fals-threads' into portasynthinca3/3965-cli_shell-toolbox * fix merge * fix. merge. * fix formatting * fix: cmd flags * fix: formatting * Added basic command descriptor structs and macros * Basic nfc commands definitions added * Nfc cli commands collection and functions added * Raw skeleton of nfc cli processor added * cli: increase default stack depth * New callbacks for ctx alloc / free added * nfc_cli moved to cli folder * Some more logic for command processor * Scanner command no works via command_processor * plugin manifest adj * Argument descriptors were removed, now only keys left * Some helper command function implemented * Command processor logic now mostly works * Added all parsers and dummy implementation of raw cmd * Now processor checks duplicated keys and treat them as errors * Some renamings * Arguments processing moved to separate function * Now command processor can reuse context of previuos command for the next one if it's allowed * can_reuse callback added for checking if context can be reused * command processor is now freed on nfc cli exit * Some cleanups * First working version of raw command * Now input data are placed directly to bit buffer * Added tag * Introduced request/response structs * Moved raw command to a separate folder * Moved some common types to header * Added protocol specific handlers for iso14a and felica * Opened felica crc header for referencing * Added handler for iso14443_3b * Opened iso15693_3_poller for referencing * Added iso15693_3 handler for raw command * NfcCliRawError enum introduced for response result * Refactored handlers implementation * Formatting functions now added as helpers * New printing result logic * Not present error value added to enum * Timeout added to raw command * Command processor now supports multivalue keys * Apdu command implementation added * NfcScanner moved to helpers and command now uses it * Helper now can format protocol names * Dump command added * Added some more functions to scanner helper * Dump main logic simplified * Dump handlers moved to protocols folder * Protocol parser added to simplify searching protocol by name * Protocol and key arguments added to dump command * Cleanups * Apdu now parses protocol using helper parser * Raw now parses protocol using helper parser * Wrong naming fix * Emulate command added to cli * Description added to action descriptor and command macros * Description field added to all commands * Removed unnecessary enum for commands * Added functions for formatting command and action info * Proper error messages and help added * Fix for unsupported single action command * Function renamed to more appropriate * Field command moved to all other commands * Cleanups * Nfc commands modified with new cli shell * Removed previous nfc_cli.c after merge * Removed nfc_cli.h header * Some renamings and cleanups * Some comments and instructions added * Some comments and instructions added * TODOs removed * Fix for missing parse callback * Added not implemented dummy for mfu actions, for now * Fix name mismatch * Remove unneeded header * Mfu command moved to separate folder, also raw info action logic added * Dictionary with id/vendors added to assets. It is used by nfc_cli_mfu_info_get_vendor function * One more unneeded header removed * Moved mfu info action to a separate file * Info action now uses sync mfu poller * mfu rdbl action added * wrbl action added for mfu command * Some formatting for rdbl command * Function for formatting mfu errors added * All mfu actions now show errors in the same way * Fix error with sync poller. Previously when read failed function returned ErrorNone, now it processes iso14a error to get proper value * Make PVS happy * Nfc cli now doesn't start if desktop app is running * Make action description look more common * Scanner now has -t key and can show detected protocol hierarchies * Apdu now checks max input payload data * Proper format * Proper error handling added to dump command * Timeout key added dump command * Fix merge issue * formatting * Pragma pack replaced with FURI_PACKED * Fix felica memory leak --------- Co-authored-by: Anna Antonenko Co-authored-by: Georgii Surkov Co-authored-by: あく Co-authored-by: hedger Co-authored-by: hedger --- applications/main/nfc/application.fam | 47 ++- .../cli/commands/apdu/nfc_cli_command_apdu.c | 311 ++++++++++++++ .../cli/commands/apdu/nfc_cli_command_apdu.h | 5 + .../iso14443_4a/nfc_cli_apdu_iso14443_4a.c | 37 ++ .../iso14443_4a/nfc_cli_apdu_iso14443_4a.h | 6 + .../iso14443_4b/nfc_cli_apdu_iso14443_4b.c | 37 ++ .../iso14443_4b/nfc_cli_apdu_iso14443_4b.h | 6 + .../iso15693_3/nfc_cli_apdu_iso15693_3.c | 37 ++ .../iso15693_3/nfc_cli_apdu_iso15693_3.h | 6 + .../nfc_cli_apdu_common_types.h | 20 + .../cli/commands/dump/nfc_cli_command_dump.c | 319 ++++++++++++++ .../cli/commands/dump/nfc_cli_command_dump.h | 5 + .../protocols/felica/nfc_cli_dump_felica.c | 32 ++ .../protocols/felica/nfc_cli_dump_felica.h | 5 + .../iso14443_3a/nfc_cli_dump_iso14443_3a.c | 25 ++ .../iso14443_3a/nfc_cli_dump_iso14443_3a.h | 5 + .../iso14443_3b/nfc_cli_dump_iso14443_3b.c | 27 ++ .../iso14443_3b/nfc_cli_dump_iso14443_3b.h | 5 + .../iso14443_4a/nfc_cli_dump_iso14443_4a.c | 26 ++ .../iso14443_4a/nfc_cli_dump_iso14443_4a.h | 5 + .../iso14443_4b/nfc_cli_dump_iso14443_4b.c | 25 ++ .../iso14443_4b/nfc_cli_dump_iso14443_4b.h | 5 + .../iso15693_3/nfc_cli_dump_iso15693_3.c | 26 ++ .../iso15693_3/nfc_cli_dump_iso15693_3.h | 5 + .../mf_classic/nfc_cli_dump_mf_classic.c | 54 +++ .../mf_classic/nfc_cli_dump_mf_classic.h | 5 + .../mf_desfire/nfc_cli_dump_mf_desfire.c | 27 ++ .../mf_desfire/nfc_cli_dump_mf_desfire.h | 5 + .../protocols/mf_plus/nfc_cli_dump_mf_plus.c | 31 ++ .../protocols/mf_plus/nfc_cli_dump_mf_plus.h | 5 + .../nfc_cli_dump_mf_ultralight.c | 39 ++ .../nfc_cli_dump_mf_ultralight.h | 5 + .../protocols/nfc_cli_dump_common_types.h | 59 +++ .../dump/protocols/slix/nfc_cli_dump_slix.c | 26 ++ .../dump/protocols/slix/nfc_cli_dump_slix.h | 5 + .../protocols/st25tb/nfc_cli_dump_st25tb.c | 29 ++ .../protocols/st25tb/nfc_cli_dump_st25tb.h | 5 + .../nfc/cli/commands/helpers/nfc_cli_format.c | 61 +++ .../nfc/cli/commands/helpers/nfc_cli_format.h | 16 + .../helpers/nfc_cli_protocol_parser.c | 58 +++ .../helpers/nfc_cli_protocol_parser.h | 21 + .../cli/commands/helpers/nfc_cli_scanner.c | 96 +++++ .../cli/commands/helpers/nfc_cli_scanner.h | 21 + .../cli/commands/mfu/nfc_cli_action_info.c | 227 ++++++++++ .../cli/commands/mfu/nfc_cli_action_info.h | 7 + .../cli/commands/mfu/nfc_cli_action_rdbl.c | 52 +++ .../cli/commands/mfu/nfc_cli_action_rdbl.h | 8 + .../cli/commands/mfu/nfc_cli_action_wrbl.c | 71 ++++ .../cli/commands/mfu/nfc_cli_action_wrbl.h | 9 + .../cli/commands/mfu/nfc_cli_command_mfu.c | 77 ++++ .../cli/commands/mfu/nfc_cli_command_mfu.h | 5 + .../cli/commands/nfc_cli_command_emulate.c | 126 ++++++ .../cli/commands/nfc_cli_command_emulate.h | 5 + .../nfc/cli/commands/nfc_cli_command_field.c | 28 ++ .../nfc/cli/commands/nfc_cli_command_field.h | 5 + .../cli/commands/nfc_cli_command_scanner.c | 97 +++++ .../cli/commands/nfc_cli_command_scanner.h | 5 + .../cli/commands/raw/nfc_cli_command_raw.c | 352 ++++++++++++++++ .../cli/commands/raw/nfc_cli_command_raw.h | 5 + .../felica/nfc_cli_raw_felica.c | 101 +++++ .../felica/nfc_cli_raw_felica.h | 8 + .../iso14443_3a/nfc_cli_raw_iso14443_3a.c | 81 ++++ .../iso14443_3a/nfc_cli_raw_iso14443_3a.h | 8 + .../iso14443_3b/nfc_cli_raw_iso14443_3b.c | 121 ++++++ .../iso14443_3b/nfc_cli_raw_iso14443_3b.h | 8 + .../iso15693_3/nfc_cli_raw_iso15693_3.c | 102 +++++ .../iso15693_3/nfc_cli_raw_iso15693_3.h | 8 + .../nfc_cli_raw_common_types.h | 28 ++ applications/main/nfc/cli/nfc_cli.c | 124 ++++++ .../main/nfc/cli/nfc_cli_command_base.h | 69 ++++ .../main/nfc/cli/nfc_cli_command_base_i.h | 130 ++++++ .../main/nfc/cli/nfc_cli_command_processor.c | 388 ++++++++++++++++++ .../main/nfc/cli/nfc_cli_command_processor.h | 15 + applications/main/nfc/cli/nfc_cli_commands.c | 161 ++++++++ applications/main/nfc/cli/nfc_cli_commands.h | 27 ++ applications/main/nfc/nfc_cli.c | 67 --- .../main/nfc/resources/nfc/assets/vendors.nfc | 115 ++++++ applications/services/cli/cli_vcp.h | 1 - lib/nfc/SConscript | 2 + lib/nfc/protocols/felica/felica.c | 1 + lib/nfc/protocols/felica/felica.h | 4 +- .../mf_ultralight/mf_ultralight_poller_sync.c | 3 +- lib/toolbox/cli/shell/cli_shell.c | 1 - targets/f7/api_symbols.csv | 12 + 84 files changed, 4180 insertions(+), 79 deletions(-) create mode 100644 applications/main/nfc/cli/commands/apdu/nfc_cli_command_apdu.c create mode 100644 applications/main/nfc/cli/commands/apdu/nfc_cli_command_apdu.h create mode 100644 applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.c create mode 100644 applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.h create mode 100644 applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.c create mode 100644 applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.h create mode 100644 applications/main/nfc/cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.c create mode 100644 applications/main/nfc/cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.h create mode 100644 applications/main/nfc/cli/commands/apdu/protocol_handlers/nfc_cli_apdu_common_types.h create mode 100644 applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.c create mode 100644 applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/felica/nfc_cli_dump_felica.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/nfc_cli_dump_common_types.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/slix/nfc_cli_dump_slix.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/slix/nfc_cli_dump_slix.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.h create mode 100644 applications/main/nfc/cli/commands/helpers/nfc_cli_format.c create mode 100644 applications/main/nfc/cli/commands/helpers/nfc_cli_format.h create mode 100644 applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.c create mode 100644 applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.h create mode 100644 applications/main/nfc/cli/commands/helpers/nfc_cli_scanner.c create mode 100644 applications/main/nfc/cli/commands/helpers/nfc_cli_scanner.h create mode 100644 applications/main/nfc/cli/commands/mfu/nfc_cli_action_info.c create mode 100644 applications/main/nfc/cli/commands/mfu/nfc_cli_action_info.h create mode 100644 applications/main/nfc/cli/commands/mfu/nfc_cli_action_rdbl.c create mode 100644 applications/main/nfc/cli/commands/mfu/nfc_cli_action_rdbl.h create mode 100644 applications/main/nfc/cli/commands/mfu/nfc_cli_action_wrbl.c create mode 100644 applications/main/nfc/cli/commands/mfu/nfc_cli_action_wrbl.h create mode 100644 applications/main/nfc/cli/commands/mfu/nfc_cli_command_mfu.c create mode 100644 applications/main/nfc/cli/commands/mfu/nfc_cli_command_mfu.h create mode 100644 applications/main/nfc/cli/commands/nfc_cli_command_emulate.c create mode 100644 applications/main/nfc/cli/commands/nfc_cli_command_emulate.h create mode 100644 applications/main/nfc/cli/commands/nfc_cli_command_field.c create mode 100644 applications/main/nfc/cli/commands/nfc_cli_command_field.h create mode 100644 applications/main/nfc/cli/commands/nfc_cli_command_scanner.c create mode 100644 applications/main/nfc/cli/commands/nfc_cli_command_scanner.h create mode 100644 applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.c create mode 100644 applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.h create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.c create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.h create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.c create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.h create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.c create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.h create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.c create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.h create mode 100644 applications/main/nfc/cli/commands/raw/protocol_handlers/nfc_cli_raw_common_types.h create mode 100644 applications/main/nfc/cli/nfc_cli.c create mode 100644 applications/main/nfc/cli/nfc_cli_command_base.h create mode 100644 applications/main/nfc/cli/nfc_cli_command_base_i.h create mode 100644 applications/main/nfc/cli/nfc_cli_command_processor.c create mode 100644 applications/main/nfc/cli/nfc_cli_command_processor.h create mode 100644 applications/main/nfc/cli/nfc_cli_commands.c create mode 100644 applications/main/nfc/cli/nfc_cli_commands.h delete mode 100644 applications/main/nfc/nfc_cli.c create mode 100644 applications/main/nfc/resources/nfc/assets/vendors.nfc diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index a9c8808a8..461cc6362 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -8,11 +8,7 @@ App( stack_size=5 * 1024, order=30, resources="resources", - sources=[ - "*.c*", - "!plugins", - "!nfc_cli.c", - ], + sources=["*.c*", "!plugins", "!nfc_cli.c", "!cli"], fap_libs=["assets", "mbedtls"], fap_icon="icon.png", fap_category="NFC", @@ -263,7 +259,46 @@ App( apptype=FlipperAppType.PLUGIN, entry_point="cli_nfc_ep", requires=["cli"], - sources=["nfc_cli.c"], + sources=[ + "helpers/mf_classic_key_cache.c", + "helpers/protocol_support/iso14443_3a/iso14443_3a_render.c", + "helpers/protocol_support/mf_ultralight/mf_ultralight_render.c", + "cli/nfc_cli.c", + "cli/nfc_cli_commands.c", + "cli/nfc_cli_command_processor.c", + "cli/commands/helpers/nfc_cli_format.c", + "cli/commands/helpers/nfc_cli_scanner.c", + "cli/commands/helpers/nfc_cli_protocol_parser.c", + "cli/commands/raw/nfc_cli_command_raw.c", + "cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.c", + "cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.c", + "cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.c", + "cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.c", + "cli/commands/apdu/nfc_cli_command_apdu.c", + "cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.c", + "cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.c", + "cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.c", + "cli/commands/dump/nfc_cli_command_dump.c", + "cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.c", + "cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.c", + "cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.c", + "cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.c", + "cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.c", + "cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.c", + "cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.c", + "cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.c", + "cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.c", + "cli/commands/dump/protocols/slix/nfc_cli_dump_slix.c", + "cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.c", + "cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c", + "cli/commands/mfu/nfc_cli_command_mfu.c", + "cli/commands/mfu/nfc_cli_action_info.c", + "cli/commands/mfu/nfc_cli_action_rdbl.c", + "cli/commands/mfu/nfc_cli_action_wrbl.c", + "cli/commands/nfc_cli_command_emulate.c", + "cli/commands/nfc_cli_command_scanner.c", + "cli/commands/nfc_cli_command_field.c", + ], ) App( diff --git a/applications/main/nfc/cli/commands/apdu/nfc_cli_command_apdu.c b/applications/main/nfc/cli/commands/apdu/nfc_cli_command_apdu.c new file mode 100644 index 000000000..cbbb86681 --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/nfc_cli_command_apdu.c @@ -0,0 +1,311 @@ +#include "nfc_cli_command_apdu.h" +#include "../helpers/nfc_cli_format.h" +#include "../helpers/nfc_cli_protocol_parser.h" + +#include "protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.h" +#include "protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.h" +#include "protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.h" + +#include +#include +#include + +#include +#include +#include + +#define TAG "APDU" + +#define NFC_CLI_PROTOCOL_SUPPORT_MAX_BUFFER_SIZE (256) + +typedef NfcCommand ( + *NfcCliApduProtocolHandler)(NfcGenericEvent event, NfcCliApduRequestResponse* instance); + +static const char* raw_error_names[] = { + [NfcCliApduErrorNone] = "None", + [NfcCliApduErrorTimeout] = "Timeout", + [NfcCliApduErrorProtocol] = "Internal protocol", + [NfcCliApduErrorWrongCrc] = "Wrong CRC", + [NfcCliApduErrorNotPresent] = "No card", +}; + +typedef enum { + NfcCliProtocolRequestTypeNormalExecute, + NfcCliProtocolRequestTypeAbort, +} NfcCliProtocolRequestType; + +typedef struct { + uint8_t* data; + size_t size; +} NfcCliApduData; + +static void ApduItem_init(NfcCliApduData* item) { + item->size = 0; + item->data = NULL; +} + +static void ApduItem_init_set(NfcCliApduData* item, const NfcCliApduData* src) { + item->data = malloc(src->size); + item->size = src->size; + memcpy(item->data, src->data, src->size); +} + +static void ApduItem_set(NfcCliApduData* item, const NfcCliApduData* src) { + if(item->data == NULL) { + item->data = malloc(src->size); + } else if(item->size != src->size) { + uint8_t* buf = realloc(item->data, src->size); + furi_check(buf); + item->data = buf; + } + + item->size = src->size; + memcpy(item->data, src->data, src->size); +} + +static void ApduItem_clear(NfcCliApduData* item) { + if(item->data) free(item->data); + item->data = NULL; + item->size = 0; +} + +ARRAY_DEF( + NfcCliApduItemArray, + NfcCliApduData, + (INIT(API_2(ApduItem_init)), + SET(API_6(ApduItem_set)), + INIT_SET(API_6(ApduItem_init_set)), + CLEAR(API_2(ApduItem_clear)))) + +typedef struct { + Nfc* nfc; + bool auto_detect; + NfcCliApduItemArray_t apdu; + NfcCliApduRequestResponse data; + FuriSemaphore* sem_done; + FuriMessageQueue* input_queue; +} NfcCliApduContext; + +static NfcCliActionContext* nfc_cli_apdu_alloc_ctx(Nfc* nfc) { + furi_assert(nfc); + NfcCliApduContext* instance = malloc(sizeof(NfcCliApduContext)); + instance->nfc = nfc; + instance->data.protocol = NfcProtocolInvalid; + instance->auto_detect = true; + NfcCliApduItemArray_init(instance->apdu); + instance->data.rx_buffer = bit_buffer_alloc(NFC_CLI_PROTOCOL_SUPPORT_MAX_BUFFER_SIZE); + instance->data.tx_buffer = bit_buffer_alloc(NFC_CLI_PROTOCOL_SUPPORT_MAX_BUFFER_SIZE); + + instance->sem_done = furi_semaphore_alloc(1, 0); + instance->input_queue = furi_message_queue_alloc(1, sizeof(NfcCliProtocolRequestType)); + return instance; +} + +static void nfc_cli_apdu_free_ctx(NfcCliActionContext* action_ctx) { + furi_assert(action_ctx); + NfcCliApduContext* instance = action_ctx; + + instance->nfc = NULL; + NfcCliApduItemArray_clear(instance->apdu); + + bit_buffer_free(instance->data.rx_buffer); + bit_buffer_free(instance->data.tx_buffer); + furi_semaphore_free(instance->sem_done); + furi_message_queue_free(instance->input_queue); + free(instance); +} + +static inline void nfc_cli_apdu_print_result(const NfcCliApduContext* instance) { + nfc_cli_printf_array( + bit_buffer_get_data(instance->data.tx_buffer), + bit_buffer_get_size_bytes(instance->data.tx_buffer), + "\r\nTx: "); + + if(instance->data.result != NfcCliApduErrorNone) + printf("\r\nError: \"%s\"\r\n", raw_error_names[instance->data.result]); + + size_t rx_size = bit_buffer_get_size_bytes(instance->data.rx_buffer); + if(rx_size > 0) { + nfc_cli_printf_array( + bit_buffer_get_data(instance->data.rx_buffer), + bit_buffer_get_size_bytes(instance->data.rx_buffer), + "\r\nRx: "); + printf("\r\n"); + } +} + +static NfcProtocol nfc_cli_apdu_protocol_autodetect(Nfc* nfc) { + const NfcProtocol supported_protocols[] = { + NfcProtocolIso14443_4a, + NfcProtocolIso14443_4b, + NfcProtocolIso15693_3, + }; + + const char* supported_names[] = {"Iso14443_4a", "Iso14443_4b", "Iso15693_3"}; + + NfcProtocol protocol = NfcProtocolInvalid; + for(uint8_t i = 0; i < COUNT_OF(supported_protocols); i++) { + NfcPoller* poller = nfc_poller_alloc(nfc, supported_protocols[i]); + bool is_detected = nfc_poller_detect(poller); + nfc_poller_free(poller); + if(is_detected) { + protocol = supported_protocols[i]; + printf("Detected tag: %s\r\n", supported_names[i]); + break; + } + } + return protocol; +} + +static NfcCliApduProtocolHandler nfc_cli_apdu_poller_get_handler(NfcProtocol protocol) { + if(protocol == NfcProtocolIso14443_4a) + return nfc_cli_apdu_iso14443_4a_handler; + else if(protocol == NfcProtocolIso14443_4b) + return nfc_cli_apdu_iso14443_4b_handler; + else if(protocol == NfcProtocolIso15693_3) + return nfc_cli_apdu_iso15693_3_handler; + else + return NULL; +} + +static NfcCommand nfc_cli_apdu_poller_callback(NfcGenericEvent event, void* context) { + NfcCliApduContext* instance = context; + + FURI_LOG_D(TAG, "Poller callback"); + NfcCliProtocolRequestType request_type = NfcCliProtocolRequestTypeAbort; + furi_message_queue_get(instance->input_queue, &request_type, FuriWaitForever); + + NfcCommand command = NfcCommandStop; + if(request_type == NfcCliProtocolRequestTypeAbort) { + FURI_LOG_D(TAG, "Aborting poller callback"); + } else { + NfcCliApduProtocolHandler handler = + nfc_cli_apdu_poller_get_handler(instance->data.protocol); + if(handler) command = handler(event, &instance->data); + } + furi_semaphore_release(instance->sem_done); + return command; +} + +static void nfc_cli_apdu_execute(PipeSide* pipe, void* context) { + UNUSED(pipe); + furi_assert(context); + NfcCliApduContext* instance = context; + + if(instance->auto_detect) { + instance->data.protocol = nfc_cli_apdu_protocol_autodetect(instance->nfc); + } + + if(instance->data.protocol != NfcProtocolInvalid) { + NfcPoller* poller = nfc_poller_alloc(instance->nfc, instance->data.protocol); + + NfcCliProtocolRequestType request_type = NfcCliProtocolRequestTypeNormalExecute; + nfc_poller_start(poller, nfc_cli_apdu_poller_callback, instance); + + NfcCliApduItemArray_it_t it; + for(NfcCliApduItemArray_it(it, instance->apdu); !NfcCliApduItemArray_end_p(it); + NfcCliApduItemArray_next(it)) { + const NfcCliApduData* item = NfcCliApduItemArray_cref(it); + bit_buffer_copy_bytes(instance->data.tx_buffer, item->data, item->size); + bit_buffer_reset(instance->data.rx_buffer); + + furi_message_queue_put(instance->input_queue, &request_type, FuriWaitForever); + furi_semaphore_acquire(instance->sem_done, FuriWaitForever); + nfc_cli_apdu_print_result(instance); + if(instance->data.result != NfcCliApduErrorNone) break; + } + + request_type = NfcCliProtocolRequestTypeAbort; + furi_message_queue_put(instance->input_queue, &request_type, FuriWaitForever); + nfc_poller_stop(poller); + nfc_poller_free(poller); + } +} + +static const NfcProtocolNameValuePair supported_protocols[] = { + {.name = "4a", .value = NfcProtocolIso14443_4a}, + {.name = "4b", .value = NfcProtocolIso14443_4b}, + {.name = "15", .value = NfcProtocolIso15693_3}, +}; + +static bool nfc_cli_apdu_parse_protocol(FuriString* value, void* output) { + NfcCliApduContext* ctx = output; + ctx->auto_detect = false; + + NfcCliProtocolParser* parser = + nfc_cli_protocol_parser_alloc(supported_protocols, COUNT_OF(supported_protocols)); + + bool result = nfc_cli_protocol_parser_get(parser, value, &ctx->data.protocol); + + nfc_cli_protocol_parser_free(parser); + return result; +} + +static bool nfc_cli_apdu_parse_data(FuriString* value, void* output) { + NfcCliApduContext* ctx = output; + + bool result = false; + FuriString* word = furi_string_alloc(); + + while(args_read_string_and_trim(value, word)) { + size_t len = furi_string_size(word); + if(len % 2 != 0) break; + + size_t data_length = len / 2; + + const size_t max_len = UINT16_MAX; + if(data_length > max_len) { + printf( + ANSI_FG_RED "\r\nData payload is too long, max length = %d bytes\r\n" ANSI_RESET, + max_len); + break; + } + + NfcCliApduData* item = NfcCliApduItemArray_push_new(ctx->apdu); + item->size = data_length; + item->data = malloc(data_length); + result = args_read_hex_bytes(word, item->data, item->size); + } + furi_string_free(word); + + return result; +} + +const NfcCliKeyDescriptor apdu_keys[] = { + { + .long_name = "protocol", + .short_name = "p", + .description = "set protocol (4a, 4b, 15) directly, otherwise autodetected", + .features = {.parameter = true, .required = false}, + .parse = nfc_cli_apdu_parse_protocol, + }, + { + .long_name = "data", + .short_name = "d", + .description = "apdu payloads in format p1 p2 p3", + .features = {.parameter = true, .multivalue = true, .required = true}, + .parse = nfc_cli_apdu_parse_data, + }, +}; + +const NfcCliActionDescriptor apdu_action = { + .name = "apdu", + .description = "Send APDU data to iso14443_4a, iso14443_4b or iso15693_3", + .alloc = nfc_cli_apdu_alloc_ctx, + .free = nfc_cli_apdu_free_ctx, + .execute = nfc_cli_apdu_execute, + .key_count = COUNT_OF(apdu_keys), + .keys = apdu_keys, +}; + +const NfcCliActionDescriptor* apdu_actions_collection[] = {&apdu_action}; + +//Command descriptor +ADD_NFC_CLI_COMMAND(apdu, "", apdu_actions_collection); + +//Command usage: apdu +//Command examples: +//apdu -d 00a404000e325041592e5359532e444446303100 00A4040008A000000333010102 +//apdu -p 4a -d 00a404000e325041592e5359532e444446303100 00A4040008A000000333010102 +//apdu -p 4b -d 00a404000e325041592e5359532e444446303100 00A4040008A000000333010102 +//apdu -p 15 -d 00a404000e325041592e5359532e444446303100 00A4040008A000000333010102 diff --git a/applications/main/nfc/cli/commands/apdu/nfc_cli_command_apdu.h b/applications/main/nfc/cli/commands/apdu/nfc_cli_command_apdu.h new file mode 100644 index 000000000..84e4ba5f6 --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/nfc_cli_command_apdu.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../../nfc_cli_command_base_i.h" + +extern const NfcCliCommandDescriptor apdu_cmd; diff --git a/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.c b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.c new file mode 100644 index 000000000..124ba46e3 --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.c @@ -0,0 +1,37 @@ +#include "nfc_cli_apdu_iso14443_4a.h" +#include "../../../helpers/nfc_cli_format.h" + +#include +#include + +#define TAG "ISO14A_4A" + +#define BIT_BUFFER_EMPTY(buffer) ((bit_buffer_get_size_bytes(buffer) == 0)) + +static NfcCliApduError nfc_cli_apdu_iso14443_4a_process_error(Iso14443_4aError error) { + switch(error) { + case Iso14443_4aErrorNone: + return NfcCliApduErrorNone; + case Iso14443_4aErrorTimeout: + return NfcCliApduErrorTimeout; + case Iso14443_4aErrorNotPresent: + return NfcCliApduErrorNotPresent; + default: + return NfcCliApduErrorProtocol; + } +} + +NfcCommand + nfc_cli_apdu_iso14443_4a_handler(NfcGenericEvent event, NfcCliApduRequestResponse* instance) { + Iso14443_4aPollerEvent* iso14443_4a_event = event.event_data; + NfcCommand command = NfcCommandContinue; + + if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeReady) { + Iso14443_4aError err = iso14443_4a_poller_send_block( + event.instance, instance->tx_buffer, instance->rx_buffer); + instance->result = nfc_cli_apdu_iso14443_4a_process_error(err); + if(err != Iso14443_4aErrorNone) command = NfcCommandStop; + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.h b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.h new file mode 100644 index 000000000..907aafeaf --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4a/nfc_cli_apdu_iso14443_4a.h @@ -0,0 +1,6 @@ +#pragma once + +#include "../nfc_cli_apdu_common_types.h" + +NfcCommand + nfc_cli_apdu_iso14443_4a_handler(NfcGenericEvent event, NfcCliApduRequestResponse* instance); diff --git a/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.c b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.c new file mode 100644 index 000000000..eee635515 --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.c @@ -0,0 +1,37 @@ +#include "nfc_cli_apdu_iso14443_4b.h" +#include "../../../helpers/nfc_cli_format.h" + +#include +#include + +#define TAG "ISO14A_4B" + +#define BIT_BUFFER_EMPTY(buffer) ((bit_buffer_get_size_bytes(buffer) == 0)) + +static NfcCliApduError nfc_cli_apdu_iso14443_4b_process_error(Iso14443_4bError error) { + switch(error) { + case Iso14443_4bErrorNone: + return NfcCliApduErrorNone; + case Iso14443_4bErrorTimeout: + return NfcCliApduErrorTimeout; + case Iso14443_4bErrorNotPresent: + return NfcCliApduErrorNotPresent; + default: + return NfcCliApduErrorProtocol; + } +} + +NfcCommand + nfc_cli_apdu_iso14443_4b_handler(NfcGenericEvent event, NfcCliApduRequestResponse* instance) { + Iso14443_4bPollerEvent* iso14443_4b_event = event.event_data; + NfcCommand command = NfcCommandContinue; + + if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeReady) { + Iso14443_4bError err = iso14443_4b_poller_send_block( + event.instance, instance->tx_buffer, instance->rx_buffer); + instance->result = nfc_cli_apdu_iso14443_4b_process_error(err); + if(err != Iso14443_4bErrorNone) command = NfcCommandStop; + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.h b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.h new file mode 100644 index 000000000..e8304f497 --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso14443_4b/nfc_cli_apdu_iso14443_4b.h @@ -0,0 +1,6 @@ +#pragma once + +#include "../nfc_cli_apdu_common_types.h" + +NfcCommand + nfc_cli_apdu_iso14443_4b_handler(NfcGenericEvent event, NfcCliApduRequestResponse* instance); diff --git a/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.c b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.c new file mode 100644 index 000000000..bd817d1f6 --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.c @@ -0,0 +1,37 @@ +#include "nfc_cli_apdu_iso15693_3.h" +#include "../../../helpers/nfc_cli_format.h" + +#include +#include + +#define TAG "ISO15" + +#define BIT_BUFFER_EMPTY(buffer) ((bit_buffer_get_size_bytes(buffer) == 0)) + +static NfcCliApduError nfc_cli_apdu_iso15693_3_process_error(Iso15693_3Error error) { + switch(error) { + case Iso15693_3ErrorNone: + return NfcCliApduErrorNone; + case Iso15693_3ErrorTimeout: + return NfcCliApduErrorTimeout; + case Iso15693_3ErrorNotPresent: + return NfcCliApduErrorNotPresent; + default: + return NfcCliApduErrorProtocol; + } +} + +NfcCommand + nfc_cli_apdu_iso15693_3_handler(NfcGenericEvent event, NfcCliApduRequestResponse* instance) { + Iso15693_3PollerEvent* iso15693_3_event = event.event_data; + NfcCommand command = NfcCommandContinue; + + if(iso15693_3_event->type == Iso15693_3PollerEventTypeReady) { + Iso15693_3Error err = iso15693_3_poller_send_frame( + event.instance, instance->tx_buffer, instance->rx_buffer, ISO15693_3_FDT_POLL_FC); + instance->result = nfc_cli_apdu_iso15693_3_process_error(err); + if(err != Iso15693_3ErrorNone) command = NfcCommandStop; + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.h b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.h new file mode 100644 index 000000000..d93626918 --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/protocol_handlers/iso15693_3/nfc_cli_apdu_iso15693_3.h @@ -0,0 +1,6 @@ +#pragma once + +#include "../nfc_cli_apdu_common_types.h" + +NfcCommand + nfc_cli_apdu_iso15693_3_handler(NfcGenericEvent event, NfcCliApduRequestResponse* instance); diff --git a/applications/main/nfc/cli/commands/apdu/protocol_handlers/nfc_cli_apdu_common_types.h b/applications/main/nfc/cli/commands/apdu/protocol_handlers/nfc_cli_apdu_common_types.h new file mode 100644 index 000000000..125d26ca5 --- /dev/null +++ b/applications/main/nfc/cli/commands/apdu/protocol_handlers/nfc_cli_apdu_common_types.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +typedef enum { + NfcCliApduErrorNone, + NfcCliApduErrorTimeout, + NfcCliApduErrorNotPresent, + NfcCliApduErrorWrongCrc, + NfcCliApduErrorProtocol, +} NfcCliApduError; + +typedef struct { + NfcProtocol protocol; + BitBuffer* tx_buffer; + BitBuffer* rx_buffer; + NfcCliApduError result; +} NfcCliApduRequestResponse; diff --git a/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.c b/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.c new file mode 100644 index 000000000..2f968b58e --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.c @@ -0,0 +1,319 @@ +#include "nfc_cli_command_dump.h" +#include "protocols/nfc_cli_dump_common_types.h" +#include "../helpers/nfc_cli_format.h" +#include "../helpers/nfc_cli_protocol_parser.h" +#include "../helpers/nfc_cli_scanner.h" + +#include "protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.h" +#include "protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.h" +#include "protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.h" +#include "protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.h" +#include "protocols/iso15693_3/nfc_cli_dump_iso15693_3.h" +#include "protocols/mf_classic/nfc_cli_dump_mf_classic.h" +#include "protocols/mf_desfire/nfc_cli_dump_mf_desfire.h" +#include "protocols/mf_plus/nfc_cli_dump_mf_plus.h" +#include "protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.h" +#include "protocols/slix/nfc_cli_dump_slix.h" +#include "protocols/st25tb/nfc_cli_dump_st25tb.h" +#include "protocols/felica/nfc_cli_dump_felica.h" + +#include +#include +#include +#include +#include + +#define NFC_DEFAULT_FOLDER EXT_PATH("nfc") +#define NFC_FILE_EXTENSION ".nfc" +#define NFC_CLI_DEFAULT_FILENAME_PREFIX "dump" + +#define NFC_CLI_DUMP_DEFAULT_TIMEOUT (5000) + +#define TAG "DUMP" + +static const char* nfc_cli_dump_error_names[NfcCliDumpErrorNum] = { + [NfcCliDumpErrorNone] = "", + [NfcCliDumpErrorNotPresent] = "card not present", + [NfcCliDumpErrorAuthFailed] = "authentication failed", + [NfcCliDumpErrorTimeout] = "timeout", + [NfcCliDumpErrorFailedToRead] = "failed to read", +}; + +static NfcCliActionContext* nfc_cli_dump_alloc_ctx(Nfc* nfc) { + furi_assert(nfc); + NfcCliDumpContext* instance = malloc(sizeof(NfcCliDumpContext)); + instance->nfc = nfc; + instance->file_path = furi_string_alloc(); + instance->storage = furi_record_open(RECORD_STORAGE); + instance->sem_done = furi_semaphore_alloc(1, 0); + instance->nfc_device = nfc_device_alloc(); + instance->desired_protocol = NfcProtocolInvalid; + instance->auth_ctx.skip_auth = true; + instance->auth_ctx.key_size = 0; + instance->timeout = NFC_CLI_DUMP_DEFAULT_TIMEOUT; + + instance->mfc_key_cache = mf_classic_key_cache_alloc(); + instance->scanner = nfc_cli_scanner_alloc(nfc); + return instance; +} + +static void nfc_cli_dump_free_ctx(NfcCliActionContext* ctx) { + furi_assert(ctx); + NfcCliDumpContext* instance = ctx; + instance->desired_protocol = NfcProtocolInvalid; + furi_string_free(instance->file_path); + instance->nfc = NULL; + furi_record_close(RECORD_STORAGE); + furi_semaphore_free(instance->sem_done); + nfc_device_free(instance->nfc_device); + + mf_classic_key_cache_free(instance->mfc_key_cache); + nfc_cli_scanner_free(instance->scanner); + free(instance); +} + +static bool nfc_cli_dump_parse_filename_key(FuriString* value, void* output) { + furi_assert(value); + furi_assert(output); + NfcCliDumpContext* ctx = output; + furi_string_set(ctx->file_path, value); + return true; +} + +NfcGenericCallback protocol_poller_callbacks[NfcProtocolNum] = { + [NfcProtocolMfUltralight] = nfc_cli_dump_poller_callback_mf_ultralight, + [NfcProtocolMfClassic] = nfc_cli_dump_poller_callback_mf_classic, + [NfcProtocolFelica] = nfc_cli_dump_poller_callback_felica, + [NfcProtocolIso14443_3a] = nfc_cli_dump_poller_callback_iso14443_3a, + [NfcProtocolIso14443_3b] = nfc_cli_dump_poller_callback_iso14443_3b, + [NfcProtocolIso14443_4a] = nfc_cli_dump_poller_callback_iso14443_4a, + [NfcProtocolIso14443_4b] = nfc_cli_dump_poller_callback_iso14443_4b, + [NfcProtocolIso15693_3] = nfc_cli_dump_poller_callback_iso15693_3, + [NfcProtocolSlix] = nfc_cli_dump_poller_callback_slix, + [NfcProtocolMfDesfire] = nfc_cli_dump_poller_callback_mf_desfire, + [NfcProtocolMfPlus] = nfc_cli_dump_poller_callback_mf_plus, + [NfcProtocolSt25tb] = nfc_cli_dump_poller_callback_st25tb, +}; + +static void nfc_cli_dump_generate_filename(FuriString* file_path) { + furi_string_set_str(file_path, NFC_DEFAULT_FOLDER); + DateTime dt; + furi_hal_rtc_get_datetime(&dt); + furi_string_cat_printf( + file_path, + "/%s-%.4d%.2d%.2d-%.2d%.2d%.2d%s", + NFC_CLI_DEFAULT_FILENAME_PREFIX, + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + NFC_FILE_EXTENSION); +} + +static bool nfc_cli_dump_check_filepath_valid(FuriString* file_path, Storage* storage) { + bool file_exists = false; + bool dir_exists = false; + + FuriString* buf = furi_string_alloc(); + + path_extract_dirname(furi_string_get_cstr(file_path), buf); + dir_exists = storage_dir_exists(storage, furi_string_get_cstr(buf)); + file_exists = storage_file_exists(storage, furi_string_get_cstr(file_path)); + + bool result = true; + if(!dir_exists) { + printf(ANSI_FG_RED "Path \'%s\' doesn't exist\r\n" ANSI_RESET, furi_string_get_cstr(buf)); + result = false; + } else if(file_exists) { + printf( + ANSI_FG_RED "File \'%s\' already exists\r\n" ANSI_RESET, + furi_string_get_cstr(file_path)); + result = false; + } + furi_string_free(buf); + + return result; +} + +static bool nfc_cli_dump_process_filename(NfcCliDumpContext* instance) { + bool result = false; + if(furi_string_empty(instance->file_path)) { + nfc_cli_dump_generate_filename(instance->file_path); + result = true; + } else { + result = nfc_cli_dump_check_filepath_valid(instance->file_path, instance->storage); + } + return result; +} + +static size_t nfc_cli_dump_set_protocol(NfcCliDumpContext* instance) { + size_t protocol_count = 0; + if(instance->desired_protocol != NfcProtocolInvalid) { + protocol_count = 1; + } else { + if(!nfc_cli_scanner_detect_protocol(instance->scanner, instance->timeout)) { + NfcCliDumpError error = NfcCliDumpErrorTimeout; + printf(ANSI_FG_RED "Error: %s\r\n" ANSI_RESET, nfc_cli_dump_error_names[error]); + } else { + nfc_cli_scanner_list_detected_protocols(instance->scanner); + protocol_count = nfc_cli_scanner_detected_protocol_num(instance->scanner); + instance->desired_protocol = nfc_cli_scanner_get_protocol(instance->scanner, 0); + } + } + return protocol_count; +} + +static bool nfc_cli_dump_card(NfcCliDumpContext* instance) { + instance->poller = nfc_poller_alloc(instance->nfc, instance->desired_protocol); + NfcGenericCallback callback = protocol_poller_callbacks[instance->desired_protocol]; + if(callback) { + nfc_poller_start(instance->poller, callback, instance); + FuriStatus status = furi_semaphore_acquire(instance->sem_done, instance->timeout); + + if(status == FuriStatusErrorTimeout) instance->result = NfcCliDumpErrorTimeout; + nfc_poller_stop(instance->poller); + } + nfc_poller_free(instance->poller); + + return instance->result == NfcCliDumpErrorNone; +} + +static void nfc_cli_dump_execute(PipeSide* pipe, NfcCliActionContext* context) { + UNUSED(pipe); + furi_assert(context); + NfcCliDumpContext* instance = context; + do { + if(!nfc_cli_dump_process_filename(instance)) break; + + size_t protocol_count = nfc_cli_dump_set_protocol(instance); + if(instance->desired_protocol == NfcProtocolInvalid) break; + + printf("Dumping as \"%s\"\r\n", nfc_cli_get_protocol_name(instance->desired_protocol)); + if(protocol_count > 1) printf("Use \'-p\' key to specify another protocol\r\n"); + + if(nfc_cli_dump_card(instance)) { + const char* path = furi_string_get_cstr(instance->file_path); + if(nfc_device_save(instance->nfc_device, path)) { + printf("Dump saved to \'%s\'\r\n", path); + } + } else { + printf( + ANSI_FG_RED "Error: %s\r\n" ANSI_RESET, + nfc_cli_dump_error_names[instance->result]); + } + } while(false); +} + +static const NfcProtocolNameValuePair supported_protocols[] = { + {.name = "14_3a", .value = NfcProtocolIso14443_3a}, + {.name = "14_3b", .value = NfcProtocolIso14443_3b}, + {.name = "14_4a", .value = NfcProtocolIso14443_4a}, + {.name = "14_4b", .value = NfcProtocolIso14443_4b}, + {.name = "15", .value = NfcProtocolIso15693_3}, + {.name = "felica", .value = NfcProtocolFelica}, + {.name = "mfu", .value = NfcProtocolMfUltralight}, + {.name = "mfc", .value = NfcProtocolMfClassic}, + {.name = "mfp", .value = NfcProtocolMfPlus}, + {.name = "des", .value = NfcProtocolMfDesfire}, + {.name = "slix", .value = NfcProtocolSlix}, + {.name = "st25", .value = NfcProtocolSt25tb}, +}; + +static bool nfc_cli_dump_parse_protocol(FuriString* value, void* output) { + furi_assert(value); + furi_assert(output); + NfcCliDumpContext* ctx = output; + + NfcCliProtocolParser* parser = + nfc_cli_protocol_parser_alloc(supported_protocols, COUNT_OF(supported_protocols)); + + bool result = nfc_cli_protocol_parser_get(parser, value, &ctx->desired_protocol); + + nfc_cli_protocol_parser_free(parser); + return result; +} + +static bool nfc_cli_dump_parse_key(FuriString* value, void* output) { + furi_assert(value); + furi_assert(output); + NfcCliDumpContext* ctx = output; + NfcCliDumpAuthContext* auth_ctx = &ctx->auth_ctx; + + bool result = false; + do { + size_t len = furi_string_size(value); + if(len % 2 != 0) break; + size_t data_length = len / 2; + + if(data_length != MF_ULTRALIGHT_AUTH_PASSWORD_SIZE && + data_length != MF_ULTRALIGHT_C_AUTH_DES_KEY_SIZE) { + printf(ANSI_FG_RED "Error: Wrong key size" ANSI_RESET); + break; + } + + if(!args_read_hex_bytes(value, auth_ctx->key.key, data_length)) break; + auth_ctx->key_size = data_length; + auth_ctx->skip_auth = false; + result = true; + } while(false); + + return result; +} + +bool nfc_cli_dump_parse_timeout(FuriString* value, NfcCliActionContext* output) { + NfcCliDumpContext* ctx = output; + + StrintParseError err = strint_to_uint32(furi_string_get_cstr(value), NULL, &ctx->timeout, 10); + return err == StrintParseNoError; +} + +const NfcCliKeyDescriptor dump_keys[] = { + { + .long_name = "key", + .short_name = "k", + .description = "key to path auth in protocols which requires it", + .features = {.required = false, .parameter = true}, + .parse = nfc_cli_dump_parse_key, + }, + { + .long_name = "protocol", + .short_name = "p", + .description = "desired protocol", + .features = {.required = false, .parameter = true}, + .parse = nfc_cli_dump_parse_protocol, + }, + { + .features = {.required = false, .parameter = true}, + .long_name = "file", + .short_name = "f", + .description = "path to new file", + .parse = nfc_cli_dump_parse_filename_key, + }, + { + .features = {.required = false, .parameter = true}, + .long_name = "timeout", + .short_name = "t", + .description = "timeout value in milliseconds", + .parse = nfc_cli_dump_parse_timeout, + }, +}; + +const NfcCliActionDescriptor dump_action = { + .name = "dump", + .description = "Dump tag to .nfc file", + .alloc = nfc_cli_dump_alloc_ctx, + .free = nfc_cli_dump_free_ctx, + .execute = nfc_cli_dump_execute, + .key_count = COUNT_OF(dump_keys), + .keys = dump_keys, +}; + +const NfcCliActionDescriptor* dump_actions_collection[] = {&dump_action}; + +//Command descriptor +ADD_NFC_CLI_COMMAND(dump, "", dump_actions_collection); + +//Command examples: +//dump -f ext/nfc/test.nfc diff --git a/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.h b/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.h new file mode 100644 index 000000000..e8d7ddc09 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../../nfc_cli_command_base_i.h" + +extern const NfcCliCommandDescriptor dump_cmd; diff --git a/applications/main/nfc/cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c b/applications/main/nfc/cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c new file mode 100644 index 000000000..b89b75aa6 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c @@ -0,0 +1,32 @@ +#include "nfc_cli_dump_felica.h" +#include + +NfcCommand nfc_cli_dump_poller_callback_felica(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolFelica); + + NfcCliDumpContext* instance = context; + const FelicaPollerEvent* felica_event = event.event_data; + NfcCommand command = NfcCommandContinue; + + if(felica_event->type == FelicaPollerEventTypeReady || + felica_event->type == FelicaPollerEventTypeIncomplete) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolFelica, nfc_poller_get_data(instance->poller)); + command = NfcCommandStop; + instance->result = NfcCliDumpErrorNone; + } else if(felica_event->type == FelicaPollerEventTypeError) { + command = NfcCommandStop; + instance->result = NfcCliDumpErrorFailedToRead; + } else if(felica_event->type == FelicaPollerEventTypeRequestAuthContext) { + FelicaAuthenticationContext* ctx = felica_event->data->auth_context; + const NfcCliDumpAuthContext* dump_auth_ctx = &instance->auth_ctx; + ctx->skip_auth = dump_auth_ctx->skip_auth; + ctx->card_key = dump_auth_ctx->key.felica_key; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/felica/nfc_cli_dump_felica.h b/applications/main/nfc/cli/commands/dump/protocols/felica/nfc_cli_dump_felica.h new file mode 100644 index 000000000..5114ff36e --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/felica/nfc_cli_dump_felica.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_felica(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.c b/applications/main/nfc/cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.c new file mode 100644 index 000000000..a2dfe3b93 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.c @@ -0,0 +1,25 @@ +#include "nfc_cli_dump_iso14443_3a.h" +#include + +NfcCommand nfc_cli_dump_poller_callback_iso14443_3a(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolIso14443_3a); + + NfcCliDumpContext* instance = context; + const Iso14443_3aPollerEvent* iso14443_3a_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + if(iso14443_3a_event->type == Iso14443_3aPollerEventTypeReady) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolIso14443_3a, nfc_poller_get_data(instance->poller)); + command = NfcCommandStop; + } else if(iso14443_3a_event->type == Iso14443_3aPollerEventTypeError) { + command = NfcCommandStop; + instance->result = NfcCliDumpErrorFailedToRead; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.h b/applications/main/nfc/cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.h new file mode 100644 index 000000000..42bb4b0f4 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso14443_3a/nfc_cli_dump_iso14443_3a.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_iso14443_3a(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.c b/applications/main/nfc/cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.c new file mode 100644 index 000000000..7a67af75f --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.c @@ -0,0 +1,27 @@ +#include "nfc_cli_dump_iso14443_3b.h" +#include + +NfcCommand nfc_cli_dump_poller_callback_iso14443_3b(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolIso14443_3b); + + NfcCliDumpContext* instance = context; + const Iso14443_3bPollerEvent* iso14443_3b_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + if(iso14443_3b_event->type == Iso14443_3bPollerEventTypeReady) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolIso14443_3b, nfc_poller_get_data(instance->poller)); + + instance->result = NfcCliDumpErrorNone; + command = NfcCommandStop; + } else if(iso14443_3b_event->type == Iso14443_3bPollerEventTypeError) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return NfcCommandContinue; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.h b/applications/main/nfc/cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.h new file mode 100644 index 000000000..eb374303e --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso14443_3b/nfc_cli_dump_iso14443_3b.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_iso14443_3b(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.c b/applications/main/nfc/cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.c new file mode 100644 index 000000000..380f1672c --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.c @@ -0,0 +1,26 @@ +#include "nfc_cli_dump_iso14443_4a.h" +#include + +NfcCommand nfc_cli_dump_poller_callback_iso14443_4a(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolIso14443_4a); + + NfcCliDumpContext* instance = context; + const Iso14443_4aPollerEvent* iso14443_4a_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeReady) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolIso14443_4a, nfc_poller_get_data(instance->poller)); + instance->result = NfcCliDumpErrorNone; + command = NfcCommandStop; + } else if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeError) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.h b/applications/main/nfc/cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.h new file mode 100644 index 000000000..5e2220107 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_iso14443_4a(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.c b/applications/main/nfc/cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.c new file mode 100644 index 000000000..eae0669fc --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.c @@ -0,0 +1,25 @@ +#include "nfc_cli_dump_iso14443_4b.h" +#include + +NfcCommand nfc_cli_dump_poller_callback_iso14443_4b(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolIso14443_4b); + + NfcCliDumpContext* instance = context; + const Iso14443_4bPollerEvent* iso14443_4b_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeReady) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolIso14443_4b, nfc_poller_get_data(instance->poller)); + command = NfcCommandStop; + } else if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeError) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.h b/applications/main/nfc/cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.h new file mode 100644 index 000000000..ffa1df346 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_iso14443_4b(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.c b/applications/main/nfc/cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.c new file mode 100644 index 000000000..e4a6ea753 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.c @@ -0,0 +1,26 @@ +#include "nfc_cli_dump_iso15693_3.h" +#include + +NfcCommand nfc_cli_dump_poller_callback_iso15693_3(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolIso15693_3); + + NfcCliDumpContext* instance = context; + const Iso15693_3PollerEvent* iso15693_3_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + if(iso15693_3_event->type == Iso15693_3PollerEventTypeReady) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolIso15693_3, nfc_poller_get_data(instance->poller)); + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } else if(iso15693_3_event->type == Iso15693_3PollerEventTypeError) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.h b/applications/main/nfc/cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.h new file mode 100644 index 000000000..7fa6135ea --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_iso15693_3(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.c b/applications/main/nfc/cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.c new file mode 100644 index 000000000..525f171eb --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.c @@ -0,0 +1,54 @@ +#include "nfc_cli_dump_mf_classic.h" +#include + +#define TAG "MFC" + +NfcCommand nfc_cli_dump_poller_callback_mf_classic(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolMfClassic); + + NfcCliDumpContext* instance = context; + const MfClassicPollerEvent* mfc_event = event.event_data; + NfcCommand command = NfcCommandContinue; + + if(mfc_event->type == MfClassicPollerEventTypeRequestMode) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(instance->poller)); + size_t uid_len = 0; + const uint8_t* uid = nfc_device_get_uid(instance->nfc_device, &uid_len); + if(mf_classic_key_cache_load(instance->mfc_key_cache, uid, uid_len)) { + FURI_LOG_D(TAG, "Key cache found"); + mfc_event->data->poller_mode.mode = MfClassicPollerModeRead; + } else { + FURI_LOG_D(TAG, "Key cache not found"); + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } + } else if(mfc_event->type == MfClassicPollerEventTypeRequestReadSector) { + uint8_t sector_num = 0; + MfClassicKey key = {}; + MfClassicKeyType key_type = MfClassicKeyTypeA; + if(mf_classic_key_cache_get_next_key( + instance->mfc_key_cache, §or_num, &key, &key_type)) { + mfc_event->data->read_sector_request_data.sector_num = sector_num; + mfc_event->data->read_sector_request_data.key = key; + mfc_event->data->read_sector_request_data.key_type = key_type; + mfc_event->data->read_sector_request_data.key_provided = true; + } else { + mfc_event->data->read_sector_request_data.key_provided = false; + } + } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(instance->poller)); + instance->result = NfcCliDumpErrorNone; + command = NfcCommandStop; + } else if(mfc_event->type == MfClassicPollerEventTypeFail) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.h b/applications/main/nfc/cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.h new file mode 100644 index 000000000..c1e3b1de1 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_mf_classic(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.c b/applications/main/nfc/cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.c new file mode 100644 index 000000000..49a5bf04e --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.c @@ -0,0 +1,27 @@ +#include "nfc_cli_dump_mf_desfire.h" +#include + +#define TAG "MFDES" + +NfcCommand nfc_cli_dump_poller_callback_mf_desfire(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolMfDesfire); + + NfcCommand command = NfcCommandContinue; + + NfcCliDumpContext* instance = context; + const MfDesfirePollerEvent* mf_desfire_event = event.event_data; + + if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(instance->poller)); + instance->result = NfcCliDumpErrorNone; + furi_semaphore_release(instance->sem_done); + command = NfcCommandStop; + } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandReset; + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.h b/applications/main/nfc/cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.h new file mode 100644 index 000000000..89cff77cf --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_mf_desfire(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.c b/applications/main/nfc/cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.c new file mode 100644 index 000000000..8ed421605 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.c @@ -0,0 +1,31 @@ +#include "nfc_cli_dump_mf_plus.h" +#include + +#define TAG "MFPLUS" + +NfcCommand nfc_cli_dump_poller_callback_mf_plus(NfcGenericEvent event, void* context) { + furi_assert(context); + furi_assert(event.protocol == NfcProtocolMfPlus); + furi_assert(event.event_data); + + NfcCliDumpContext* instance = context; + const MfPlusPollerEvent* mf_plus_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + + if(mf_plus_event->type == MfPlusPollerEventTypeReadSuccess) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolMfPlus, nfc_poller_get_data(instance->poller)); + instance->result = NfcCliDumpErrorNone; + command = NfcCommandStop; + } else if(mf_plus_event->type == MfPlusPollerEventTypeReadFailed) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandReset; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.h b/applications/main/nfc/cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.h new file mode 100644 index 000000000..63954e268 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_mf_plus(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.c b/applications/main/nfc/cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.c new file mode 100644 index 000000000..cc607a349 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.c @@ -0,0 +1,39 @@ +#include "nfc_cli_dump_mf_ultralight.h" +#include + +#define TAG "MFU" + +NfcCommand nfc_cli_dump_poller_callback_mf_ultralight(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolMfUltralight); + NfcCliDumpContext* instance = context; + const MfUltralightPollerEvent* mf_ultralight_event = event.event_data; + NfcCommand command = NfcCommandContinue; + + if(mf_ultralight_event->type == MfUltralightPollerEventTypeReadSuccess) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolMfUltralight, nfc_poller_get_data(instance->poller)); + + instance->result = NfcCliDumpErrorNone; + command = NfcCommandStop; + } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeAuthRequest) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolMfUltralight, nfc_poller_get_data(instance->poller)); + + const NfcCliDumpAuthContext* auth_ctx = &instance->auth_ctx; + mf_ultralight_event->data->auth_context.skip_auth = auth_ctx->skip_auth; + mf_ultralight_event->data->auth_context.password = auth_ctx->key.password; + mf_ultralight_event->data->auth_context.tdes_key = auth_ctx->key.tdes_key; + } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeAuthFailed) { + instance->result = NfcCliDumpErrorAuthFailed; + command = NfcCommandStop; + } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeReadFailed) { + instance->result = NfcCliDumpErrorAuthFailed; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.h b/applications/main/nfc/cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.h new file mode 100644 index 000000000..a005f6c16 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_mf_ultralight(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/nfc_cli_dump_common_types.h b/applications/main/nfc/cli/commands/dump/protocols/nfc_cli_dump_common_types.h new file mode 100644 index 000000000..c14a31bdd --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/nfc_cli_dump_common_types.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include "../../../../helpers/mf_classic_key_cache.h" +#include "../../helpers/nfc_cli_scanner.h" + +#include +#include +#include +#include + +#include +#include + +#include + +#define NFC_CLI_DUMP_KEY_MAX_SIZE (16) + +typedef union { + MfUltralightAuthPassword password; + FelicaCardKey felica_key; + MfUltralightC3DesAuthKey tdes_key; + uint8_t key[NFC_CLI_DUMP_KEY_MAX_SIZE]; +} NfcCliDumpKeyUnion; + +typedef struct { + NfcCliDumpKeyUnion key; + uint8_t key_size; + bool skip_auth; +} NfcCliDumpAuthContext; + +typedef enum { + NfcCliDumpErrorNone, + NfcCliDumpErrorTimeout, + NfcCliDumpErrorNotPresent, + NfcCliDumpErrorFailedToRead, + NfcCliDumpErrorAuthFailed, + + NfcCliDumpErrorNum, +} NfcCliDumpError; + +typedef struct { + Nfc* nfc; + FuriString* file_path; + Storage* storage; + NfcCliScanner* scanner; + NfcProtocol desired_protocol; + uint32_t timeout; + FuriSemaphore* sem_done; + + NfcCliDumpError result; + + NfcCliDumpAuthContext auth_ctx; + MfClassicKeyCache* mfc_key_cache; + + NfcPoller* poller; + NfcDevice* nfc_device; +} NfcCliDumpContext; diff --git a/applications/main/nfc/cli/commands/dump/protocols/slix/nfc_cli_dump_slix.c b/applications/main/nfc/cli/commands/dump/protocols/slix/nfc_cli_dump_slix.c new file mode 100644 index 000000000..4fdd66a44 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/slix/nfc_cli_dump_slix.c @@ -0,0 +1,26 @@ +#include "nfc_cli_dump_slix.h" +#include + +NfcCommand nfc_cli_dump_poller_callback_slix(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolSlix); + + NfcCliDumpContext* instance = context; + const SlixPollerEvent* slix_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + + if(slix_event->type == SlixPollerEventTypeReady) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolSlix, nfc_poller_get_data(instance->poller)); + command = NfcCommandStop; + } else if(slix_event->type == SlixPollerEventTypeError) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/slix/nfc_cli_dump_slix.h b/applications/main/nfc/cli/commands/dump/protocols/slix/nfc_cli_dump_slix.h new file mode 100644 index 000000000..91b01d5d1 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/slix/nfc_cli_dump_slix.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_slix(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.c b/applications/main/nfc/cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.c new file mode 100644 index 000000000..c6fd0ea19 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.c @@ -0,0 +1,29 @@ +#include "nfc_cli_dump_st25tb.h" +#include + +NfcCommand nfc_cli_dump_poller_callback_st25tb(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolSt25tb); + + NfcCliDumpContext* instance = context; + const St25tbPollerEvent* st25tb_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + + if(st25tb_event->type == St25tbPollerEventTypeRequestMode) { + st25tb_event->data->mode_request.mode = St25tbPollerModeRead; + } else if(st25tb_event->type == St25tbPollerEventTypeSuccess) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolSt25tb, nfc_poller_get_data(instance->poller)); + instance->result = NfcCliDumpErrorNone; + command = NfcCommandStop; + } else if(st25tb_event->type == St25tbPollerEventTypeFailure) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.h b/applications/main/nfc/cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.h new file mode 100644 index 000000000..e1512864f --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_st25tb(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/helpers/nfc_cli_format.c b/applications/main/nfc/cli/commands/helpers/nfc_cli_format.c new file mode 100644 index 000000000..abe153938 --- /dev/null +++ b/applications/main/nfc/cli/commands/helpers/nfc_cli_format.c @@ -0,0 +1,61 @@ +#include "nfc_cli_format.h" + +static const char* protocol_names[NfcProtocolNum] = { + [NfcProtocolIso14443_3a] = "Iso14443-3a", + [NfcProtocolIso14443_3b] = "Iso14443-3b", + [NfcProtocolIso14443_4a] = "Iso14443-4a", + [NfcProtocolIso14443_4b] = "Iso14443-4b", + [NfcProtocolIso15693_3] = "Iso15693-3", + [NfcProtocolFelica] = "FeliCa", + [NfcProtocolMfUltralight] = "Mifare Ultralight", + [NfcProtocolMfClassic] = "Mifare Classic", + [NfcProtocolMfDesfire] = "Mifare DESFire", + [NfcProtocolMfPlus] = "Mifare Plus", + [NfcProtocolSlix] = "Slix", + [NfcProtocolSt25tb] = "St25tb", +}; + +const char* nfc_cli_get_protocol_name(NfcProtocol protocol) { + furi_assert(protocol < NfcProtocolNum); + return protocol_names[protocol]; +} + +static const char* mf_ultralight_error_names[] = { + [MfUltralightErrorNone] = "OK", + [MfUltralightErrorNotPresent] = "Card not present", + [MfUltralightErrorProtocol] = "Protocol failure", + [MfUltralightErrorAuth] = "Auth failed", + [MfUltralightErrorTimeout] = "Timeout", +}; + +const char* nfc_cli_mf_ultralight_get_error(MfUltralightError error) { + furi_assert(error < COUNT_OF(mf_ultralight_error_names)); + return mf_ultralight_error_names[error]; +} + +void nfc_cli_format_array( + const uint8_t* data, + const size_t data_size, + const char* header, + FuriString* output) { + furi_assert(data); + furi_assert(data_size > 0); + furi_assert(header); + furi_assert(output); + + furi_string_cat_printf(output, "%s", header); + for(size_t i = 0; i < data_size; i++) { + furi_string_cat_printf(output, "%02X ", data[i]); + } +} + +void nfc_cli_printf_array(const uint8_t* data, const size_t data_size, const char* header) { + furi_assert(data); + furi_assert(data_size > 0); + furi_assert(header); + + printf("%s", header); + for(size_t i = 0; i < data_size; i++) { + printf("%02X ", data[i]); + } +} diff --git a/applications/main/nfc/cli/commands/helpers/nfc_cli_format.h b/applications/main/nfc/cli/commands/helpers/nfc_cli_format.h new file mode 100644 index 000000000..5f9c2e574 --- /dev/null +++ b/applications/main/nfc/cli/commands/helpers/nfc_cli_format.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +const char* nfc_cli_get_protocol_name(NfcProtocol protocol); +const char* nfc_cli_mf_ultralight_get_error(MfUltralightError error); + +void nfc_cli_format_array( + const uint8_t* data, + const size_t data_size, + const char* header, + FuriString* output); + +void nfc_cli_printf_array(const uint8_t* data, const size_t data_size, const char* header); diff --git a/applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.c b/applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.c new file mode 100644 index 000000000..21a8d29b4 --- /dev/null +++ b/applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.c @@ -0,0 +1,58 @@ +#include "nfc_cli_protocol_parser.h" + +#include + +#define PROTOCOLS_TREE_RANK 4 + +BPTREE_DEF2( + ProtocolTree, + PROTOCOLS_TREE_RANK, + FuriString*, + FURI_STRING_OPLIST, + NfcProtocol, + M_POD_OPLIST); + +#define M_OPL_ProtocolTree_t() BPTREE_OPLIST(ProtocolTree, M_POD_OPLIST) + +struct NfcCliProtocolParser { + ProtocolTree_t protocols; +}; + +NfcCliProtocolParser* nfc_cli_protocol_parser_alloc( + const NfcProtocolNameValuePair* valid_protocols, + const size_t valid_count) { + furi_assert(valid_protocols); + furi_assert(valid_count > 0); + + NfcCliProtocolParser* instance = malloc(sizeof(NfcCliProtocolParser)); + + FuriString* name = furi_string_alloc(); + ProtocolTree_init(instance->protocols); + for(size_t i = 0; i < valid_count; i++) { + const NfcProtocolNameValuePair* item = &valid_protocols[i]; + furi_string_set_str(name, item->name); + ProtocolTree_set_at(instance->protocols, name, item->value); + } + + furi_string_free(name); + return instance; +} + +void nfc_cli_protocol_parser_free(NfcCliProtocolParser* instance) { + furi_assert(instance); + ProtocolTree_clear(instance->protocols); + free(instance); +} + +bool nfc_cli_protocol_parser_get( + NfcCliProtocolParser* instance, + FuriString* key, + NfcProtocol* result) { + furi_assert(instance); + furi_assert(key); + + NfcProtocol* protocol = ProtocolTree_get(instance->protocols, key); + if(protocol) *result = *protocol; + + return protocol != NULL; +} diff --git a/applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.h b/applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.h new file mode 100644 index 000000000..a87b26fe5 --- /dev/null +++ b/applications/main/nfc/cli/commands/helpers/nfc_cli_protocol_parser.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +typedef struct { + const char* name; + NfcProtocol value; +} NfcProtocolNameValuePair; + +typedef struct NfcCliProtocolParser NfcCliProtocolParser; + +NfcCliProtocolParser* nfc_cli_protocol_parser_alloc( + const NfcProtocolNameValuePair* valid_protocols, + const size_t valid_count); + +void nfc_cli_protocol_parser_free(NfcCliProtocolParser* instance); +bool nfc_cli_protocol_parser_get( + NfcCliProtocolParser* instance, + FuriString* key, + NfcProtocol* result); diff --git a/applications/main/nfc/cli/commands/helpers/nfc_cli_scanner.c b/applications/main/nfc/cli/commands/helpers/nfc_cli_scanner.c new file mode 100644 index 000000000..12d043cdc --- /dev/null +++ b/applications/main/nfc/cli/commands/helpers/nfc_cli_scanner.c @@ -0,0 +1,96 @@ +#include "nfc_cli_scanner.h" +#include +#include "nfc_cli_format.h" + +#define NFC_CLI_SCANNER_FLAG_DETECTED (1UL << 0) + +struct NfcCliScanner { + Nfc* nfc; + size_t protocols_detected_num; + NfcProtocol protocols_detected[NfcProtocolNum]; + FuriThreadId thread_id; + NfcScanner* scanner; +}; + +NfcCliScanner* nfc_cli_scanner_alloc(Nfc* nfc) { + NfcCliScanner* instance = malloc(sizeof(NfcCliScanner)); + instance->nfc = nfc; + instance->thread_id = furi_thread_get_current_id(); + return instance; +} + +void nfc_cli_scanner_free(NfcCliScanner* instance) { + furi_assert(instance); + free(instance); +} + +static void nfc_cli_scanner_detect_callback(NfcScannerEvent event, void* context) { + furi_assert(context); + NfcCliScanner* instance = context; + + if(event.type == NfcScannerEventTypeDetected) { + instance->protocols_detected_num = event.data.protocol_num; + memcpy( + instance->protocols_detected, + event.data.protocols, + event.data.protocol_num * sizeof(NfcProtocol)); + furi_thread_flags_set(instance->thread_id, NFC_CLI_SCANNER_FLAG_DETECTED); + } +} + +bool nfc_cli_scanner_detect_protocol(NfcCliScanner* instance, uint32_t timeout) { + instance->scanner = nfc_scanner_alloc(instance->nfc); + nfc_scanner_start(instance->scanner, nfc_cli_scanner_detect_callback, instance); + uint32_t event = + furi_thread_flags_wait(NFC_CLI_SCANNER_FLAG_DETECTED, FuriFlagWaitAny, timeout); + nfc_scanner_stop(instance->scanner); + nfc_scanner_free(instance->scanner); + return (event == NFC_CLI_SCANNER_FLAG_DETECTED); +} + +void nfc_cli_scanner_begin_scan(NfcCliScanner* instance) { + instance->scanner = nfc_scanner_alloc(instance->nfc); + nfc_scanner_start(instance->scanner, nfc_cli_scanner_detect_callback, instance); +} + +bool nfc_cli_scanner_wait_scan(NfcCliScanner* instance, uint32_t timeout) { + UNUSED(instance); + uint32_t event = + furi_thread_flags_wait(NFC_CLI_SCANNER_FLAG_DETECTED, FuriFlagWaitAny, timeout); + return (event == NFC_CLI_SCANNER_FLAG_DETECTED); +} + +void nfc_cli_scanner_end_scan(NfcCliScanner* instance) { + nfc_scanner_stop(instance->scanner); + nfc_scanner_free(instance->scanner); +} + +void nfc_cli_scanner_list_detected_protocols(NfcCliScanner* instance) { + printf("Protocols detected: "); + size_t n = instance->protocols_detected_num; + for(size_t i = 0; i < n; i++) { + const char* name = nfc_cli_get_protocol_name(instance->protocols_detected[i]); + printf((i == (n - 1)) ? "%s\r\n" : "%s, ", name); + } +} + +bool nfc_cli_scanner_protocol_was_detected(NfcCliScanner* instance, NfcProtocol protocol) { + furi_assert(instance); + furi_assert(protocol < NfcProtocolNum); + + for(size_t i = 0; i < instance->protocols_detected_num; i++) { + if(instance->protocols_detected[i] == protocol) return true; + } + return false; +} + +NfcProtocol nfc_cli_scanner_get_protocol(NfcCliScanner* instance, size_t idx) { + furi_assert(instance); + furi_assert(idx < instance->protocols_detected_num); + return instance->protocols_detected[idx]; +} + +size_t nfc_cli_scanner_detected_protocol_num(NfcCliScanner* instance) { + furi_assert(instance); + return instance->protocols_detected_num; +} diff --git a/applications/main/nfc/cli/commands/helpers/nfc_cli_scanner.h b/applications/main/nfc/cli/commands/helpers/nfc_cli_scanner.h new file mode 100644 index 000000000..9ca9a5705 --- /dev/null +++ b/applications/main/nfc/cli/commands/helpers/nfc_cli_scanner.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +typedef struct NfcCliScanner NfcCliScanner; + +NfcCliScanner* nfc_cli_scanner_alloc(Nfc* nfc); +void nfc_cli_scanner_free(NfcCliScanner* instance); + +bool nfc_cli_scanner_detect_protocol(NfcCliScanner* instance, uint32_t timeout); + +void nfc_cli_scanner_begin_scan(NfcCliScanner* instance); +bool nfc_cli_scanner_wait_scan(NfcCliScanner* instance, uint32_t timeout); +void nfc_cli_scanner_end_scan(NfcCliScanner* instance); + +void nfc_cli_scanner_list_detected_protocols(NfcCliScanner* instance); +size_t nfc_cli_scanner_detected_protocol_num(NfcCliScanner* instance); +bool nfc_cli_scanner_protocol_was_detected(NfcCliScanner* instance, NfcProtocol protocol); +NfcProtocol nfc_cli_scanner_get_protocol(NfcCliScanner* instance, size_t idx); diff --git a/applications/main/nfc/cli/commands/mfu/nfc_cli_action_info.c b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_info.c new file mode 100644 index 000000000..3d3ad1125 --- /dev/null +++ b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_info.c @@ -0,0 +1,227 @@ + +#include "nfc_cli_action_info.h" + +#include "../../../helpers/protocol_support/mf_ultralight/mf_ultralight_render.h" +#include "../helpers/nfc_cli_format.h" + +#include +#include +#include +#include + +#define TAG "INFO" + +typedef struct { + uint8_t magic; + union { + uint8_t value; + struct { + uint8_t minor : 4; + uint8_t major : 4; + }; + } version; + + uint8_t size; + + union { + uint8_t value; + struct { + uint8_t write : 4; + uint8_t read : 4; + }; + } access; +} FURI_PACKED MfUltralightCapabilityContainer; + +typedef struct { + Nfc* nfc; + MfUltralightData* data; +} NfcCliMfuContext; + +static void nfc_cli_mfu_info_get_vendor(const uint8_t vendor_key, FuriString* output) { + furi_assert(output); + + FuriString* buf = furi_string_alloc(); + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* ff = flipper_format_file_alloc(storage); + + do { + if(!flipper_format_file_open_existing(ff, EXT_PATH("nfc/assets/vendors.nfc"))) { + FURI_LOG_W(TAG, "NFC Vendors dict not found"); + break; + } + + char uid_str[5]; + snprintf(uid_str, sizeof(uid_str), "%d", vendor_key); + + if(flipper_format_read_string(ff, uid_str, buf)) + furi_string_printf(output, "%s, %s", uid_str, furi_string_get_cstr(buf)); + else + furi_string_printf(output, "unknown"); + } while(false); + + flipper_format_free(ff); + furi_record_close(RECORD_STORAGE); + furi_string_free(buf); +} + +const char* + nfc_cli_mfu_capability_container_get_access_description(const uint8_t value, bool read) { + const char* description = "RFU"; //value 0x01 - 0x07, and 0xF when read + if(value == 0x00) + description = "access fully granted"; + else if(value >= 0x08 && value <= 0x0E) + description = "proprietary"; + else if(value == 0x0F && !read) + description = "no access granted at all"; + + return description; +} + +static void nfc_cli_mfu_info_print_common(const MfUltralightData* data) { + FuriString* str = furi_string_alloc(); + + printf(ANSI_FG_GREEN "\r\n\tTag information\r\n" ANSI_RESET); + printf( + "Type: " ANSI_FG_YELLOW "%s\r\n" ANSI_RESET, + mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); + + nfc_cli_mfu_info_get_vendor(data->iso14443_3a_data->uid[0], str); + printf("Vendor ID: %s\r\n", furi_string_get_cstr(str)); + + furi_string_reset(str); + nfc_render_mf_ultralight_info(data, NfcProtocolFormatTypeFull, str); + printf("%s\r\n", furi_string_get_cstr(str)); + printf("BCC0: %02X\r\nBCC1: %02X\r\n", data->page[0].data[3], data->page[2].data[0]); + + furi_string_free(str); +} + +static void nfc_cli_mfu_info_print_ndef(const MfUltralightData* data) { + const MfUltralightCapabilityContainer* cc = + (const MfUltralightCapabilityContainer*)data->page[3].data; + if(cc->magic == 0xE1) { + printf(ANSI_FG_GREEN "\r\n\tNDEF Message\r\n" ANSI_RESET); + nfc_cli_printf_array(data->page[3].data, 4, "Capability container: "); + printf( + "\r\nMagic number: %02X\r\nVersion %d.%d\r\nSize: [%02X] - %d bytes\r\n", + cc->magic, + cc->version.major, + cc->version.minor, + cc->size, + cc->size * 8); + printf( + "Access read: [%02X] - %s", + cc->access.read, + nfc_cli_mfu_capability_container_get_access_description(cc->access.read, true)); + printf( + "Access write: [%02X] - %s", + cc->access.write, + nfc_cli_mfu_capability_container_get_access_description(cc->access.write, false)); + } +} + +static void nfc_cli_mfu_info_print_counter(const MfUltralightData* data) { + uint32_t features = mf_ultralight_get_feature_support_set(data->type); + if(!mf_ultralight_support_feature(features, MfUltralightFeatureSupportReadCounter)) return; + + printf(ANSI_FG_GREEN "\r\n\n\tTag counters\r\n" ANSI_RESET); + uint8_t i = + mf_ultralight_support_feature(features, MfUltralightFeatureSupportSingleCounter) ? 2 : 0; + + for(; i < MF_ULTRALIGHT_COUNTER_NUM; i++) { + printf("Counter [%d]: ", i); + nfc_cli_printf_array(data->counter[i].data, MF_ULTRALIGHT_COUNTER_SIZE, ""); + printf(" Value: %lu\r\n", data->counter[i].counter); + + const uint8_t tf = data->tearing_flag[i].data; + printf( + "Tearing [%d]: [%02X] %s", + i, + tf, + tf == MF_ULTRALIGHT_TEARING_FLAG_DEFAULT ? "(ok)" : ""); + } +} + +static void nfc_cli_mfu_info_print_signature(const MfUltralightData* data) { + uint32_t features = mf_ultralight_get_feature_support_set(data->type); + if(!mf_ultralight_support_feature(features, MfUltralightFeatureSupportReadSignature)) return; + + const MfUltralightSignature* signature = &data->signature; + printf(ANSI_FG_GREEN "\r\n\n\tTag signature\r\n" ANSI_RESET); + nfc_cli_printf_array(signature->data, sizeof(signature->data), "ECC signature: "); +} + +static void nfc_cli_mfu_info_print_version_storage_size(uint8_t storage_size) { + uint16_t max_size = 1 << ((storage_size >> 1) + 1); + uint16_t min_exact_size = 1 << (storage_size >> 1); + + bool exact_size = !(storage_size & 0x01); + if(exact_size) + printf("[%02X], (%u bytes)", storage_size, min_exact_size); + else + printf("[%02X], (%u <-> %u bytes)", storage_size, min_exact_size, max_size); +} + +static void nfc_cli_mfu_info_print_version(const MfUltralightData* data) { + uint32_t features = mf_ultralight_get_feature_support_set(data->type); + if(!mf_ultralight_support_feature(features, MfUltralightFeatureSupportReadVersion)) return; + + const MfUltralightVersion* version = &data->version; + printf(ANSI_FG_GREEN "\r\n\n\tTag Version\r\n" ANSI_RESET); + nfc_cli_printf_array((uint8_t*)version, sizeof(MfUltralightVersion), "Raw bytes: "); + + FuriString* str = furi_string_alloc(); + nfc_cli_mfu_info_get_vendor(version->vendor_id, str); + printf("\r\nVendor ID: %s\r\n", furi_string_get_cstr(str)); + furi_string_free(str); + + printf("Product type: %02X\r\n", version->prod_type); + + printf( + "Protocol type: %02X%s\r\n", + version->protocol_type, + (version->protocol_type == 0x3) ? ", ISO14443-3 Compliant" : ""); + + printf( + "Product subtype: [%02X], %s\r\n", + version->prod_subtype, + (version->prod_subtype == 1) ? "17 pF" : "50pF"); + printf( + "Major version: %02X\r\nMinor version: %02X\r\nSize: ", + version->prod_ver_major, + version->prod_ver_minor); + nfc_cli_mfu_info_print_version_storage_size(version->storage_size); +} + +NfcCliActionContext* nfc_cli_mfu_info_alloc_ctx(Nfc* nfc) { + NfcCliMfuContext* instance = malloc(sizeof(NfcCliMfuContext)); + instance->nfc = nfc; + instance->data = mf_ultralight_alloc(); + return instance; +} + +void nfc_cli_mfu_info_free_ctx(NfcCliActionContext* ctx) { + NfcCliMfuContext* instance = ctx; + mf_ultralight_free(instance->data); + free(instance); +} + +void nfc_cli_mfu_info_execute(PipeSide* pipe, NfcCliActionContext* ctx) { + furi_assert(pipe); + furi_assert(ctx); + + NfcCliMfuContext* instance = ctx; + + MfUltralightError error = + mf_ultralight_poller_sync_read_card(instance->nfc, instance->data, NULL); + if(error == MfUltralightErrorNone) { + const MfUltralightData* data = instance->data; + nfc_cli_mfu_info_print_common(data); + nfc_cli_mfu_info_print_ndef(data); + nfc_cli_mfu_info_print_counter(data); + nfc_cli_mfu_info_print_signature(data); + nfc_cli_mfu_info_print_version(data); + } else { + printf(ANSI_FG_RED "Error: %s" ANSI_RESET, nfc_cli_mf_ultralight_get_error(error)); + } +} diff --git a/applications/main/nfc/cli/commands/mfu/nfc_cli_action_info.h b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_info.h new file mode 100644 index 000000000..998dd8def --- /dev/null +++ b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_info.h @@ -0,0 +1,7 @@ +#pragma once + +#include "../../nfc_cli_command_base_i.h" + +NfcCliActionContext* nfc_cli_mfu_info_alloc_ctx(Nfc* nfc); +void nfc_cli_mfu_info_free_ctx(NfcCliActionContext* ctx); +void nfc_cli_mfu_info_execute(PipeSide* pipe, NfcCliActionContext* ctx); diff --git a/applications/main/nfc/cli/commands/mfu/nfc_cli_action_rdbl.c b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_rdbl.c new file mode 100644 index 000000000..d8a43ab71 --- /dev/null +++ b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_rdbl.c @@ -0,0 +1,52 @@ +#include "nfc_cli_action_rdbl.h" + +#include "../helpers/nfc_cli_format.h" + +#include +#include +#include + +#define MF_ULTRALIGHT_POLLER_COMPLETE_EVENT (1UL << 0) + +typedef struct { + Nfc* nfc; + uint16_t block; +} NfcCliMfuRdblContext; + +NfcCliActionContext* nfc_cli_mfu_rdbl_alloc_ctx(Nfc* nfc) { + furi_assert(nfc); + NfcCliMfuRdblContext* instance = malloc(sizeof(NfcCliMfuRdblContext)); + instance->nfc = nfc; + return instance; +} + +void nfc_cli_mfu_rdbl_free_ctx(NfcCliActionContext* ctx) { + furi_assert(ctx); + NfcCliMfuRdblContext* instance = ctx; + free(instance); +} + +void nfc_cli_mfu_rdbl_execute(PipeSide* pipe, NfcCliActionContext* ctx) { + furi_assert(pipe); + + NfcCliMfuRdblContext* instance = ctx; + MfUltralightPage page = {0}; + + MfUltralightError error = + mf_ultralight_poller_sync_read_page(instance->nfc, instance->block, &page); + + if(error == MfUltralightErrorNone) { + printf("\r\nBlock: %d ", instance->block); + nfc_cli_printf_array(page.data, sizeof(MfUltralightPage), "Data: "); + printf("\r\n"); + } else { + printf(ANSI_FG_RED "Error: %s" ANSI_RESET, nfc_cli_mf_ultralight_get_error(error)); + } +} + +bool nfc_cli_mfu_rdbl_parse_block(FuriString* value, NfcCliActionContext* output) { + NfcCliMfuRdblContext* ctx = output; + + StrintParseError err = strint_to_uint16(furi_string_get_cstr(value), NULL, &ctx->block, 10); + return err == StrintParseNoError; +} diff --git a/applications/main/nfc/cli/commands/mfu/nfc_cli_action_rdbl.h b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_rdbl.h new file mode 100644 index 000000000..ec7e19c2d --- /dev/null +++ b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_rdbl.h @@ -0,0 +1,8 @@ +#pragma once + +#include "../../nfc_cli_command_base_i.h" + +NfcCliActionContext* nfc_cli_mfu_rdbl_alloc_ctx(Nfc* nfc); +void nfc_cli_mfu_rdbl_free_ctx(NfcCliActionContext* ctx); +void nfc_cli_mfu_rdbl_execute(PipeSide* pipe, NfcCliActionContext* ctx); +bool nfc_cli_mfu_rdbl_parse_block(FuriString* value, NfcCliActionContext* ctx); diff --git a/applications/main/nfc/cli/commands/mfu/nfc_cli_action_wrbl.c b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_wrbl.c new file mode 100644 index 000000000..bdc20557c --- /dev/null +++ b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_wrbl.c @@ -0,0 +1,71 @@ +#include "nfc_cli_action_rdbl.h" + +#include "../helpers/nfc_cli_format.h" + +#include +#include +#include +#include + +#define MF_ULTRALIGHT_POLLER_COMPLETE_EVENT (1UL << 0) + +typedef struct { + Nfc* nfc; + uint16_t block; + MfUltralightPage page; +} NfcCliMfuWrblContext; + +NfcCliActionContext* nfc_cli_mfu_wrbl_alloc_ctx(Nfc* nfc) { + furi_assert(nfc); + NfcCliMfuWrblContext* instance = malloc(sizeof(NfcCliMfuWrblContext)); + instance->nfc = nfc; + return instance; +} + +void nfc_cli_mfu_wrbl_free_ctx(NfcCliActionContext* ctx) { + furi_assert(ctx); + NfcCliMfuWrblContext* instance = ctx; + free(instance); +} + +void nfc_cli_mfu_wrbl_execute(PipeSide* pipe, NfcCliActionContext* ctx) { + furi_assert(pipe); + + NfcCliMfuWrblContext* instance = ctx; + + MfUltralightError error = + mf_ultralight_poller_sync_write_page(instance->nfc, instance->block, &instance->page); + + if(error == MfUltralightErrorNone) { + printf(ANSI_FG_BR_GREEN "\r\nSuccess\r\n" ANSI_RESET); + printf("Block: %d ", instance->block); + nfc_cli_printf_array(instance->page.data, sizeof(MfUltralightPage), "Data: "); + printf("\r\n"); + } else { + printf(ANSI_FG_RED "Error: %s" ANSI_RESET, nfc_cli_mf_ultralight_get_error(error)); + } +} + +bool nfc_cli_mfu_wrbl_parse_block(FuriString* value, NfcCliActionContext* output) { + NfcCliMfuWrblContext* ctx = output; + + StrintParseError err = strint_to_uint16(furi_string_get_cstr(value), NULL, &ctx->block, 10); + return err == StrintParseNoError; +} + +bool nfc_cli_mfu_wrbl_parse_data(FuriString* value, void* output) { + NfcCliMfuWrblContext* ctx = output; + + bool result = false; + do { + size_t len = furi_string_size(value); + if(len % 2 != 0) break; + + size_t data_length = len / 2; + if(data_length != MF_ULTRALIGHT_PAGE_SIZE) break; + + result = args_read_hex_bytes(value, ctx->page.data, data_length); + } while(false); + + return result; +} diff --git a/applications/main/nfc/cli/commands/mfu/nfc_cli_action_wrbl.h b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_wrbl.h new file mode 100644 index 000000000..abddd367f --- /dev/null +++ b/applications/main/nfc/cli/commands/mfu/nfc_cli_action_wrbl.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../../nfc_cli_command_base_i.h" + +NfcCliActionContext* nfc_cli_mfu_wrbl_alloc_ctx(Nfc* nfc); +void nfc_cli_mfu_wrbl_free_ctx(NfcCliActionContext* ctx); +void nfc_cli_mfu_wrbl_execute(PipeSide* pipe, NfcCliActionContext* ctx); +bool nfc_cli_mfu_wrbl_parse_block(FuriString* value, NfcCliActionContext* ctx); +bool nfc_cli_mfu_wrbl_parse_data(FuriString* value, void* output); diff --git a/applications/main/nfc/cli/commands/mfu/nfc_cli_command_mfu.c b/applications/main/nfc/cli/commands/mfu/nfc_cli_command_mfu.c new file mode 100644 index 000000000..0bfcffe3c --- /dev/null +++ b/applications/main/nfc/cli/commands/mfu/nfc_cli_command_mfu.c @@ -0,0 +1,77 @@ +#include "nfc_cli_command_mfu.h" +#include "nfc_cli_action_info.h" +#include "nfc_cli_action_rdbl.h" +#include "nfc_cli_action_wrbl.h" + +#define TAG "MFU" + +//mfu info +const NfcCliActionDescriptor info_action = { + .name = "info", + .description = "Get basic information about the card", + .alloc = nfc_cli_mfu_info_alloc_ctx, + .free = nfc_cli_mfu_info_free_ctx, + .execute = nfc_cli_mfu_info_execute, + .key_count = 0, + .keys = NULL, +}; + +const NfcCliKeyDescriptor rdbl_action_keys[] = { + { + .short_name = "b", + .long_name = "block", + .features = {.required = true, .parameter = true}, + .description = "desired block number", + .parse = nfc_cli_mfu_rdbl_parse_block, + }, +}; + +//mfu rdbl -b 0 +//mfu rdbl --block 0 +const NfcCliActionDescriptor rdbl_action = { + .name = "rdbl", + .description = "Read block from ultralight card", + .alloc = nfc_cli_mfu_rdbl_alloc_ctx, + .free = nfc_cli_mfu_rdbl_free_ctx, + .execute = nfc_cli_mfu_rdbl_execute, + .key_count = COUNT_OF(rdbl_action_keys), + .keys = rdbl_action_keys, +}; + +const NfcCliKeyDescriptor wrbl_action_keys[] = { + { + .short_name = "b", + .long_name = "block", + .features = {.required = true, .parameter = true}, + .description = "desired block number", + .parse = nfc_cli_mfu_wrbl_parse_block, + }, + { + .short_name = "d", + .long_name = "data", + .features = {.required = true, .parameter = true}, + .description = "new data for block", + .parse = nfc_cli_mfu_wrbl_parse_data, + }, +}; + +//mfu wrbl -b 0 -d DEADBEAF +//mfu rdbl --block 0 -- data DEADBEEF +const NfcCliActionDescriptor wrbl_action = { + .name = "wrbl", + .description = "Read block from ultralight card", + .alloc = nfc_cli_mfu_wrbl_alloc_ctx, + .free = nfc_cli_mfu_wrbl_free_ctx, + .execute = nfc_cli_mfu_wrbl_execute, + .key_count = COUNT_OF(wrbl_action_keys), + .keys = wrbl_action_keys, +}; + +const NfcCliActionDescriptor* mfu_actions[] = { + &rdbl_action, + &info_action, + &wrbl_action, +}; + +//Command descriptor +ADD_NFC_CLI_COMMAND(mfu, "Mifare Ultralight specific commands", mfu_actions); diff --git a/applications/main/nfc/cli/commands/mfu/nfc_cli_command_mfu.h b/applications/main/nfc/cli/commands/mfu/nfc_cli_command_mfu.h new file mode 100644 index 000000000..a0530682c --- /dev/null +++ b/applications/main/nfc/cli/commands/mfu/nfc_cli_command_mfu.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../../nfc_cli_command_base_i.h" + +extern const NfcCliCommandDescriptor mfu_cmd; diff --git a/applications/main/nfc/cli/commands/nfc_cli_command_emulate.c b/applications/main/nfc/cli/commands/nfc_cli_command_emulate.c new file mode 100644 index 000000000..60ff62cc5 --- /dev/null +++ b/applications/main/nfc/cli/commands/nfc_cli_command_emulate.c @@ -0,0 +1,126 @@ +#include "nfc_cli_command_emulate.h" +#include "helpers/nfc_cli_format.h" + +#include +#include +#include + +#include + +typedef struct { + Nfc* nfc; + NfcDevice* nfc_device; + FuriString* file_path; + Storage* storage; +} NfcCliEmulateContext; + +static NfcCliActionContext* nfc_cli_emulate_alloc_ctx(Nfc* nfc) { + furi_assert(nfc); + NfcCliEmulateContext* instance = malloc(sizeof(NfcCliEmulateContext)); + instance->nfc = nfc; + instance->file_path = furi_string_alloc(); + instance->nfc_device = nfc_device_alloc(); + instance->storage = furi_record_open(RECORD_STORAGE); + return instance; +} + +static void nfc_cli_emulate_free_ctx(NfcCliActionContext* ctx) { + furi_assert(ctx); + NfcCliEmulateContext* instance = ctx; + furi_record_close(RECORD_STORAGE); + furi_string_free(instance->file_path); + nfc_device_free(instance->nfc_device); + free(instance); +} + +static const NfcProtocol supported_protocols[] = { + NfcProtocolIso14443_3a, + NfcProtocolIso14443_4a, + NfcProtocolIso15693_3, + NfcProtocolMfUltralight, + NfcProtocolMfClassic, + NfcProtocolSlix, + NfcProtocolFelica, +}; + +static bool nfc_cli_emulate_protocol_supports_emulation(NfcProtocol protocol) { + for(size_t i = 0; i < COUNT_OF(supported_protocols); i++) { + if(supported_protocols[i] == protocol) return true; + } + return false; +} + +static void nfc_cli_emulate_execute(PipeSide* pipe, NfcCliActionContext* context) { + UNUSED(pipe); + furi_assert(context); + NfcCliEmulateContext* instance = context; + do { + const char* path = furi_string_get_cstr(instance->file_path); + if(!storage_common_exists(instance->storage, path)) { + printf(ANSI_FG_RED "Wrong path \'%s\'.\r\n" ANSI_RESET, path); + break; + } + + if(!nfc_device_load(instance->nfc_device, path)) { + printf(ANSI_FG_RED "Failed to load \'%s\'.\r\n" ANSI_RESET, path); + break; + } + + const NfcProtocol protocol = nfc_device_get_protocol(instance->nfc_device); + + if(!nfc_cli_emulate_protocol_supports_emulation(protocol)) { + printf( + ANSI_FG_RED "Error. Emulation for %s is not supported\r\n" ANSI_RESET, + nfc_cli_get_protocol_name(protocol)); + break; + } + + const NfcDeviceData* data = nfc_device_get_data(instance->nfc_device, protocol); + NfcListener* listener = nfc_listener_alloc(instance->nfc, protocol, data); + + nfc_listener_start(listener, NULL, NULL); + printf("\r\nEmulating. Press Ctrl+C to abort\r\n"); + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { + furi_delay_ms(100); + } + nfc_listener_stop(listener); + nfc_listener_free(listener); + } while(false); +} + +static bool nfc_cli_emulate_parse_filename_key(FuriString* value, void* output) { + furi_assert(value); + furi_assert(output); + NfcCliEmulateContext* ctx = output; + furi_string_set(ctx->file_path, value); + return true; +} + +const NfcCliKeyDescriptor emulate_keys[] = { + { + .features = {.required = true, .parameter = true}, + .long_name = "file", + .short_name = "f", + .description = "path to new file", + .parse = nfc_cli_emulate_parse_filename_key, + }, +}; + +const NfcCliActionDescriptor emulate_action = { + .name = "emulate", + .description = "Emulate .nfc file content", + .alloc = nfc_cli_emulate_alloc_ctx, + .free = nfc_cli_emulate_free_ctx, + .execute = nfc_cli_emulate_execute, + .key_count = COUNT_OF(emulate_keys), + .keys = emulate_keys, +}; + +const NfcCliActionDescriptor* emulate_actions_collection[] = {&emulate_action}; + +//Command descriptor +ADD_NFC_CLI_COMMAND(emulate, "", emulate_actions_collection); + +//Command usage: emulate [-f ] +//Command examples: +//emulate -f ext/nfc/test.nfc diff --git a/applications/main/nfc/cli/commands/nfc_cli_command_emulate.h b/applications/main/nfc/cli/commands/nfc_cli_command_emulate.h new file mode 100644 index 000000000..ff1ae5b1a --- /dev/null +++ b/applications/main/nfc/cli/commands/nfc_cli_command_emulate.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_command_base_i.h" + +extern const NfcCliCommandDescriptor emulate_cmd; diff --git a/applications/main/nfc/cli/commands/nfc_cli_command_field.c b/applications/main/nfc/cli/commands/nfc_cli_command_field.c new file mode 100644 index 000000000..753ba451e --- /dev/null +++ b/applications/main/nfc/cli/commands/nfc_cli_command_field.c @@ -0,0 +1,28 @@ +#include "nfc_cli_command_field.h" + +#include + +static void nfc_cli_field(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(args); + UNUSED(context); + + furi_hal_nfc_low_power_mode_stop(); + furi_hal_nfc_poller_field_on(); + + printf("Field is on. Don't leave device in this mode for too long.\r\n"); + printf("Press Ctrl+C to abort\r\n"); + + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { + furi_delay_ms(50); + } + + furi_hal_nfc_low_power_mode_start(); +} + +const NfcCliCommandDescriptor field_cmd = { + .name = "field", + .description = "Turns NFC field on", + .callback = nfc_cli_field, + .action_count = 0, + .actions = NULL, +}; diff --git a/applications/main/nfc/cli/commands/nfc_cli_command_field.h b/applications/main/nfc/cli/commands/nfc_cli_command_field.h new file mode 100644 index 000000000..b96ef3b5a --- /dev/null +++ b/applications/main/nfc/cli/commands/nfc_cli_command_field.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_command_base_i.h" + +extern const NfcCliCommandDescriptor field_cmd; diff --git a/applications/main/nfc/cli/commands/nfc_cli_command_scanner.c b/applications/main/nfc/cli/commands/nfc_cli_command_scanner.c new file mode 100644 index 000000000..fec8d72c1 --- /dev/null +++ b/applications/main/nfc/cli/commands/nfc_cli_command_scanner.c @@ -0,0 +1,97 @@ + +#include "nfc_cli_command_scanner.h" +#include "helpers/nfc_cli_scanner.h" +#include "helpers/nfc_cli_format.h" + +typedef struct { + NfcCliScanner* scanner; + bool display_tree; +} NfcCliCmdScannerContext; + +static NfcCliActionContext* nfc_cli_command_scanner_alloc_ctx(Nfc* nfc) { + furi_assert(nfc); + NfcCliCmdScannerContext* instance = malloc(sizeof(NfcCliCmdScannerContext)); + instance->scanner = nfc_cli_scanner_alloc(nfc); + instance->display_tree = false; + + return instance; +} + +static void nfc_cli_command_scanner_free_ctx(NfcCliActionContext* ctx) { + furi_assert(ctx); + NfcCliCmdScannerContext* instance = ctx; + nfc_cli_scanner_free(instance->scanner); + + free(instance); +} + +static void + nfc_cli_command_scanner_format_protocol_tree(NfcProtocol protocol, FuriString* output) { + const char* names[10] = {0}; + uint8_t cnt = 0; + while(protocol != NfcProtocolInvalid) { + names[cnt++] = nfc_cli_get_protocol_name(protocol); + protocol = nfc_protocol_get_parent(protocol); + } + + for(int8_t i = cnt - 1; i >= 0; i--) { + furi_string_cat_printf(output, (i == 0) ? "%s" : "%s -> ", names[i]); + } +} + +static void nfc_cli_command_scanner_format_detected_protocols(NfcCliScanner* instance) { + FuriString* str = furi_string_alloc(); + printf("Protocols detected: \r\n"); + for(size_t i = 0; i < nfc_cli_scanner_detected_protocol_num(instance); i++) { + furi_string_reset(str); + NfcProtocol protocol = nfc_cli_scanner_get_protocol(instance, i); + nfc_cli_command_scanner_format_protocol_tree(protocol, str); + printf("Protocol [%zu]: %s\r\n", i + 1, furi_string_get_cstr(str)); + } + furi_string_free(str); +} + +static void nfc_cli_command_scanner_execute(PipeSide* pipe, void* context) { + NfcCliCmdScannerContext* instance = context; + + printf("Press Ctrl+C to abort\r\n\n"); + nfc_cli_scanner_begin_scan(instance->scanner); + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe) && + !nfc_cli_scanner_wait_scan(instance->scanner, 50)) + ; + nfc_cli_scanner_end_scan(instance->scanner); + + if(!instance->display_tree) + nfc_cli_scanner_list_detected_protocols(instance->scanner); + else + nfc_cli_command_scanner_format_detected_protocols(instance->scanner); +} + +static bool nfc_cli_command_scanner_parse_tree(FuriString* value, void* output) { + UNUSED(value); + NfcCliCmdScannerContext* ctx = output; + ctx->display_tree = true; + return true; +} + +const NfcCliKeyDescriptor tree_key = { + .short_name = "t", + .long_name = "tree", + .features = {.parameter = false, .required = false, .multivalue = false}, + .description = "displays protocol hierarchy for each detected protocol", + .parse = nfc_cli_command_scanner_parse_tree, +}; + +const NfcCliActionDescriptor scanner_action = { + .name = "scanner", + .description = "Detect tag type", + .key_count = 1, + .keys = &tree_key, + .execute = nfc_cli_command_scanner_execute, + .alloc = nfc_cli_command_scanner_alloc_ctx, + .free = nfc_cli_command_scanner_free_ctx, +}; + +const NfcCliActionDescriptor* scanner_actions_collection[] = {&scanner_action}; + +ADD_NFC_CLI_COMMAND(scanner, "", scanner_actions_collection); diff --git a/applications/main/nfc/cli/commands/nfc_cli_command_scanner.h b/applications/main/nfc/cli/commands/nfc_cli_command_scanner.h new file mode 100644 index 000000000..e41958859 --- /dev/null +++ b/applications/main/nfc/cli/commands/nfc_cli_command_scanner.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_command_base_i.h" + +extern const NfcCliCommandDescriptor scanner_cmd; diff --git a/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.c b/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.c new file mode 100644 index 000000000..f90adb4c6 --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.c @@ -0,0 +1,352 @@ +#include "nfc_cli_command_raw.h" +#include "../helpers/nfc_cli_format.h" +#include "../helpers/nfc_cli_protocol_parser.h" + +#include "protocol_handlers/nfc_cli_raw_common_types.h" +#include "protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.h" +#include "protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.h" +#include "protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.h" +#include "protocol_handlers/felica/nfc_cli_raw_felica.h" + +#include + +#define NFC_CLI_PROTOCOL_SUPPORT_MAX_BUFFER_SIZE (256) + +#define TAG "RAW" + +typedef enum { + NfcCliProtocolRequestTypeNormalExecute, + NfcCliProtocolRequestTypeAbort, +} NfcCliProtocolRequestType; + +typedef enum { + NfcPollerStateStopped, + NfcPollerStateStarted, +} NfcPollerState; + +typedef NfcCommand (*NfcCliRawProtocolSpecificHandler)( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response); + +typedef struct { + Nfc* nfc; + NfcCliRawRequest request; + NfcCliRawResponse response; + + NfcPoller* poller; + NfcPollerState poller_state; + + NfcCliProtocolRequestType request_type; + FuriMessageQueue* input_queue; + FuriSemaphore* sem_done; + +} NfcCliRawCmdContext; + +static const char* raw_error_names[] = { + [NfcCliRawErrorNone] = "None", + [NfcCliRawErrorTimeout] = "Timeout", + [NfcCliRawErrorProtocol] = "Internal protocol", + [NfcCliRawErrorWrongCrc] = "Wrong CRC", + [NfcCliRawErrorNotPresent] = "No card", +}; + +static NfcCliActionContext* nfc_cli_raw_alloc_ctx(Nfc* nfc) { + furi_assert(nfc); + NfcCliRawCmdContext* instance = malloc(sizeof(NfcCliRawCmdContext)); + instance->nfc = nfc; + + instance->request.protocol = NfcProtocolInvalid; + + instance->request.tx_buffer = bit_buffer_alloc(NFC_CLI_PROTOCOL_SUPPORT_MAX_BUFFER_SIZE); + instance->response.rx_buffer = bit_buffer_alloc(NFC_CLI_PROTOCOL_SUPPORT_MAX_BUFFER_SIZE); + + instance->input_queue = furi_message_queue_alloc(5, sizeof(NfcCliProtocolRequestType)); + instance->sem_done = furi_semaphore_alloc(1, 0); + instance->response.activation_string = furi_string_alloc(); + instance->request.timeout = 0; + return instance; +} + +static void nfc_cli_raw_abort_nfc_thread(NfcCliRawCmdContext* instance) { + if(instance->poller_state == NfcPollerStateStarted) { + instance->request_type = NfcCliProtocolRequestTypeAbort; + furi_message_queue_put(instance->input_queue, &instance->request_type, FuriWaitForever); + furi_semaphore_acquire(instance->sem_done, FuriWaitForever); + instance->poller_state = NfcPollerStateStopped; + } + if(instance->poller) nfc_poller_stop(instance->poller); +} + +static void nfc_cli_raw_free_ctx(NfcCliActionContext* ctx) { + furi_assert(ctx); + NfcCliRawCmdContext* instance = ctx; + + nfc_cli_raw_abort_nfc_thread(instance); + if(instance->poller) nfc_poller_free(instance->poller); + + furi_message_queue_free(instance->input_queue); + furi_semaphore_free(instance->sem_done); + + furi_string_free(instance->response.activation_string); + bit_buffer_free(instance->response.rx_buffer); + bit_buffer_free(instance->request.tx_buffer); + instance->nfc = NULL; + free(instance); +} + +static bool nfc_cli_raw_can_reuse_ctx(NfcCliActionContext* ctx) { + furi_assert(ctx); + NfcCliRawCmdContext* instance = ctx; + NfcCliRawRequest* request = &instance->request; + + bool result = request->keep_field; + request->keep_field = false; + request->append_crc = false; + request->select = false; + instance->request.timeout = 0; + return result; +} + +const NfcCliRawProtocolSpecificHandler nfc_cli_raw_protocol_handlers[] = { + [NfcProtocolIso14443_3a] = nfc_cli_raw_iso14443_3a_handler, + [NfcProtocolIso14443_3b] = nfc_cli_raw_iso14443_3b_handler, + [NfcProtocolIso14443_4a] = NULL, + [NfcProtocolIso14443_4b] = NULL, + [NfcProtocolIso15693_3] = nfc_cli_raw_iso15693_3_handler, + [NfcProtocolFelica] = nfc_cli_raw_felica_handler, + [NfcProtocolMfUltralight] = NULL, + [NfcProtocolMfClassic] = NULL, + [NfcProtocolMfDesfire] = NULL, + [NfcProtocolSlix] = NULL, + [NfcProtocolSt25tb] = NULL, +}; + +static NfcCommand nfc_cli_raw_poller_callback(NfcGenericEventEx event, void* context) { + NfcEvent* nfc_event = event.parent_event_data; + NfcCliRawCmdContext* instance = context; + + NfcCommand command = NfcCommandContinue; + + if(nfc_event->type == NfcEventTypePollerReady) { + FURI_LOG_D(TAG, "Poller callback"); + NfcCliProtocolRequestType request_type = NfcCliProtocolRequestTypeAbort; + furi_message_queue_get(instance->input_queue, &request_type, FuriWaitForever); + + if(request_type == NfcCliProtocolRequestTypeAbort) { + command = NfcCommandStop; + } else { + const NfcCliRawProtocolSpecificHandler handler = + nfc_cli_raw_protocol_handlers[instance->request.protocol]; + if(handler) handler(event.poller, &instance->request, &instance->response); + } + } + furi_semaphore_release(instance->sem_done); + if(command == NfcCommandStop) { + FURI_LOG_D(TAG, "Aborting poller callback"); + instance->poller_state = NfcPollerStateStopped; + } + return command; +} + +static inline void nfc_cli_raw_print_result(const NfcCliRawCmdContext* instance) { + if(!furi_string_empty(instance->response.activation_string)) + printf("%s\r\n", furi_string_get_cstr(instance->response.activation_string)); + + nfc_cli_printf_array( + bit_buffer_get_data(instance->request.tx_buffer), + bit_buffer_get_size_bytes(instance->request.tx_buffer), + "Tx: "); + + if(instance->response.result != NfcCliRawErrorNone) + printf("\r\nError: \"%s\"\r\n", raw_error_names[instance->response.result]); + + size_t rx_size = bit_buffer_get_size_bytes(instance->response.rx_buffer); + if(rx_size > 0) { + nfc_cli_printf_array( + bit_buffer_get_data(instance->response.rx_buffer), + bit_buffer_get_size_bytes(instance->response.rx_buffer), + "\r\nRx: "); + } +} + +static void nfc_cli_raw_execute(PipeSide* pipe, void* context) { + UNUSED(pipe); + furi_assert(context); + NfcCliRawCmdContext* instance = context; + + furi_string_reset(instance->response.activation_string); + + if(instance->poller_state == NfcPollerStateStopped) { + if(instance->poller == NULL) + instance->poller = nfc_poller_alloc(instance->nfc, instance->request.protocol); + + nfc_poller_start_ex(instance->poller, nfc_cli_raw_poller_callback, instance); + instance->poller_state = NfcPollerStateStarted; + } + + instance->request_type = NfcCliProtocolRequestTypeNormalExecute; + furi_message_queue_put(instance->input_queue, &instance->request_type, FuriWaitForever); + furi_semaphore_acquire(instance->sem_done, FuriWaitForever); + + nfc_cli_raw_print_result(instance); +} + +static const NfcProtocolNameValuePair supported_protocols[] = { + {.name = "14a", .value = NfcProtocolIso14443_3a}, + {.name = "iso14a", .value = NfcProtocolIso14443_3a}, + + {.name = "14b", .value = NfcProtocolIso14443_3b}, + {.name = "iso14b", .value = NfcProtocolIso14443_3b}, + + {.name = "15", .value = NfcProtocolIso15693_3}, + {.name = "felica", .value = NfcProtocolFelica}, +}; + +static bool nfc_cli_raw_parse_protocol(FuriString* value, void* output) { + NfcCliRawCmdContext* ctx = output; + NfcProtocol new_protocol = NfcProtocolInvalid; + + NfcCliProtocolParser* parser = + nfc_cli_protocol_parser_alloc(supported_protocols, COUNT_OF(supported_protocols)); + + bool result = nfc_cli_protocol_parser_get(parser, value, &new_protocol); + + nfc_cli_protocol_parser_free(parser); + + if(result && ctx->request.protocol != NfcProtocolInvalid && + ctx->request.protocol != new_protocol) { + printf( + ANSI_FG_RED "Error: previous %s != new %s. Unable to continue." ANSI_RESET, + nfc_cli_get_protocol_name(ctx->request.protocol), + nfc_cli_get_protocol_name(new_protocol)); + result = false; + } + + if(result) { + ctx->request.protocol = new_protocol; + } + return result; +} + +static bool nfc_cli_raw_parse_data(FuriString* value, void* output) { + NfcCliRawCmdContext* ctx = output; + + bool result = false; + do { + size_t len = furi_string_size(value); + if(len % 2 != 0) break; + + size_t data_length = len / 2; + uint8_t* data = malloc(data_length); + + if(args_read_hex_bytes(value, data, data_length)) { + bit_buffer_reset(ctx->request.tx_buffer); + bit_buffer_copy_bytes(ctx->request.tx_buffer, data, data_length); + result = true; + } + + free(data); + } while(false); + + return result; +} + +static bool nfc_cli_raw_parse_timeout(FuriString* value, void* output) { + furi_assert(value); + furi_assert(output); + NfcCliRawCmdContext* ctx = output; + + bool result = false; + + int timeout = 0; + if(args_read_int_and_trim(value, &timeout)) { + ctx->request.timeout = timeout; + result = true; + } + return result; +} + +static bool nfc_cli_raw_parse_select(FuriString* value, void* output) { + UNUSED(value); + NfcCliRawCmdContext* ctx = output; + ctx->request.select = true; + return true; +} + +static bool nfc_cli_raw_parse_crc(FuriString* value, void* output) { + UNUSED(value); + NfcCliRawCmdContext* ctx = output; + ctx->request.append_crc = true; + return true; +} + +static bool nfc_cli_raw_parse_keep(FuriString* value, void* output) { + UNUSED(value); + NfcCliRawCmdContext* ctx = output; + ctx->request.keep_field = true; + return true; +} + +const NfcCliKeyDescriptor raw_action_keys[] = { + { + .long_name = NULL, + .short_name = "t", + .features = {.parameter = true, .required = false}, + .description = "timeout in fc", + .parse = nfc_cli_raw_parse_timeout, + }, + { + .long_name = NULL, + .short_name = "k", + .description = "keep signal field ON after receive", + .parse = nfc_cli_raw_parse_keep, + }, + { + .long_name = NULL, + .short_name = "c", + .description = "calculate and append CRC", + .parse = nfc_cli_raw_parse_crc, + }, + { + .long_name = NULL, + .short_name = "s", + .description = "Select on FieldOn", + .parse = nfc_cli_raw_parse_select, + }, + { + .long_name = "protocol", + .short_name = "p", + .description = "desired protocol. Possible values: 14a, iso14a, 14b, iso14b, 15, felica", + .features = {.parameter = true, .required = true}, + .parse = nfc_cli_raw_parse_protocol, + }, + { + .long_name = "data", + .short_name = "d", + .description = "Raw bytes to send in HEX format", + .features = {.parameter = true, .required = true}, + .parse = nfc_cli_raw_parse_data, + }, +}; + +const NfcCliActionDescriptor raw_action = { + .name = "raw", + .description = "Sends raw bytes using different protocols", + .key_count = COUNT_OF(raw_action_keys), + .keys = raw_action_keys, + .execute = nfc_cli_raw_execute, + .alloc = nfc_cli_raw_alloc_ctx, + .free = nfc_cli_raw_free_ctx, + .can_reuse = nfc_cli_raw_can_reuse_ctx, +}; + +const NfcCliActionDescriptor* raw_actions_collection[] = {&raw_action}; + +ADD_NFC_CLI_COMMAND(raw, "", raw_actions_collection); + +//Command usage: raw [keys] +//Command examples: +//raw iso14a -sc 3000 +//raw iso14a 3000 +//raw iso14a 3000 -sc diff --git a/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.h b/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.h new file mode 100644 index 000000000..d1d6dadee --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../../nfc_cli_command_base_i.h" + +extern const NfcCliCommandDescriptor raw_cmd; diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.c b/applications/main/nfc/cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.c new file mode 100644 index 000000000..c613b0d84 --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.c @@ -0,0 +1,101 @@ +#include "nfc_cli_raw_felica.h" +#include "../../../helpers/nfc_cli_format.h" + +#include +#include +#include + +#define TAG "FELICA" + +#define BIT_BUFFER_EMPTY(buffer) ((bit_buffer_get_size_bytes(buffer) == 0)) + +static inline void felica_format_activation_data(const FelicaData* data, FuriString* output) { + nfc_cli_format_array(data->idm.data, FELICA_IDM_SIZE, "IDm: ", output); + nfc_cli_format_array(data->pmm.data, FELICA_PMM_SIZE, " PMm: ", output); +} + +static NfcCliRawError nfc_cli_raw_felica_process_error(FelicaError error) { + switch(error) { + case FelicaErrorNone: + return NfcCliRawErrorNone; + case FelicaErrorTimeout: + return NfcCliRawErrorTimeout; + case FelicaErrorWrongCrc: + return NfcCliRawErrorWrongCrc; + case FelicaErrorNotPresent: + return NfcCliRawErrorNotPresent; + default: + return NfcCliRawErrorProtocol; + } +} + +static FelicaError nfc_cli_raw_felica_poller_process_error(NfcError error) { + switch(error) { + case NfcErrorNone: + return FelicaErrorNone; + case NfcErrorTimeout: + return FelicaErrorTimeout; + default: + return FelicaErrorNotPresent; + } +} + +static inline NfcCliRawError + nfc_cli_raw_felica_activate(NfcGenericInstance* poller, FuriString* activation_string) { + FelicaData felica_data = {}; + FelicaPoller* felica_poller = poller; + FURI_LOG_D(TAG, "Activating..."); + + FelicaError error = felica_poller_activate(felica_poller, &felica_data); + if(error == FelicaErrorNone) { + felica_format_activation_data(&felica_data, activation_string); + } + + return nfc_cli_raw_felica_process_error(error); +} + +static inline NfcCliRawError nfc_cli_raw_felica_txrx( + NfcGenericInstance* poller, + BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t timeout) { + FURI_LOG_D(TAG, "TxRx"); + FelicaPoller* felica_poller = poller; + + bit_buffer_reset(rx_buffer); + + FelicaError error = FelicaErrorNone; + + NfcError nfc_error = nfc_poller_trx(felica_poller->nfc, tx_buffer, rx_buffer, timeout); + if(nfc_error != NfcErrorNone) { + error = nfc_cli_raw_felica_poller_process_error(nfc_error); + } else if(!felica_crc_check(rx_buffer)) { + error = FelicaErrorWrongCrc; + } + + return nfc_cli_raw_felica_process_error(error); +} + +NfcCommand nfc_cli_raw_felica_handler( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response) { + do { + if(request->select) { + response->result = nfc_cli_raw_felica_activate(poller, response->activation_string); + } + + if(response->result != NfcCliRawErrorNone) break; + if(BIT_BUFFER_EMPTY(request->tx_buffer)) break; + + if(request->append_crc) { + FURI_LOG_D(TAG, "Add CRC"); + felica_crc_append(request->tx_buffer); + } + + uint32_t timeout = request->timeout > 0 ? request->timeout : FELICA_FDT_POLL_FC; + response->result = + nfc_cli_raw_felica_txrx(poller, request->tx_buffer, response->rx_buffer, timeout); + } while(false); + return request->keep_field ? NfcCommandContinue : NfcCommandStop; +} diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.h b/applications/main/nfc/cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.h new file mode 100644 index 000000000..ef3b6c13a --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/felica/nfc_cli_raw_felica.h @@ -0,0 +1,8 @@ +#pragma once + +#include "../nfc_cli_raw_common_types.h" + +NfcCommand nfc_cli_raw_felica_handler( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response); diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.c b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.c new file mode 100644 index 000000000..bdeaeff61 --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.c @@ -0,0 +1,81 @@ +#include "nfc_cli_raw_iso14443_3a.h" +#include "../../../helpers/nfc_cli_format.h" + +#include +#include +#include + +#define TAG "ISO14A" + +#define BIT_BUFFER_EMPTY(buffer) ((bit_buffer_get_size_bytes(buffer) == 0)) + +static NfcCliRawError nfc_cli_raw_iso14443_3a_process_error(Iso14443_3aError error) { + switch(error) { + case Iso14443_3aErrorNone: + return NfcCliRawErrorNone; + case Iso14443_3aErrorTimeout: + return NfcCliRawErrorTimeout; + case Iso14443_3aErrorWrongCrc: + return NfcCliRawErrorWrongCrc; + case Iso14443_3aErrorNotPresent: + return NfcCliRawErrorNotPresent; + default: + return NfcCliRawErrorProtocol; + } +} + +static void iso14443_3a_format_activation_data(const Iso14443_3aData* data, FuriString* output) { + nfc_cli_format_array(data->uid, data->uid_len, "UID: ", output); + furi_string_cat_printf( + output, " ATQA: %02X%02X SAK: %02X", data->atqa[0], data->atqa[1], data->sak); +} + +static inline NfcCliRawError + nfc_cli_raw_iso14443_3a_activate(NfcGenericInstance* poller, FuriString* activation_string) { + Iso14443_3aData iso3_data = {}; + FURI_LOG_D(TAG, "Activating..."); + + Iso14443_3aError error = iso14443_3a_poller_activate(poller, &iso3_data); + if(error == Iso14443_3aErrorNone) + iso14443_3a_format_activation_data(&iso3_data, activation_string); + + return nfc_cli_raw_iso14443_3a_process_error(error); +} + +static inline NfcCliRawError nfc_cli_raw_iso14443_3a_txrx( + NfcGenericInstance* poller, + BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t timeout) { + FURI_LOG_D(TAG, "TxRx"); + bit_buffer_reset(rx_buffer); + Iso14443_3aError error = iso14443_3a_poller_txrx(poller, tx_buffer, rx_buffer, timeout); + return nfc_cli_raw_iso14443_3a_process_error(error); +} + +NfcCommand nfc_cli_raw_iso14443_3a_handler( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response) { + do { + response->result = NfcCliRawErrorNone; + if(request->select) { + response->result = + nfc_cli_raw_iso14443_3a_activate(poller, response->activation_string); + } + + if(response->result != NfcCliRawErrorNone) break; + if(BIT_BUFFER_EMPTY(request->tx_buffer)) break; + + if(request->append_crc) { + FURI_LOG_D(TAG, "Add CRC"); + iso14443_crc_append(Iso14443CrcTypeA, request->tx_buffer); + } + + uint32_t timeout = request->timeout > 0 ? request->timeout : ISO14443_3A_FDT_LISTEN_FC; + response->result = + nfc_cli_raw_iso14443_3a_txrx(poller, request->tx_buffer, response->rx_buffer, timeout); + } while(false); + + return request->keep_field ? NfcCommandContinue : NfcCommandStop; +} diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.h b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.h new file mode 100644 index 000000000..5ba7fccbe --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3a/nfc_cli_raw_iso14443_3a.h @@ -0,0 +1,8 @@ +#pragma once + +#include "../nfc_cli_raw_common_types.h" + +NfcCommand nfc_cli_raw_iso14443_3a_handler( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response); diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.c b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.c new file mode 100644 index 000000000..01851eb5d --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.c @@ -0,0 +1,121 @@ +#include "nfc_cli_raw_iso14443_3b.h" +#include "../../../helpers/nfc_cli_format.h" + +#include +#include +#include + +#define TAG "ISO14B" + +#define BIT_BUFFER_EMPTY(buffer) ((bit_buffer_get_size_bytes(buffer) == 0)) + +static NfcCliRawError nfc_cli_raw_iso14443_3b_process_error(Iso14443_3bError error) { + switch(error) { + case Iso14443_3bErrorNone: + return NfcCliRawErrorNone; + case Iso14443_3bErrorTimeout: + return NfcCliRawErrorTimeout; + case Iso14443_3bErrorWrongCrc: + return NfcCliRawErrorWrongCrc; + case Iso14443_3bErrorNotPresent: + return NfcCliRawErrorNotPresent; + default: + return NfcCliRawErrorProtocol; + } +} + +static Iso14443_3bError nfc_cli_raw_iso14443_3b_poller_process_error(NfcError error) { + switch(error) { + case NfcErrorNone: + return Iso14443_3bErrorNone; + case NfcErrorTimeout: + return Iso14443_3bErrorTimeout; + default: + return Iso14443_3bErrorNotPresent; + } +} + +static void iso14443_3b_format_activation_data(const Iso14443_3bData* data, FuriString* output) { + nfc_cli_format_array(data->uid, ISO14443_3B_UID_SIZE, "UID: ", output); + + const Iso14443_3bProtocolInfo* info = &data->protocol_info; + furi_string_cat_printf( + output, + " BitRate: %d, Protocol: %d, Max Frame Size: %d, Fo: %d, Adc: %d, Fwi: %d", + info->bit_rate_capability, + info->protocol_type, + info->max_frame_size, + info->fo, + info->adc, + info->fwi); +} + +static inline NfcCliRawError nfc_cli_raw_iso14443_3b_activate( + NfcGenericInstance* poller, + Iso14443_3bData* iso3b_data, + FuriString* activation_string) { + FURI_LOG_D(TAG, "Activating..."); + + Iso14443_3bError error = iso14443_3b_poller_activate(poller, iso3b_data); + if(error == Iso14443_3bErrorNone) + iso14443_3b_format_activation_data(iso3b_data, activation_string); + + return nfc_cli_raw_iso14443_3b_process_error(error); +} + +static inline NfcCliRawError nfc_cli_raw_iso14443_3b_txrx( + NfcGenericInstance* poller, + BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t timeout) { + FURI_LOG_D(TAG, "TxRx"); + Iso14443_3bPoller* iso14b_poller = poller; + + bit_buffer_reset(rx_buffer); + + Iso14443_3bError error = Iso14443_3bErrorNone; + + NfcError nfc_error = nfc_poller_trx(iso14b_poller->nfc, tx_buffer, rx_buffer, timeout); + if(nfc_error != NfcErrorNone) { + error = nfc_cli_raw_iso14443_3b_poller_process_error(nfc_error); + } else if(!iso14443_crc_check(Iso14443CrcTypeB, rx_buffer)) { + error = Iso14443_3bErrorWrongCrc; + } + + return nfc_cli_raw_iso14443_3b_process_error(error); +} + +NfcCommand nfc_cli_raw_iso14443_3b_handler( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response) { + Iso14443_3bData iso3b_data = {0}; + bool activated = false; + do { + response->result = NfcCliRawErrorNone; + if(request->select) { + response->result = + nfc_cli_raw_iso14443_3b_activate(poller, &iso3b_data, response->activation_string); + activated = response->result == NfcCliRawErrorNone; + } + + if(response->result != NfcCliRawErrorNone) break; + if(BIT_BUFFER_EMPTY(request->tx_buffer)) break; + + uint32_t timeout = ISO14443_3B_FDT_POLL_FC; + if(request->timeout > 0) { + timeout = request->timeout; + } else if(activated) { + timeout = iso14443_3b_get_fwt_fc_max(&iso3b_data); + } + + if(request->append_crc) { + FURI_LOG_D(TAG, "Add CRC"); + iso14443_crc_append(Iso14443CrcTypeB, request->tx_buffer); + } + + response->result = + nfc_cli_raw_iso14443_3b_txrx(poller, request->tx_buffer, response->rx_buffer, timeout); + } while(false); + return request->keep_field ? NfcCommandContinue : NfcCommandStop; +} diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.h b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.h new file mode 100644 index 000000000..6fed95ef9 --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso14443_3b/nfc_cli_raw_iso14443_3b.h @@ -0,0 +1,8 @@ +#pragma once + +#include "../nfc_cli_raw_common_types.h" + +NfcCommand nfc_cli_raw_iso14443_3b_handler( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response); diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.c b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.c new file mode 100644 index 000000000..c96b7d83c --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.c @@ -0,0 +1,102 @@ +#include "nfc_cli_raw_iso15693_3.h" +#include "../../../helpers/nfc_cli_format.h" + +#include +#include +#include + +#define TAG "ISO15" + +#define BIT_BUFFER_EMPTY(buffer) ((bit_buffer_get_size_bytes(buffer) == 0)) + +static NfcCliRawError nfc_cli_raw_iso15693_3_process_error(Iso15693_3Error error) { + switch(error) { + case Iso15693_3ErrorNone: + return NfcCliRawErrorNone; + case Iso15693_3ErrorTimeout: + return NfcCliRawErrorTimeout; + case Iso15693_3ErrorWrongCrc: + return NfcCliRawErrorWrongCrc; + case Iso15693_3ErrorNotPresent: + return NfcCliRawErrorNotPresent; + default: + return NfcCliRawErrorProtocol; + } +} + +static Iso15693_3Error nfc_cli_raw_iso15693_3_poller_process_nfc_error(NfcError error) { + switch(error) { + case NfcErrorNone: + return Iso15693_3ErrorNone; + case NfcErrorTimeout: + return Iso15693_3ErrorTimeout; + default: + return Iso15693_3ErrorNotPresent; + } +} + +static inline void iso15693_3_format_activation_data(const uint8_t* data, FuriString* output) { + nfc_cli_format_array(data, ISO15693_3_UID_SIZE, "UID: ", output); +} + +static inline NfcCliRawError + nfc_cli_raw_iso15693_3_activate(NfcGenericInstance* poller, FuriString* activation_string) { + FURI_LOG_D(TAG, "Activating..."); + + Iso15693_3Poller* iso15_poller = poller; + uint8_t uid[ISO15693_3_UID_SIZE] = {0}; + + Iso15693_3Error error = iso15693_3_poller_inventory(iso15_poller, uid); + if(error == Iso15693_3ErrorNone) { + iso15693_3_format_activation_data(uid, activation_string); + } + return nfc_cli_raw_iso15693_3_process_error(error); +} + +static inline NfcCliRawError nfc_cli_raw_iso15693_3_txrx( + NfcGenericInstance* poller, + BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t timeout) { + FURI_LOG_D(TAG, "TxRx"); + + Iso15693_3Poller* iso15_poller = poller; + + bit_buffer_reset(rx_buffer); + + Iso15693_3Error error = Iso15693_3ErrorNone; + + NfcError nfc_error = nfc_poller_trx(iso15_poller->nfc, tx_buffer, rx_buffer, timeout); + if(nfc_error != NfcErrorNone) { + error = nfc_cli_raw_iso15693_3_poller_process_nfc_error(nfc_error); + } else if(!iso13239_crc_check(Iso13239CrcTypeDefault, rx_buffer)) { + error = Iso15693_3ErrorWrongCrc; + } + + return nfc_cli_raw_iso15693_3_process_error(error); +} + +NfcCommand nfc_cli_raw_iso15693_3_handler( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response) { + do { + if(request->select) { + response->result = + nfc_cli_raw_iso15693_3_activate(poller, response->activation_string); + } + + if(response->result != NfcCliRawErrorNone) break; + if(BIT_BUFFER_EMPTY(request->tx_buffer)) break; + + if(request->append_crc) { + FURI_LOG_D(TAG, "Add CRC"); + iso13239_crc_append(Iso13239CrcTypeDefault, request->tx_buffer); + } + + uint32_t timeout = request->timeout > 0 ? request->timeout : ISO15693_3_FDT_POLL_FC; + response->result = + nfc_cli_raw_iso15693_3_txrx(poller, request->tx_buffer, response->rx_buffer, timeout); + } while(false); + return request->keep_field ? NfcCommandContinue : NfcCommandStop; +} diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.h b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.h new file mode 100644 index 000000000..1911abe57 --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/iso15693_3/nfc_cli_raw_iso15693_3.h @@ -0,0 +1,8 @@ +#pragma once + +#include "../nfc_cli_raw_common_types.h" + +NfcCommand nfc_cli_raw_iso15693_3_handler( + NfcGenericInstance* poller, + const NfcCliRawRequest* request, + NfcCliRawResponse* const response); diff --git a/applications/main/nfc/cli/commands/raw/protocol_handlers/nfc_cli_raw_common_types.h b/applications/main/nfc/cli/commands/raw/protocol_handlers/nfc_cli_raw_common_types.h new file mode 100644 index 000000000..1eb3ca242 --- /dev/null +++ b/applications/main/nfc/cli/commands/raw/protocol_handlers/nfc_cli_raw_common_types.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +typedef enum { + NfcCliRawErrorNone, + NfcCliRawErrorTimeout, + NfcCliRawErrorNotPresent, + NfcCliRawErrorWrongCrc, + NfcCliRawErrorProtocol, +} NfcCliRawError; + +typedef struct { + bool select; + bool keep_field; + bool append_crc; + NfcProtocol protocol; + BitBuffer* tx_buffer; + uint32_t timeout; +} NfcCliRawRequest; + +typedef struct { + NfcCliRawError result; + BitBuffer* rx_buffer; + FuriString* activation_string; +} NfcCliRawResponse; diff --git a/applications/main/nfc/cli/nfc_cli.c b/applications/main/nfc/cli/nfc_cli.c new file mode 100644 index 000000000..60250c857 --- /dev/null +++ b/applications/main/nfc/cli/nfc_cli.c @@ -0,0 +1,124 @@ +#include "nfc_cli_commands.h" +#include "nfc_cli_command_processor.h" + +#include "applications/services/loader/loader.h" +#include "applications/services/cli/cli_main_commands.h" +#include +#include + +#define NFC_DESKTOP_APP_NAME "NFC" + +#define TAG "NfcCli" + +#define NFC_PROMPT "[" ANSI_FG_GREEN "nfc" ANSI_RESET "]" + +typedef struct { + Nfc* nfc; + CliRegistry* registry; + CliShell* shell; + NfcCliProcessorContext* processor_context; +} NfcCliContext; + +static void nfc_cli_shell_motd(void* context) { + UNUSED(context); + printf(ANSI_FG_BR_BLUE "\r\n" + " 0000 \r\n" + " 0000 \r\n" + " 000 0000 \r\n" + " 0000 00000 \r\n" + " 000 00000 0000 \r\n" + " 0 0000 0000 00000 \r\n" + " 000000 0000 00000 0000 \r\n" + " 00000000 0000 0000 0000 \r\n" + " 0000000000 0000 00000 0000 \r\n" + " 0000 00000000 00000 00000 0000 \r\n" + " 0000 0000000 00000 00000 0000 \r\n" + " 0000 000000000000 0000 0000 \r\n" + " 00000 000000000 00000 0000 \r\n" + " 00 000000 0000 00000 \r\n" + " 00 00000 0000 \r\n" + " 0000 00000 \r\n" + " 000 0000 \r\n" + " 0000 \r\n" + " 0005 \r\n" + "\r\n" ANSI_FG_BR_WHITE "Welcome to NFC Command Line Interface!\r\n" + "Run `help` or `?` to list available commands\r\n" ANSI_RESET); +} + +static void nfc_cli_subscribe_commands(NfcCliContext* instance) { + size_t cnt = nfc_cli_command_get_count(); + for(size_t i = 0; i < cnt; i++) { + const NfcCliCommandDescriptor* cmd = nfc_cli_command_get_by_index(i); + CliCommandExecuteCallback callback = nfc_cli_command_get_execute(cmd); + if(callback == NULL) continue; + const char* name = nfc_cli_command_get_name(cmd); + cli_registry_add_command( + instance->registry, + name, + CliCommandFlagParallelSafe, + callback, + instance->processor_context); + } +} + +static bool nfc_cli_desktop_app_is_running() { + FuriString* app_name = furi_string_alloc(); + Loader* ldr = furi_record_open(RECORD_LOADER); + bool result = false; + + if(loader_get_application_name(ldr, app_name)) { + result = furi_string_equal_str(app_name, NFC_DESKTOP_APP_NAME); + } + + furi_record_close(RECORD_LOADER); + furi_string_free(app_name); + return result; +} + +static NfcCliContext* nfc_cli_alloc(PipeSide* pipe) { + NfcCliContext* instance = malloc(sizeof(NfcCliContext)); + instance->nfc = nfc_alloc(); + instance->processor_context = nfc_cli_command_processor_alloc(instance->nfc); + + instance->registry = cli_registry_alloc(); + + nfc_cli_subscribe_commands(instance); + + instance->shell = + cli_shell_alloc(nfc_cli_shell_motd, instance, pipe, instance->registry, NULL); + + cli_shell_set_prompt(instance->shell, NFC_PROMPT); + return instance; +} + +void nfc_cli_free(NfcCliContext* instance) { + furi_assert(instance); + nfc_cli_command_processor_free(instance->processor_context); + + cli_shell_free(instance->shell); + cli_registry_free(instance->registry); + + nfc_free(instance->nfc); + free(instance); +} + +void nfc_cli_execute(PipeSide* pipe, FuriString* args, void* context) { + furi_assert(pipe); + UNUSED(args); + UNUSED(context); + + if(nfc_cli_desktop_app_is_running()) { + printf(ANSI_FG_YELLOW + "NFC app is running, unable to run NFC CLI at the same time!\r\n" ANSI_RESET); + return; + } + + NfcCliContext* instance = nfc_cli_alloc(pipe); + + cli_shell_start(instance->shell); + cli_shell_join(instance->shell); + + nfc_cli_free(instance); +} + +CLI_COMMAND_INTERFACE(nfc, nfc_cli_execute, CliCommandFlagParallelSafe, 1024, CLI_APPID); diff --git a/applications/main/nfc/cli/nfc_cli_command_base.h b/applications/main/nfc/cli/nfc_cli_command_base.h new file mode 100644 index 000000000..8d7f0c0ac --- /dev/null +++ b/applications/main/nfc/cli/nfc_cli_command_base.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +/** + * @brief Type for action context to be created before action execution + * must be hanlded through callbacks in each action separately + */ +typedef void NfcCliActionContext; + +/** + * @brief Callback type for function of action context allocation + * @param nfc Instance of NFC subsystem, will be used during action execution + * @return Pointer to action context + */ +typedef NfcCliActionContext* (*NfcCliActionContextAlloc)(Nfc* nfc); + +/** + * @brief Callback for action context deleting + * @param action_ctx Action context to be freed + */ +typedef void (*NfcCliActionContextFree)(NfcCliActionContext* action_ctx); + +/** + * @brief Callback invoked by command processor to determine whether already + * existing context (from previously executed command) can be reused for the new one. + * + * @param action_ctx Action context + * + * In most cases re-creating of a new context is not needed. + * It is used in 'raw' command, where nfc field sometimes need to stay turned on between + * commands. + * + * Handling of this situation and decision about reusing action context is on developer, + * who need to decide, can this command reuse context. + * + * It can be done by comparing parameters of previously executed command and a new one, + * or by some args in command. + * + * See implementation of 'keep_field' flag in 'raw' command for example. + */ +typedef bool (*NfcCliActionContextCanReuse)(NfcCliActionContext* ctx); + +/** + * @brief Action execution callback + * @param pipe provided by cli shell, can be used for command termination + * @param ctx Action context + */ +typedef void (*NfcCliActionHandlerCallback)(PipeSide* pipe, NfcCliActionContext* ctx); + +/** + * @brief Callback used for parsing argument key. + * Each key added to command must have this, otherwise parsing result will always be + * false and command will never be executed. + * + * @param value Text value for the key to be parsed + * @param ctx Action context + * @return true when parsing was fine, otherwise false. If any argument in command input + * generates false during parsing, command will not be executed and error will be shown + */ +typedef bool (*NfcCliArgParseCallback)(FuriString* value, NfcCliActionContext* ctx); + +typedef struct NfcCliKeyDescriptor NfcCliKeyDescriptor; + +typedef struct NfcCliActionDescriptor NfcCliActionDescriptor; + +typedef struct NfcCliCommandDescriptor NfcCliCommandDescriptor; diff --git a/applications/main/nfc/cli/nfc_cli_command_base_i.h b/applications/main/nfc/cli/nfc_cli_command_base_i.h new file mode 100644 index 000000000..2ffc047f9 --- /dev/null +++ b/applications/main/nfc/cli/nfc_cli_command_base_i.h @@ -0,0 +1,130 @@ +#pragma once + +#include "nfc_cli_command_base.h" +#include +#include +#include +#include "nfc_cli_command_processor.h" + +/** + * @brief How to add command. + * + * There are 3 possible option on how to add new command to nfc_cli: + * + * @see Option 1 "Add action command directly to nfc_shell" + * + * In this case command will be invoked with argument string from nfc_shell. + * Command registration must be performed directly by user. + * + * Steps: + * 1. Add new function for command to nfc_cli.c + * 2. In nfc_cli_alloc function register command using cli_registry_add_command after nfc_cli_subscribe_commands + * + * This option is NOT RECOMENDED, because such command will not have any 'help' + * processing and parsing error checks. Argument parsing must also be done by hand. + * + * -------------------------------------------------------------------------- + * + * @see Option 2 "Add action command to collection without further processing" + * + * In this case command will be invoked with argument string from nfc_shell. + * nfc_cli_command_processor is skipped, so argument handling is up to the developer. + * + * Steps: + * 1. Add new pair of nfc_cli_command_.c/.h files to /commands folder + * 2. Define const NfcCliCommandDescriptor instance in .c file and its extern definition in .h file + * 3. Include .h file to nfc_cli_commands.c file below comment "Include new commands here" + * 4. Add new command reference to nfc_cli_commands array + * 5. Add path to nfc_cli_command_.c file into 'cli_nfc' plugin in application.fam file + * + * This option suites for simple commands with no any parameters. + * @see nfc_cli_command_field.c implementation as an example. + * + * -------------------------------------------------------------------------- + * + * @see Option 3 "Add action command to collection with full processing" + * + * In this nfc_cli_command_processor will be invoked for parsing command arguments + * and action execution. Also it will handle errors and help printing. + * + * Steps: + * 1. Add new pair of nfc_cli_command_.c/.h files to /commands folder + * 2. Use macro ADD_NFC_CLI_COMMAND to define command in .c file + * 3. Define command extern in .h file, using command name from macro in form of "_cmd" + * 4. Add all desired actions and keys to your command + * 5. Include .h file to nfc_cli_commands.c file below comment "Include new commands here" + * 6. Add new command reference to nfc_cli_commands array + * 7. Add path to nfc_cli_command_.c file into 'cli_nfc' plugin in application.fam file + * + * This option suites for "difficult" commands which has actions with lots of keys. + * @see nfc_cli_command_emulate.c implementation as an example. + * + */ + +/** + * @brief Used to decorate argument with some properties + */ +typedef struct { + bool required : 1; /**< Command always needs this argument. Missing arguments with this set to true will result execution error.*/ + bool parameter : 1; /**< Such argument requires value after its name, otherwise it is a simple on/off switch */ + bool multivalue : 1; /**< Such argument can take multiple values after its name, like this "-key value1 value2 .. valueN" */ +} FURI_PACKED NfcCliKeyFeatureSupport; + +/** + * @brief Describes key for action + */ +struct NfcCliKeyDescriptor { + NfcCliKeyFeatureSupport features; /**< Features supported defining key behaviour */ + const char* long_name; /**< Long key name starts with '--' symbol in argument string */ + const char* short_name; /**< Short key name starts with '-' symbol in argument string */ + const char* description; /**< Key description showed in help */ + NfcCliArgParseCallback parse; /**< Parsing callback */ +}; + +/** + * @brief Describes action + */ +struct NfcCliActionDescriptor { + const char* name; /**< Action name MUST be the first argument after command.*/ + const char* description; /**< Description showed in help */ + size_t key_count; /**< Amount of key entries in keys array */ + const NfcCliKeyDescriptor* keys; /**< Keys available for action */ + + NfcCliActionHandlerCallback execute; /**< Action callback, invoked if parsing is ok */ + NfcCliActionContextAlloc alloc; /**< Allocates action context during command processing */ + NfcCliActionContextFree free; /**< Frees action context */ + NfcCliActionContextCanReuse can_reuse; /**< Checks context reuse possibility */ +}; + +/** + * @brief Describes command + */ +struct NfcCliCommandDescriptor { + const char* name; /** Used to register command in cli shell */ + const char* description; /**< Description showed in help */ + size_t action_count; /** Amount of actions available in scope of this particular command */ + const NfcCliActionDescriptor** actions; /**< Actions available for command */ + CliCommandExecuteCallback callback; /** Entry point for command */ +}; + +/** + * @brief This macro simplifies command creation. It fills instance of + * NfcCliCommandDescriptor and generates a callback which invokes + * nfc_cli_command_processor inside + */ +#define ADD_NFC_CLI_COMMAND(name, description, actions) \ + static void nfc_cli_command_##name##_callback( \ + PipeSide* pipe, FuriString* args, void* context); \ + \ + const NfcCliCommandDescriptor name##_cmd = { \ + #name, \ + #description, \ + COUNT_OF(actions), \ + actions, \ + nfc_cli_command_##name##_callback, \ + }; \ + \ + static void nfc_cli_command_##name##_callback( \ + PipeSide* pipe, FuriString* args, void* context) { \ + nfc_cli_command_processor_run(&name##_cmd, pipe, args, context); \ + } diff --git a/applications/main/nfc/cli/nfc_cli_command_processor.c b/applications/main/nfc/cli/nfc_cli_command_processor.c new file mode 100644 index 000000000..839ba4267 --- /dev/null +++ b/applications/main/nfc/cli/nfc_cli_command_processor.c @@ -0,0 +1,388 @@ +#include "nfc_cli_command_processor.h" +#include "nfc_cli_commands.h" +#include "nfc_cli_command_base_i.h" + +#include +#include +#include + +#define TAG "NfcCliProcessor" + +#define NFC_CLI_KEYS_FOUND_SIZE_BYTES (10 * sizeof(NfcCliKeyDescriptor*)) + +typedef enum { + NfcCliArgumentTypeShortNameKey, + NfcCliArgumentTypeShortNameKeyGroup, + NfcCliArgumentTypeLongNameKey, + + NfcCliArgumentTypeUnknown +} NfcCliArgumentType; + +/** + * @brief Error codes for different processing states + */ +typedef enum { + NfcCliProcessorErrorNone, /**< Command was parsed successfully and execute callback will be invoked*/ + NfcCliProcessorErrorNoneButHelp, /**< There was no error, but help needs to be printed. Command wil not be executed */ + NfcCliProcessorErrorActionNotFound, /**< Wrong action was passed as first command parameter */ + NfcCliProcessorErrorKeyNotSupported, /**< Unsupported key was passed in arguments. Details will be printed in erro_message*/ + NfcCliProcessorErrorKeyParameterInGroup, /**< Parameter which requires value was passed in group. Example: -sckd */ + NfcCliProcessorErrorKeyParameterValueMissing, /**< Value is missing for the parameter which requires it */ + NfcCliProcessorErrorKeyDuplication, /**< Some argument key was duplicated in input parameters */ + NfcCliProcessorErrorKeyParseError, /**< Error happened during argument value parsing */ + NfcCliProcessorErrorKeyRequiredMissing, /**< Some keys required for command execution is missing*/ + + NfcCliProcessorErrorNum +} NfcCliProcessorError; + +struct NfcCliProcessorContext { + const NfcCliCommandDescriptor* cmd; + const NfcCliActionDescriptor* action; + const NfcCliKeyDescriptor** keys_found; + uint8_t total_keys_found; + uint8_t required_keys_expected; + uint8_t required_keys_found; + + Nfc* nfc; + void* action_context; + + FuriString* error_message; +}; + +static const NfcCliActionDescriptor* + nfc_cli_get_action_from_args(const NfcCliCommandDescriptor* cmd, FuriString* args) { + const NfcCliActionDescriptor* action = cmd->actions[0]; + + bool multiple_action_cmd = nfc_cli_command_has_multiple_actions(cmd); + if(multiple_action_cmd) { + action = NULL; + FuriString* arg_str = furi_string_alloc(); + if(args_read_string_and_trim(args, arg_str)) { + action = nfc_cli_command_get_action_by_name(cmd, arg_str); + } + furi_string_free(arg_str); + } + + return action; +} + +static bool nfc_cli_action_can_reuse_context( + NfcCliProcessorContext* instance, + const NfcCliActionDescriptor* new_action) { + bool result = false; + do { + if(instance->action != new_action) break; + if(new_action->can_reuse == NULL) break; + result = new_action->can_reuse(instance->action_context); + } while(false); + return result; +} + +static void nfc_cli_action_free(NfcCliProcessorContext* instance) { + if(instance->action && instance->action->free) { + FURI_LOG_D(TAG, "Free previous \"%s\" action context", instance->action->name); + instance->action->free(instance->action_context); + } + instance->action = NULL; +} + +static NfcCliProcessorError + nfc_cli_action_alloc(NfcCliProcessorContext* instance, FuriString* args) { + const NfcCliCommandDescriptor* cmd = instance->cmd; + + NfcCliProcessorError result = NfcCliProcessorErrorNone; + do { + const NfcCliActionDescriptor* action = nfc_cli_get_action_from_args(cmd, args); + if(action == NULL) { + result = NfcCliProcessorErrorActionNotFound; + furi_string_printf(instance->error_message, "Action not found"); + break; + } + + if(!nfc_cli_action_can_reuse_context(instance, action)) { + nfc_cli_action_free(instance); + + instance->action = action; + if(action->alloc && action->free) { + FURI_LOG_D(TAG, "Allocating context for action \"%s\"", action->name); + instance->action_context = instance->action->alloc(instance->nfc); + } else if(action->alloc && (action->free == NULL)) { + FURI_LOG_W( + TAG, + "Free callback not defined for action \"%s\". Skip allocation to avoid memory leak.", + action->name); + instance->action_context = NULL; + } else { + FURI_LOG_D(TAG, "No alloc context callback for action \"%s\"", action->name); + instance->action_context = NULL; + } + } else + FURI_LOG_D(TAG, "Reusing context from previous \"%s\" action", action->name); + + memset(instance->keys_found, 0, NFC_CLI_KEYS_FOUND_SIZE_BYTES); + instance->required_keys_expected = nfc_cli_action_get_required_keys_count(action); + instance->required_keys_found = 0; + instance->total_keys_found = 0; + } while(false); + + return result; +} + +static NfcCliArgumentType nfc_cli_get_argument_type(FuriString* argument) { + size_t arg_len = furi_string_size(argument); + NfcCliArgumentType type = NfcCliArgumentTypeUnknown; + + if(arg_len > 2) { + char ch1 = furi_string_get_char(argument, 0); + char ch2 = furi_string_get_char(argument, 1); + if(ch1 == '-') { + type = (ch2 == '-') ? NfcCliArgumentTypeLongNameKey : + NfcCliArgumentTypeShortNameKeyGroup; + } + } else if(arg_len == 2) { + char ch1 = furi_string_get_char(argument, 0); + type = (ch1 == '-') ? NfcCliArgumentTypeShortNameKey : NfcCliArgumentTypeUnknown; + } + + return type; +} + +static bool + nfc_cli_check_duplicate_keys(NfcCliProcessorContext* instance, const NfcCliKeyDescriptor* key) { + bool result = false; + for(size_t i = 0; i < instance->total_keys_found; i++) { + const NfcCliKeyDescriptor* buf = instance->keys_found[i]; + if(buf != key) continue; + result = true; + break; + } + + return result; +} + +static void nfc_cli_trim_multivalue_arg(FuriString* args, FuriString* value) { + furi_string_set(value, args); + size_t index = furi_string_search_char(value, '-', 0); + if(index != STRING_FAILURE) { + furi_string_left(value, index); + furi_string_right(args, index); + } else { + furi_string_reset(args); + } +} + +static NfcCliProcessorError nfc_cli_parse_single_key( + NfcCliProcessorContext* instance, + FuriString* argument, + FuriString* args, + bool from_group) { + FuriString* value_str = furi_string_alloc(); + + NfcCliProcessorError result = NfcCliProcessorErrorNone; + do { + const NfcCliKeyDescriptor* key = + nfc_cli_action_get_key_descriptor(instance->action, argument); + if(key == NULL) { + if(furi_string_equal_str(argument, "h")) + result = NfcCliProcessorErrorNoneButHelp; + else { + furi_string_printf( + instance->error_message, + "Key \'%s\' is not supported", + furi_string_get_cstr(argument)); + result = NfcCliProcessorErrorKeyNotSupported; + } + break; + } + + if(key->features.parameter && from_group) { + furi_string_printf( + instance->error_message, + "Parameter key \'%s\' can\'t be grouped", + furi_string_get_cstr(argument)); + result = NfcCliProcessorErrorKeyParameterInGroup; + break; + } + + if(nfc_cli_check_duplicate_keys(instance, key)) { + furi_string_printf( + instance->error_message, "Duplicated key \'%s\'", furi_string_get_cstr(argument)); + result = NfcCliProcessorErrorKeyDuplication; + break; + } + + if(key->features.multivalue && !key->features.parameter) break; + if(key->features.multivalue) { + nfc_cli_trim_multivalue_arg(args, value_str); + FURI_LOG_D(TAG, "Multivalue: %s", furi_string_get_cstr(value_str)); + } else if(key->features.parameter && !args_read_string_and_trim(args, value_str)) { + result = NfcCliProcessorErrorKeyParameterValueMissing; + furi_string_printf( + instance->error_message, + "Missing value for \'%s\'", + furi_string_get_cstr(argument)); + break; + } + + if(key->parse == NULL) { + furi_string_printf( + instance->error_message, + "Parse callback for key \'%s\' not defined", + furi_string_get_cstr(argument)); + result = NfcCliProcessorErrorKeyParseError; + break; + } + + FURI_LOG_D(TAG, "Parsing key \"%s\"", furi_string_get_cstr(argument)); + if(!key->parse(value_str, instance->action_context)) { + furi_string_printf( + instance->error_message, + "Unable to parse value \'%s\' for key \'%s\'", + furi_string_get_cstr(value_str), + furi_string_get_cstr(argument)); + result = NfcCliProcessorErrorKeyParseError; + break; + } + + instance->keys_found[instance->total_keys_found] = key; + instance->total_keys_found++; + if(key->features.required) instance->required_keys_found++; + } while(false); + furi_string_free(value_str); + + return result; +} + +static NfcCliProcessorError + nfc_cli_parse_group_key(NfcCliProcessorContext* instance, FuriString* argument) { + NfcCliProcessorError result = NfcCliProcessorErrorNone; + FURI_LOG_D(TAG, "Parsing key group\"%s\"", furi_string_get_cstr(argument)); + + FuriString* arg_buf = furi_string_alloc(); + for(size_t i = 0; i < furi_string_size(argument); i++) { + furi_string_set_n(arg_buf, argument, i, 1); + result = nfc_cli_parse_single_key(instance, arg_buf, NULL, true); + if(result != NfcCliProcessorErrorNone) break; + } + furi_string_free(arg_buf); + + return result; +} + +static NfcCliProcessorError nfc_cli_parse_argument( + NfcCliProcessorContext* instance, + FuriString* argument, + FuriString* args) { + NfcCliArgumentType type = nfc_cli_get_argument_type(argument); + + furi_string_trim(argument, "-"); + + NfcCliProcessorError result = NfcCliProcessorErrorNone; + + if(type == NfcCliArgumentTypeShortNameKeyGroup) + result = nfc_cli_parse_group_key(instance, argument); + else if((type == NfcCliArgumentTypeShortNameKey) || (type == NfcCliArgumentTypeLongNameKey)) { + result = nfc_cli_parse_single_key(instance, argument, args, false); + } else if(type == NfcCliArgumentTypeUnknown) { //-V547 + result = NfcCliProcessorErrorKeyNotSupported; + furi_string_printf( + instance->error_message, + "Key \'%s\' is not supported", + furi_string_get_cstr(argument)); + } + + return result; +} + +static NfcCliProcessorError + nfc_cli_process_arguments(NfcCliProcessorContext* instance, FuriString* args) { + NfcCliProcessorError result = NfcCliProcessorErrorNone; + + FuriString* argument = furi_string_alloc(); + while(args_read_string_and_trim(args, argument)) { + result = nfc_cli_parse_argument(instance, argument, args); + if(result != NfcCliProcessorErrorNone) break; + } + furi_string_free(argument); + + if((result == NfcCliProcessorErrorNone) && + (instance->required_keys_expected != instance->required_keys_found)) { + furi_string_printf(instance->error_message, "Some required keys missing"); + result = NfcCliProcessorErrorKeyRequiredMissing; + } + + return result; +} + +static inline void nfc_cli_command_process_error( + const NfcCliProcessorContext* instance, + NfcCliProcessorError error) { + do { + if(error == NfcCliProcessorErrorNone) break; + + if(error != NfcCliProcessorErrorNoneButHelp) + printf( + ANSI_FG_RED "Error: %s\r\n" ANSI_RESET, + furi_string_get_cstr(instance->error_message)); + + if(error == NfcCliProcessorErrorActionNotFound) + nfc_cli_command_format_info(instance->cmd, instance->error_message); + else + nfc_cli_action_format_info(instance->action, instance->error_message); + + printf("\n%s", furi_string_get_cstr(instance->error_message)); + } while(false); +} + +void nfc_cli_command_processor_run( + const NfcCliCommandDescriptor* cmd, + PipeSide* pipe, + FuriString* args, + void* context) { + furi_assert(pipe); + furi_assert(cmd); + furi_assert(args); + NfcCliProcessorContext* instance = context; + furi_string_reset(instance->error_message); + + NfcCliProcessorError error = NfcCliProcessorErrorNone; + instance->cmd = cmd; + do { + error = nfc_cli_action_alloc(instance, args); + if(error != NfcCliProcessorErrorNone) break; + + error = nfc_cli_process_arguments(instance, args); + if(error != NfcCliProcessorErrorNone) break; + + if(instance->action && instance->action->execute) { + instance->action->execute(pipe, instance->action_context); + } else { + FURI_LOG_W(TAG, "Action execute callback missing"); + } + } while(false); + + nfc_cli_command_process_error(instance, error); +} + +NfcCliProcessorContext* nfc_cli_command_processor_alloc(Nfc* nfc) { + furi_assert(nfc); + NfcCliProcessorContext* instance = malloc(sizeof(NfcCliProcessorContext)); + instance->nfc = nfc; + instance->keys_found = malloc(NFC_CLI_KEYS_FOUND_SIZE_BYTES); + instance->total_keys_found = 0; + instance->required_keys_found = 0; + instance->required_keys_expected = 0; + + instance->error_message = furi_string_alloc(); + return instance; +} + +void nfc_cli_command_processor_free(NfcCliProcessorContext* instance) { + furi_assert(instance); + nfc_cli_action_free(instance); + free(instance->keys_found); + furi_string_free(instance->error_message); + + instance->nfc = NULL; + free(instance); +} diff --git a/applications/main/nfc/cli/nfc_cli_command_processor.h b/applications/main/nfc/cli/nfc_cli_command_processor.h new file mode 100644 index 000000000..2c3849c5b --- /dev/null +++ b/applications/main/nfc/cli/nfc_cli_command_processor.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include +#include "nfc_cli_command_base.h" + +typedef struct NfcCliProcessorContext NfcCliProcessorContext; + +NfcCliProcessorContext* nfc_cli_command_processor_alloc(Nfc* nfc); +void nfc_cli_command_processor_free(NfcCliProcessorContext* instance); + +void nfc_cli_command_processor_run( + const NfcCliCommandDescriptor* cmd, + PipeSide* pipe, + FuriString* args, + void* context); diff --git a/applications/main/nfc/cli/nfc_cli_commands.c b/applications/main/nfc/cli/nfc_cli_commands.c new file mode 100644 index 000000000..82321cede --- /dev/null +++ b/applications/main/nfc/cli/nfc_cli_commands.c @@ -0,0 +1,161 @@ +#include "nfc_cli_commands.h" +#include "nfc_cli_command_base_i.h" + +/** Include new commands here */ +#include "commands/raw/nfc_cli_command_raw.h" +#include "commands/apdu/nfc_cli_command_apdu.h" +#include "commands/dump/nfc_cli_command_dump.h" +#include "commands/mfu/nfc_cli_command_mfu.h" +#include "commands/nfc_cli_command_emulate.h" +#include "commands/nfc_cli_command_scanner.h" +#include "commands/nfc_cli_command_field.h" + +#define TAG "NfcCliCommands" + +/** Add new commands here */ +static const NfcCliCommandDescriptor* nfc_cli_commands[] = { + &apdu_cmd, + &raw_cmd, + &emulate_cmd, + &mfu_cmd, + &scanner_cmd, + &dump_cmd, + &field_cmd, +}; + +size_t nfc_cli_command_get_count() { + return COUNT_OF(nfc_cli_commands); +} + +const NfcCliActionDescriptor* + nfc_cli_command_get_action_by_name(const NfcCliCommandDescriptor* cmd, const FuriString* name) { + furi_assert(cmd); + furi_assert(name); + + for(size_t i = 0; i < cmd->action_count; i++) { + const NfcCliActionDescriptor* action = cmd->actions[i]; + if(furi_string_equal_str(name, action->name)) return action; + } + return NULL; +} + +const NfcCliCommandDescriptor* nfc_cli_command_get_by_index(size_t index) { + furi_assert(index < COUNT_OF(nfc_cli_commands)); + return nfc_cli_commands[index]; +} + +bool nfc_cli_command_has_multiple_actions(const NfcCliCommandDescriptor* cmd) { + furi_assert(cmd); + furi_check(cmd->action_count > 0); + return (cmd->action_count > 1); +} + +const char* nfc_cli_command_get_name(const NfcCliCommandDescriptor* cmd) { + furi_assert(cmd); + return cmd->name; +} + +CliCommandExecuteCallback nfc_cli_command_get_execute(const NfcCliCommandDescriptor* cmd) { + furi_assert(cmd); + return cmd->callback; +} + +static inline const NfcCliKeyDescriptor* nfc_cli_action_get_key_by_name( + const NfcCliActionDescriptor* action, + const FuriString* name, + bool long_name) { + for(size_t i = 0; i < action->key_count; i++) { + const NfcCliKeyDescriptor* key = &action->keys[i]; + const char* buf = long_name ? key->long_name : key->short_name; + if((buf != NULL) && furi_string_equal_str(name, buf)) return key; + } + return NULL; +} + +const NfcCliKeyDescriptor* + nfc_cli_action_get_key_descriptor(const NfcCliActionDescriptor* action, FuriString* argument) { + furi_assert(action); + furi_assert(argument); + + return nfc_cli_action_get_key_by_name(action, argument, furi_string_size(argument) > 1); +} + +size_t nfc_cli_action_get_required_keys_count(const NfcCliActionDescriptor* action) { + furi_assert(action); + + size_t required_key_count = 0; + for(size_t i = 0; i < action->key_count; i++) { + const NfcCliKeyDescriptor* key = &action->keys[i]; + if(!key->features.required) continue; + required_key_count++; + } + return required_key_count; +} + +static int nfc_cli_action_format_key_name(const NfcCliKeyDescriptor* key, FuriString* output) { + int len = 0; + FuriString* name = furi_string_alloc(); + if(key->short_name && key->long_name) { + len = furi_string_printf(name, "-%s, --%s", key->short_name, key->long_name); + } else if(key->short_name && (key->long_name == NULL)) { + len = furi_string_printf(name, "-%s", key->short_name); + } else if((key->short_name == NULL) && key->long_name) { + len = furi_string_printf(name, "--%s", key->long_name); + } + + const char* color = key->features.required ? ANSI_FLIPPER_BRAND_ORANGE : ANSI_RESET; + furi_string_printf(output, "%s%s%s", color, furi_string_get_cstr(name), ANSI_RESET); + furi_string_free(name); + return len; +} + +void nfc_cli_action_format_info(const NfcCliActionDescriptor* action, FuriString* output) { + furi_assert(action); + furi_assert(output); + furi_string_printf( + output, + action->description ? "%s - %s\r\n\n" : "%s\r\n\n", + action->name, + action->description); + + if(action->key_count == 0) return; + + FuriString* buf = furi_string_alloc(); + furi_string_cat_printf( + output, + ANSI_FG_BR_GREEN "Keys " ANSI_FLIPPER_BRAND_ORANGE "(required) " ANSI_RESET + "(optional):\r\n"); + + for(size_t i = 0; i < action->key_count; i++) { + const NfcCliKeyDescriptor* key = &action->keys[i]; + + int len = nfc_cli_action_format_key_name(key, buf); + furi_string_cat_printf(output, "%s", furi_string_get_cstr(buf)); + + if(key->description) { + const int offset = 20; + furi_string_cat_printf( + output, ANSI_CURSOR_RIGHT_BY("%d") "%s", offset - len, key->description); + } + furi_string_cat_printf(output, "\r\n"); + } + furi_string_free(buf); +} + +void nfc_cli_command_format_info(const NfcCliCommandDescriptor* cmd, FuriString* output) { + furi_assert(cmd); + furi_assert(output); + furi_string_printf(output, "%s - %s\r\n", cmd->name, cmd->description); + if(cmd->action_count > 1) { + furi_string_cat_printf(output, "Possible actions: \r\n"); + + for(size_t i = 0; i < cmd->action_count; i++) { + const NfcCliActionDescriptor* action = cmd->actions[i]; + furi_string_cat_printf( + output, + action->description ? "\t%s\t-\t%s\r\n" : "%s\r\n", + action->name, + action->description); + } + } +} diff --git a/applications/main/nfc/cli/nfc_cli_commands.h b/applications/main/nfc/cli/nfc_cli_commands.h new file mode 100644 index 000000000..eb2b30f5a --- /dev/null +++ b/applications/main/nfc/cli/nfc_cli_commands.h @@ -0,0 +1,27 @@ +#pragma once + +#include "nfc_cli_command_base.h" +#include +#include + +size_t nfc_cli_command_get_count(); + +const NfcCliCommandDescriptor* nfc_cli_command_get_by_index(size_t index); + +const char* nfc_cli_command_get_name(const NfcCliCommandDescriptor* cmd); + +CliCommandExecuteCallback nfc_cli_command_get_execute(const NfcCliCommandDescriptor* cmd); + +bool nfc_cli_command_has_multiple_actions(const NfcCliCommandDescriptor* cmd); + +const NfcCliActionDescriptor* + nfc_cli_command_get_action_by_name(const NfcCliCommandDescriptor* cmd, const FuriString* name); + +size_t nfc_cli_action_get_required_keys_count(const NfcCliActionDescriptor* action); + +const NfcCliKeyDescriptor* + nfc_cli_action_get_key_descriptor(const NfcCliActionDescriptor* action, FuriString* argument); + +void nfc_cli_command_format_info(const NfcCliCommandDescriptor* cmd, FuriString* output); + +void nfc_cli_action_format_info(const NfcCliActionDescriptor* action, FuriString* output); diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c deleted file mode 100644 index af3fd62eb..000000000 --- a/applications/main/nfc/nfc_cli.c +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include - -#define FLAG_EVENT (1 << 10) - -static void nfc_cli_print_usage(void) { - printf("Usage:\r\n"); - printf("nfc \r\n"); - printf("Cmd list:\r\n"); - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - printf("\tfield\t - turn field on\r\n"); - } -} - -static void nfc_cli_field(PipeSide* pipe, FuriString* args) { - UNUSED(args); - // Check if nfc worker is not busy - if(furi_hal_nfc_is_hal_ready() != FuriHalNfcErrorNone) { - printf("NFC chip failed to start\r\n"); - return; - } - - furi_hal_nfc_acquire(); - furi_hal_nfc_low_power_mode_stop(); - furi_hal_nfc_poller_field_on(); - - printf("Field is on. Don't leave device in this mode for too long.\r\n"); - printf("Press Ctrl+C to abort\r\n"); - - while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { - furi_delay_ms(50); - } - - furi_hal_nfc_low_power_mode_start(); - furi_hal_nfc_release(); -} - -static void execute(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(context); - FuriString* cmd; - cmd = furi_string_alloc(); - - do { - if(!args_read_string_and_trim(args, cmd)) { - nfc_cli_print_usage(); - break; - } - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - if(furi_string_cmp_str(cmd, "field") == 0) { - nfc_cli_field(pipe, args); - break; - } - } - - nfc_cli_print_usage(); - } while(false); - - furi_string_free(cmd); -} - -CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/nfc/resources/nfc/assets/vendors.nfc b/applications/main/nfc/resources/nfc/assets/vendors.nfc new file mode 100644 index 000000000..7750e99b7 --- /dev/null +++ b/applications/main/nfc/resources/nfc/assets/vendors.nfc @@ -0,0 +1,115 @@ +Filetype: NFC Vendors +Version: 1 +# Please do not change IDs in this list. Add new to the end if necessary. +# ID: "Vendor Country" +1: Motorola UK +2: ST Microelectronics SA France +3: Hitachi, Ltd Japan +4: NXP Semiconductors Germany +5: Infineon Technologies AG Germany +6: Cylink USA +7: Texas Instrument France +8: Fujitsu Limited Japan +9: Matsushita Electronics Corporation, Semiconductor Company Japan +10: NEC Japan +11: Oki Electric Industry Co. Ltd Japan +12: Toshiba Corp. Japan +13: Mitsubishi Electric Corp. Japan +14: Samsung Electronics Co. Ltd Korea +15: Hynix / Hyundai, Korea +16: LG-Semiconductors Co. Ltd Korea +17: Emosyn-EM Microelectronics USA +18: INSIDE Technology France +19: ORGA Kartensysteme GmbH Germany +20: SHARP Corporation Japan +21: ATMEL France +22: EM Microelectronic-Marin SA Switzerland +23: KSW Microtec GmbH Germany +24: ZMD AG Germany +25: XICOR, Inc. USA +26: Sony Corporation Japan +27: Malaysia Microelectronic Solutions Sdn. Bhd Malaysia +28: Emosyn USA +29: Shanghai Fudan Microelectronics Co. Ltd. P.R. China +30: Magellan Technology Pty Limited Australia +31: Melexis NV BO Switzerland +32: Renesas Technology Corp. Japan +33: TAGSYS France +34: Transcore USA +35: Shanghai belling corp., ltd. China +36: Masktech Germany Gmbh Germany +37: Innovision Research and Technology Plc UK +38: Hitachi ULSI Systems Co., Ltd. Japan +39: Cypak AB Sweden +40: Ricoh Japan +41: ASK France +42: Unicore Microsystems, LLC Russian Federation +43: Dallas Semiconductor/Maxim USA +44: Impinj, Inc. USA +45: RightPlug Alliance USA +46: Broadcom Corporation USA +47: MStar Semiconductor, Inc Taiwan, ROC +48: BeeDar Technology Inc. USA +49: RFIDsec Denmark +50: Schweizer Electronic AG Germany +51: AMIC Technology Corp Taiwan +52: Mikron JSC Russia +53: Fraunhofer Institute for Photonic Microsystems Germany +54: IDS Microchip AG Switzerland +55: Thinfilm - Kovio USA +56: HMT Microelectronic Ltd Switzerland +57: Silicon Craft Technology Thailand +58: Advanced Film Device Inc. Japan +59: Nitecrest Ltd UK +60: Verayo Inc. USA +61: HID Global USA +62: Productivity Engineering Gmbh Germany +63: Austriamicrosystems AG (reserved) Austria +64: Gemalto SA France +65: Renesas Electronics Corporation Japan +66: 3Alogics Inc Korea +67: Top TroniQ Asia Limited Hong Kong +68: Gentag Inc. USA +69: Invengo Information Technology Co.Ltd China +70: Guangzhou Sysur Microelectronics, Inc China +71: CEITEC S.A. Brazil +72: Shanghai Quanray Electronics Co. Ltd. China +73: MediaTek Inc Taiwan +74: Angstrem PJSC Russia +75: Celisic Semiconductor (Hong Kong) Limited China +76: LEGIC Identsystems AG Switzerland +77: Balluff GmbH Germany +78: Oberthur Technologies France +79: Silterra Malaysia Sdn. Bhd. Malaysia +80: DELTA Danish Electronics, Light & Acoustics Denmark +81: Giesecke & Devrient GmbH Germany +82: Shenzhen China Vision Microelectronics Co., Ltd. China +83: Shanghai Feiju Microelectronics Co. Ltd. China +84: Intel Corporation USA +85: Microsensys GmbH Germany +86: Sonix Technology Co., Ltd. Taiwan +87: Qualcomm Technologies Inc USA +88: Realtek Semiconductor Corp Taiwan +89: Freevision Technologies Co. Ltd China +90: Giantec Semiconductor Inc. China +91: JSC Angstrem-T Russia +92: STARCHIP France +93: SPIRTECH France +94: GANTNER Electronic GmbH Austria +95: Nordic Semiconductor Norway +96: Verisiti Inc USA +97: Wearlinks Technology Inc. China +98: Userstar Information Systems Co., Ltd Taiwan +99: Pragmatic Printing Ltd. UK +100: Associacao do Laboratorio de Sistemas Integraveis Tecnologico - LSI-TEC Brazil +101: Tendyron Corporation China +102: MUTO Smart Co., Ltd. Korea +103: ON Semiconductor USA +104: TUBITAK BILGEM Turkey +105: Huada Semiconductor Co., Ltd China +106: SEVENEY France +107: ISSM France +108: Wisesec Ltd Israel +124: DB HiTek Co Ltd Korea +125: SATO Vicinity Australia +126: Holtek Taiwan diff --git a/applications/services/cli/cli_vcp.h b/applications/services/cli/cli_vcp.h index 10e286183..e9a0671a8 100644 --- a/applications/services/cli/cli_vcp.h +++ b/applications/services/cli/cli_vcp.h @@ -15,7 +15,6 @@ typedef struct CliVcp CliVcp; void cli_vcp_enable(CliVcp* cli_vcp); void cli_vcp_disable(CliVcp* cli_vcp); - #ifdef __cplusplus } #endif diff --git a/lib/nfc/SConscript b/lib/nfc/SConscript index 82b87ea2a..585e98197 100644 --- a/lib/nfc/SConscript +++ b/lib/nfc/SConscript @@ -31,6 +31,7 @@ env.Append( File("protocols/iso14443_3b/iso14443_3b_poller.h"), File("protocols/iso14443_4a/iso14443_4a_poller.h"), File("protocols/iso14443_4b/iso14443_4b_poller.h"), + File("protocols/iso15693_3/iso15693_3_poller.h"), File("protocols/mf_ultralight/mf_ultralight_poller.h"), File("protocols/mf_classic/mf_classic_poller.h"), File("protocols/mf_plus/mf_plus_poller.h"), @@ -52,6 +53,7 @@ env.Append( File("protocols/felica/felica_poller_sync.h"), # Misc File("helpers/nfc_util.h"), + File("helpers/felica_crc.h"), File("helpers/iso14443_crc.h"), File("helpers/iso13239_crc.h"), File("helpers/nfc_data_generator.h"), diff --git a/lib/nfc/protocols/felica/felica.c b/lib/nfc/protocols/felica/felica.c index e9ffb771a..a298f37a9 100644 --- a/lib/nfc/protocols/felica/felica.c +++ b/lib/nfc/protocols/felica/felica.c @@ -104,6 +104,7 @@ bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { break; } } + furi_string_free(temp_str); } while(false); return parsed; diff --git a/lib/nfc/protocols/felica/felica.h b/lib/nfc/protocols/felica/felica.h index b4e7af9d1..1fcaffc53 100644 --- a/lib/nfc/protocols/felica/felica.h +++ b/lib/nfc/protocols/felica/felica.h @@ -155,15 +155,13 @@ typedef struct { FelicaFSUnion data; } FelicaData; -#pragma pack(push, 1) -typedef struct { +typedef struct FURI_PACKED { uint8_t code; FelicaIDm idm; uint8_t service_num; uint16_t service_code; uint8_t block_count; } FelicaCommandHeader; -#pragma pack(pop) typedef struct { uint8_t length; diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.c index 252c46399..be6bca970 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.c @@ -248,7 +248,8 @@ static NfcCommand mf_ultralight_poller_read_callback(NfcGenericEvent event, void poller_context->error = MfUltralightErrorNone; command = NfcCommandStop; } else if(mfu_event->type == MfUltralightPollerEventTypeReadFailed) { - poller_context->error = mfu_event->data->error; + poller_context->error = mf_ultralight_process_error( + mfu_poller->iso14443_3a_poller->iso14443_3a_event_data.error); command = NfcCommandStop; } else if(mfu_event->type == MfUltralightPollerEventTypeAuthRequest) { if(poller_context->auth_context != NULL) { diff --git a/lib/toolbox/cli/shell/cli_shell.c b/lib/toolbox/cli/shell/cli_shell.c index b2648d127..3516b51ce 100644 --- a/lib/toolbox/cli/shell/cli_shell.c +++ b/lib/toolbox/cli/shell/cli_shell.c @@ -442,7 +442,6 @@ static int32_t cli_shell_thread(void* context) { // ========== // Public API // ========== - CliShell* cli_shell_alloc( CliShellMotd motd, void* context, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 1169749cd..62f0245df 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -129,6 +129,7 @@ Header,+,lib/nanopb/pb.h,, Header,+,lib/nanopb/pb_decode.h,, Header,+,lib/nanopb/pb_encode.h,, Header,+,lib/nfc/helpers/crypto1.h,, +Header,+,lib/nfc/helpers/felica_crc.h,, Header,+,lib/nfc/helpers/iso13239_crc.h,, Header,+,lib/nfc/helpers/iso14443_crc.h,, Header,+,lib/nfc/helpers/nfc_data_generator.h,, @@ -153,6 +154,7 @@ Header,+,lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.h,, Header,+,lib/nfc/protocols/iso14443_4a/iso14443_4a_poller.h,, Header,+,lib/nfc/protocols/iso14443_4b/iso14443_4b.h,, Header,+,lib/nfc/protocols/iso14443_4b/iso14443_4b_poller.h,, +Header,+,lib/nfc/protocols/iso15693_3/iso15693_3_poller.h,, Header,+,lib/nfc/protocols/mf_classic/mf_classic.h,, Header,+,lib/nfc/protocols/mf_classic/mf_classic_listener.h,, Header,+,lib/nfc/protocols/mf_classic/mf_classic_poller.h,, @@ -1059,6 +1061,9 @@ Function,+,felica_calculate_mac_write,void,"mbedtls_des3_context*, const uint8_t Function,+,felica_calculate_session_key,void,"mbedtls_des3_context*, const uint8_t*, const uint8_t*, uint8_t*" Function,+,felica_check_mac,_Bool,"mbedtls_des3_context*, const uint8_t*, const uint8_t*, const uint8_t*, const uint8_t, uint8_t*" Function,+,felica_copy,void,"FelicaData*, const FelicaData*" +Function,+,felica_crc_append,void,BitBuffer* +Function,+,felica_crc_check,_Bool,const BitBuffer* +Function,+,felica_crc_trim,void,BitBuffer* Function,+,felica_free,void,FelicaData* Function,+,felica_get_base_data,FelicaData*,const FelicaData* Function,+,felica_get_device_name,const char*,"const FelicaData*, NfcDeviceNameType" @@ -2215,6 +2220,13 @@ Function,+,iso15693_3_get_uid,const uint8_t*,"const Iso15693_3Data*, size_t*" Function,+,iso15693_3_is_block_locked,_Bool,"const Iso15693_3Data*, uint8_t" Function,+,iso15693_3_is_equal,_Bool,"const Iso15693_3Data*, const Iso15693_3Data*" Function,+,iso15693_3_load,_Bool,"Iso15693_3Data*, FlipperFormat*, uint32_t" +Function,+,iso15693_3_poller_activate,Iso15693_3Error,"Iso15693_3Poller*, Iso15693_3Data*" +Function,+,iso15693_3_poller_get_blocks_security,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint16_t" +Function,+,iso15693_3_poller_get_system_info,Iso15693_3Error,"Iso15693_3Poller*, Iso15693_3SystemInfo*" +Function,+,iso15693_3_poller_inventory,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*" +Function,+,iso15693_3_poller_read_block,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint8_t, uint8_t" +Function,+,iso15693_3_poller_read_blocks,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint16_t, uint8_t" +Function,+,iso15693_3_poller_send_frame,Iso15693_3Error,"Iso15693_3Poller*, const BitBuffer*, BitBuffer*, uint32_t" Function,+,iso15693_3_reset,void,Iso15693_3Data* Function,+,iso15693_3_save,_Bool,"const Iso15693_3Data*, FlipperFormat*" Function,+,iso15693_3_set_uid,_Bool,"Iso15693_3Data*, const uint8_t*, size_t" From e7634d7563cd807caf5f522806e83c1d7393340b Mon Sep 17 00:00:00 2001 From: Nathan N Date: Mon, 29 Sep 2025 13:05:06 -0400 Subject: [PATCH 2/4] 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 3/4] 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 4/4] 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*