MX 2 лет назад
Родитель
Сommit
4d791b774b
30 измененных файлов с 516 добавлено и 114 удалено
  1. 4 1
      app_api_table_i.h
  2. 2 2
      application.fam
  3. 5 3
      assets/cli/cli_help.txt
  4. 13 4
      cli/plugins/details/details.c
  5. 12 8
      cli/plugins/details/formatters/table/details_output_formatter_table.c
  6. 1 0
      cli/plugins/details/formatters/table/details_output_formatter_table.h
  7. 4 0
      cli/plugins/details/formatters/tsv/details_output_formatter_tsv.c
  8. 1 0
      cli/plugins/details/formatters/tsv/details_output_formatter_tsv.h
  9. 7 7
      cli/plugins/list/formatters/table/list_output_formatter_table.c
  10. 4 4
      cli/plugins/list/formatters/tsv/list_output_formatter_tsv.c
  11. 4 2
      cli/plugins/modify/add/add.c
  12. 47 1
      cli/plugins/modify/common.c
  13. 31 8
      cli/plugins/modify/common.h
  14. 4 2
      cli/plugins/modify/update/update.c
  15. 3 1
      services/config/constants.h
  16. 21 1
      services/config/migrations/common_migration.c
  17. 77 1
      services/config/token_info_iterator.c
  18. 8 0
      services/config/token_info_iterator.h
  19. 8 0
      services/totp/totp.c
  20. 15 1
      services/totp/totp.h
  21. 35 1
      types/token_info.c
  22. 54 2
      types/token_info.h
  23. 95 39
      ui/scenes/add_new_token/totp_scene_add_new_token.c
  24. 1 1
      ui/scenes/app_settings/totp_app_settings.c
  25. 1 1
      ui/scenes/authenticate/totp_scene_authenticate.c
  26. 33 9
      ui/scenes/generate_token/totp_scene_generate_token.c
  27. 1 1
      ui/scenes/token_menu/totp_scene_token_menu.c
  28. 1 1
      version.h
  29. 22 11
      workers/generate_totp_code/generate_totp_code.c
  30. 2 2
      workers/generate_totp_code/generate_totp_code.h

+ 4 - 1
app_api_table_i.h

@@ -62,4 +62,7 @@ static constexpr auto app_api_table = sort(create_array_t<sym_entry>(
         bool,
         (TokenInfo*, const char*, size_t, PlainTokenSecretEncoding, const CryptoSettings*)),
     API_METHOD(totp_crypto_check_key_slot, bool, (uint8_t)),
-    API_METHOD(totp_bt_type_code_worker_free, void, (TotpBtTypeCodeWorkerContext*))));
+    API_METHOD(totp_bt_type_code_worker_free, void, (TotpBtTypeCodeWorkerContext*)),
+    API_METHOD(token_info_set_token_type_from_str, bool, (TokenInfo*, const FuriString*)),
+    API_METHOD(token_info_set_token_counter_from_str, bool, (TokenInfo*, const FuriString*)),
+    API_METHOD(token_info_get_type_as_cstr, const char*, (const TokenInfo*))));

+ 2 - 2
application.fam

@@ -7,9 +7,9 @@ App(
     requires=["gui", "cli", "dialogs", "storage", "input", "notification", "bt"],
     stack_size=2 * 1024,
     order=20,
-    fap_version="5.00",
+    fap_version="5.50",
     fap_author="Alexander Kopachov (@akopachov)",
-    fap_description="Software-based TOTP authenticator for Flipper Zero device",
+    fap_description="Software-based TOTP/HOTP authenticator for Flipper Zero device",
     fap_weburl="https://github.com/akopachov/flipper-zero_authenticator",
     fap_category="Tools",
     fap_icon_assets="images",

+ 5 - 3
assets/cli/cli_help.txt

@@ -3,8 +3,8 @@ Usage:
   totp version
   totp (list | ls)
   totp (lsattr | cat) <index>
-  totp (add | mk | new) <name> [-a <algo>] [-e <encoding>] [-d <digits>] [-l <duration>] [-u] [-b <feature>]...
-  totp (update) <index> [-a <algo>] [-e <encoding>] [-n <name>] [-d <digits>] [-l <duration>] [-u] [-s] [-b <feature>]...
+  totp (add | mk | new) <name> [-t <type>] [-i <counter>] [-a <algo>] [-e <encoding>] [-d <digits>] [-l <duration>] [-u] [-b <feature>]...
+  totp (update) <index> [-t <type>] [-i <counter>] [-a <algo>] [-e <encoding>] [-n <name>] [-d <digits>] [-l <duration>] [-u] [-s] [-b <feature>]...
   totp (delete | rm) <index> [-f]
   totp (move | mv) <index> <new_index>
   totp pin (set | remove) [-c <slot>]
@@ -37,10 +37,12 @@ Arguments:
   automation    Automation method to be set. Must be one of: none, usb, bt
 
 Options:
+  -t <type>      Token type. Must be one of: totp, hotp [default: totp]
+  -i <counter>   Token initial counter. Applicable for HOTP tokens only. Must be positive integer number [default: 0]
   -a <algo>      Token hashing algorithm. Must be one of: sha1, sha256, sha512, steam [default: sha1]
   -d <digits>    Number of digits to generate, one of: 5, 6, 8 [default: 6]
   -e <encoding>  Token secret encoding, one of base32, base64 [default: base32]
-  -l <duration>  Token lifetime duration in seconds, between: 15 and 255 [default: 30]
+  -l <duration>  Token lifetime duration in seconds. Applicable for TOTP tokens only.Must be between: 15 and 255 [default: 30]
   -u             Show console user input as-is without masking
   -b <feature>   Token automation features to be enabled. Must be one of: none, enter, tab [default: none]
                  # none - No features

+ 13 - 4
cli/plugins/details/details.c

@@ -19,6 +19,7 @@ typedef void (*TOTP_CLI_DETAILS_AUTOMATION_FEATURE_ITEM_FORMATTER)(
 typedef void (*TOTP_CLI_DETAILS_CSTR_FORMATTER)(const char* key, const char* value);
 typedef void (*TOTP_CLI_DETAILS_UINT8T_FORMATTER)(const char* key, uint8_t value);
 typedef void (*TOTP_CLI_DETAILS_SIZET_FORMATTER)(const char* key, size_t value);
+typedef void (*TOTP_CLI_DETAILS_UINT64T_FORMATTER)(const char* key, uint64_t value);
 
 typedef struct {
     const TOTP_CLI_DETAILS_HEADER_FORMATTER header_formatter;
@@ -27,6 +28,7 @@ typedef struct {
     const TOTP_CLI_DETAILS_CSTR_FORMATTER cstr_formatter;
     const TOTP_CLI_DETAILS_UINT8T_FORMATTER uint8t_formatter;
     const TOTP_CLI_DETAILS_SIZET_FORMATTER sizet_formatter;
+    const TOTP_CLI_DETAILS_UINT64T_FORMATTER uint64t_formatter;
 } TotpCliDetailsFormatter;
 
 static const TotpCliDetailsFormatter available_formatters[] = {
@@ -35,14 +37,16 @@ static const TotpCliDetailsFormatter available_formatters[] = {
      .automation_feature_item_formatter = &details_output_formatter_print_automation_feature_table,
      .cstr_formatter = &details_output_formatter_print_cstr_table,
      .uint8t_formatter = &details_output_formatter_print_uint8t_table,
-     .sizet_formatter = &details_output_formatter_print_sizet_table},
+     .sizet_formatter = &details_output_formatter_print_sizet_table,
+     .uint64t_formatter = &details_output_formatter_print_uint64t_table},
 
     {.header_formatter = &details_output_formatter_print_header_tsv,
      .footer_formatter = &details_output_formatter_print_footer_tsv,
      .automation_feature_item_formatter = &details_output_formatter_print_automation_feature_tsv,
      .cstr_formatter = &details_output_formatter_print_cstr_tsv,
      .uint8t_formatter = &details_output_formatter_print_uint8t_tsv,
-     .sizet_formatter = &details_output_formatter_print_sizet_tsv},
+     .sizet_formatter = &details_output_formatter_print_sizet_tsv,
+     .uint64t_formatter = &details_output_formatter_print_uint64t_tsv},
 };
 
 static void print_automation_features(
@@ -103,10 +107,15 @@ static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
 
         (*formatter->header_formatter)();
         (*formatter->sizet_formatter)("Index", token_number);
+        (*formatter->cstr_formatter)("Type", token_info_get_type_as_cstr(token_info));
         (*formatter->cstr_formatter)("Name", furi_string_get_cstr(token_info->name));
         (*formatter->cstr_formatter)("Hashing algorithm", token_info_get_algo_as_cstr(token_info));
         (*formatter->uint8t_formatter)("Number of digits", token_info->digits);
-        (*formatter->uint8t_formatter)("Token lifetime", token_info->duration);
+        if(token_info->type == TokenTypeTOTP) {
+            (*formatter->uint8t_formatter)("Token lifetime", token_info->duration);
+        } else if(token_info->type == TokenTypeHOTP) {
+            (*formatter->uint64t_formatter)("Token counter", token_info->counter);
+        }
         print_automation_features(token_info, formatter);
         (*formatter->footer_formatter)();
     } else {
@@ -128,4 +137,4 @@ static const FlipperAppPluginDescriptor plugin_descriptor = {
 
 const FlipperAppPluginDescriptor* totp_cli_details_plugin_ep() {
     return &plugin_descriptor;
-}
+}

+ 12 - 8
cli/plugins/details/formatters/table/details_output_formatter_table.c

@@ -3,31 +3,35 @@
 #include "../../../../cli_helpers.h"
 
 void details_output_formatter_print_header_table() {
-    TOTP_CLI_PRINTF("+----------------------+------------------------------+\r\n");
-    TOTP_CLI_PRINTF("| %-20s | %-28s |\r\n", "Property", "Value");
-    TOTP_CLI_PRINTF("+----------------------+------------------------------+\r\n");
+    TOTP_CLI_PRINTF("+----------------------+-------------------------------+\r\n");
+    TOTP_CLI_PRINTF("| %-20s | %-29s |\r\n", "Property", "Value");
+    TOTP_CLI_PRINTF("+----------------------+-------------------------------+\r\n");
 }
 
 void details_output_formatter_print_footer_table() {
-    TOTP_CLI_PRINTF("+----------------------+------------------------------+\r\n");
+    TOTP_CLI_PRINTF("+----------------------+-------------------------------+\r\n");
 }
 
 void details_output_formatter_print_automation_feature_table(
     const char* key,
     const char* feature,
     bool* header_printed) {
-    TOTP_CLI_PRINTF("| %-20s | %-28.28s |\r\n", *header_printed ? "" : key, feature);
+    TOTP_CLI_PRINTF("| %-20s | %-29.29s |\r\n", *header_printed ? "" : key, feature);
     *header_printed = true;
 }
 
 void details_output_formatter_print_cstr_table(const char* key, const char* value) {
-    TOTP_CLI_PRINTF("| %-20s | %-28.28s |\r\n", key, value);
+    TOTP_CLI_PRINTF("| %-20s | %-29.29s |\r\n", key, value);
 }
 
 void details_output_formatter_print_uint8t_table(const char* key, uint8_t value) {
-    TOTP_CLI_PRINTF("| %-20s | %-28" PRIu8 " |\r\n", key, value);
+    TOTP_CLI_PRINTF("| %-20s | %-29" PRIu8 " |\r\n", key, value);
 }
 
 void details_output_formatter_print_sizet_table(const char* key, size_t value) {
-    TOTP_CLI_PRINTF("| %-20s | %-28" PRIu16 " |\r\n", key, value);
+    TOTP_CLI_PRINTF("| %-20s | %-29" PRIu16 " |\r\n", key, value);
+}
+
+void details_output_formatter_print_uint64t_table(const char* key, uint64_t value) {
+    TOTP_CLI_PRINTF("| %-20s | %-29" PRIu64 " |\r\n", key, value);
 }

+ 1 - 0
cli/plugins/details/formatters/table/details_output_formatter_table.h

@@ -13,3 +13,4 @@ void details_output_formatter_print_automation_feature_table(
 void details_output_formatter_print_cstr_table(const char* key, const char* value);
 void details_output_formatter_print_uint8t_table(const char* key, uint8_t value);
 void details_output_formatter_print_sizet_table(const char* key, size_t value);
+void details_output_formatter_print_uint64t_table(const char* key, uint64_t value);

+ 4 - 0
cli/plugins/details/formatters/tsv/details_output_formatter_tsv.c

@@ -28,3 +28,7 @@ void details_output_formatter_print_uint8t_tsv(const char* key, uint8_t value) {
 void details_output_formatter_print_sizet_tsv(const char* key, size_t value) {
     TOTP_CLI_PRINTF("%s\t%" PRIu16 "\r\n", key, value);
 }
+
+void details_output_formatter_print_uint64t_tsv(const char* key, uint64_t value) {
+    TOTP_CLI_PRINTF("%s\t%" PRIu64 "\r\n", key, value);
+}

+ 1 - 0
cli/plugins/details/formatters/tsv/details_output_formatter_tsv.h

@@ -13,3 +13,4 @@ void details_output_formatter_print_automation_feature_tsv(
 void details_output_formatter_print_cstr_tsv(const char* key, const char* value);
 void details_output_formatter_print_uint8t_tsv(const char* key, uint8_t value);
 void details_output_formatter_print_sizet_tsv(const char* key, size_t value);
+void details_output_formatter_print_uint64t_tsv(const char* key, uint64_t value);

+ 7 - 7
cli/plugins/list/formatters/table/list_output_formatter_table.c

@@ -3,21 +3,21 @@
 #include "../../../../cli_helpers.h"
 
 void list_output_formatter_print_header_table() {
-    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
-    TOTP_CLI_PRINTF("| %-3s | %-25s | %-6s | %-s | %-s |\r\n", "#", "Name", "Algo", "Ln", "Dur");
-    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
+    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+------+\r\n");
+    TOTP_CLI_PRINTF("| %-3s | %-25s | %-6s | %-s | %-s |\r\n", "#", "Name", "Algo", "Ln", "Type");
+    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+------+\r\n");
 }
 
 void list_output_formatter_print_body_item_table(size_t index, const TokenInfo* token_info) {
     TOTP_CLI_PRINTF(
-        "| %-3" PRIu16 " | %-25.25s | %-6s | %-2" PRIu8 " | %-3" PRIu8 " |\r\n",
+        "| %-3" PRIu16 " | %-25.25s | %-6s | %-2" PRIu8 " | %-4s |\r\n",
         index + 1,
         furi_string_get_cstr(token_info->name),
         token_info_get_algo_as_cstr(token_info),
         token_info->digits,
-        token_info->duration);
+        token_info_get_type_as_cstr(token_info));
 }
 
 void list_output_formatter_print_footer_table() {
-    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
-}
+    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+------+\r\n");
+}

+ 4 - 4
cli/plugins/list/formatters/tsv/list_output_formatter_tsv.c

@@ -3,18 +3,18 @@
 #include "../../../../cli_helpers.h"
 
 void list_output_formatter_print_header_tsv() {
-    TOTP_CLI_PRINTF("%s\t%s\t%s\t%s\t%s\r\n", "#", "Name", "Algo", "Ln", "Dur");
+    TOTP_CLI_PRINTF("%s\t%s\t%s\t%s\t%s\r\n", "#", "Name", "Algo", "Ln", "Type");
 }
 
 void list_output_formatter_print_body_item_tsv(size_t index, const TokenInfo* token_info) {
     TOTP_CLI_PRINTF(
-        "%" PRIu16 "\t%s\t%s\t%" PRIu8 "\t%" PRIu8 "\r\n",
+        "%" PRIu16 "\t%s\t%s\t%" PRIu8 "\t%s\r\n",
         index + 1,
         furi_string_get_cstr(token_info->name),
         token_info_get_algo_as_cstr(token_info),
         token_info->digits,
-        token_info->duration);
+        token_info_get_type_as_cstr(token_info));
 }
 
 void list_output_formatter_print_footer_tsv() {
-}
+}

+ 4 - 2
cli/plugins/modify/add/add.c

@@ -43,7 +43,9 @@ static TotpIteratorUpdateTokenResult
            !totp_cli_try_read_unsecure_flag(temp_str, &parsed, &mask_user_input) &&
            !totp_cli_try_read_automation_features(token_info, temp_str, context_t->args, &parsed) &&
            !totp_cli_try_read_plain_token_secret_encoding(
-               temp_str, context_t->args, &parsed, &token_secret_encoding)) {
+               temp_str, context_t->args, &parsed, &token_secret_encoding) &&
+           !totp_cli_try_read_token_type(token_info, temp_str, context_t->args, &parsed) &&
+           !totp_cli_try_read_token_counter(token_info, temp_str, context_t->args, &parsed)) {
             totp_cli_printf_unknown_argument(temp_str);
         }
 
@@ -123,4 +125,4 @@ static const FlipperAppPluginDescriptor plugin_descriptor = {
 
 const FlipperAppPluginDescriptor* totp_cli_add_plugin_ep() {
     return &plugin_descriptor;
-}
+}

+ 47 - 1
cli/plugins/modify/common.c

@@ -133,4 +133,50 @@ bool totp_cli_try_read_plain_token_secret_encoding(
     }
 
     return false;
-}
+}
+
+bool totp_cli_try_read_token_type(
+    TokenInfo* token_info,
+    FuriString* arg,
+    FuriString* args,
+    bool* parsed) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_TYPE_PREFIX) == 0) {
+        if(!args_read_string_and_trim(args, arg)) {
+            totp_cli_printf_missed_argument_value(TOTP_CLI_COMMAND_ARG_TYPE_PREFIX);
+        } else if(!token_info_set_token_type_from_str(token_info, arg)) {
+            TOTP_CLI_PRINTF_ERROR(
+                "\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ARG_TYPE_PREFIX
+                "\"\r\n",
+                furi_string_get_cstr(arg));
+        } else {
+            *parsed = true;
+        }
+
+        return true;
+    }
+
+    return false;
+}
+
+bool totp_cli_try_read_token_counter(
+    TokenInfo* token_info,
+    FuriString* arg,
+    FuriString* args,
+    bool* parsed) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_COUNTER_PREFIX) == 0) {
+        if(!args_read_string_and_trim(args, arg)) {
+            totp_cli_printf_missed_argument_value(TOTP_CLI_COMMAND_ARG_COUNTER_PREFIX);
+        } else if(!token_info_set_token_counter_from_str(token_info, arg)) {
+            TOTP_CLI_PRINTF_ERROR(
+                "\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ARG_COUNTER_PREFIX
+                "\"\r\n",
+                furi_string_get_cstr(arg));
+        } else {
+            *parsed = true;
+        }
+
+        return true;
+    }
+
+    return false;
+}

+ 31 - 8
cli/plugins/modify/common.h

@@ -6,20 +6,15 @@
 extern "C" {
 #endif
 
-#define TOTP_CLI_COMMAND_ARG_NAME "name"
 #define TOTP_CLI_COMMAND_ARG_NAME_PREFIX "-n"
-#define TOTP_CLI_COMMAND_ARG_ALGO "algo"
 #define TOTP_CLI_COMMAND_ARG_ALGO_PREFIX "-a"
-#define TOTP_CLI_COMMAND_ARG_DIGITS "digits"
 #define TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX "-d"
 #define TOTP_CLI_COMMAND_ARG_UNSECURE_PREFIX "-u"
-#define TOTP_CLI_COMMAND_ARG_DURATION "duration"
 #define TOTP_CLI_COMMAND_ARG_DURATION_PREFIX "-l"
 #define TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX "-b"
-#define TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE "feature"
-#define TOTP_CLI_COMMAND_ARG_INDEX "index"
 #define TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX "-e"
-#define TOTP_CLI_COMMAND_ARG_SECRET_ENCODING "encoding"
+#define TOTP_CLI_COMMAND_ARG_TYPE_PREFIX "-t"
+#define TOTP_CLI_COMMAND_ARG_COUNTER_PREFIX "-i"
 
 /**
  * @brief Tries to read token hashing algo
@@ -96,6 +91,34 @@ bool totp_cli_try_read_plain_token_secret_encoding(
     bool* parsed,
     PlainTokenSecretEncoding* secret_encoding);
 
+/**
+ * @brief Tries to read token type
+ * @param token_info token info to set parsed token type to if successfully read and parsed
+ * @param arg argument to parse
+ * @param args rest of arguments
+ * @param parsed will be set to \c true if token type sucecssfully read and parsed; \c false otherwise
+ * @return \c true if \c arg represents token type argument; \c false otherwise
+ */
+bool totp_cli_try_read_token_type(
+    TokenInfo* token_info,
+    FuriString* arg,
+    FuriString* args,
+    bool* parsed);
+
+/**
+ * @brief Tries to read token counter
+ * @param token_info token info to set parsed token counter to if successfully read and parsed
+ * @param arg argument to parse
+ * @param args rest of arguments
+ * @param parsed will be set to \c true if token counter sucecssfully read and parsed; \c false otherwise
+ * @return \c true if \c arg represents token counter argument; \c false otherwise
+ */
+bool totp_cli_try_read_token_counter(
+    TokenInfo* token_info,
+    FuriString* arg,
+    FuriString* args,
+    bool* parsed);
+
 #ifdef __cplusplus
 }
-#endif
+#endif

+ 4 - 2
cli/plugins/modify/update/update.c

@@ -71,7 +71,9 @@ static TotpIteratorUpdateTokenResult
            !totp_cli_try_read_change_secret_flag(temp_str, &parsed, &update_token_secret) &&
            !totp_cli_try_read_automation_features(token_info, temp_str, context_t->args, &parsed) &&
            !totp_cli_try_read_plain_token_secret_encoding(
-               temp_str, context_t->args, &parsed, &token_secret_encoding)) {
+               temp_str, context_t->args, &parsed, &token_secret_encoding) &&
+           !totp_cli_try_read_token_type(token_info, temp_str, context_t->args, &parsed) &&
+           !totp_cli_try_read_token_counter(token_info, temp_str, context_t->args, &parsed)) {
             totp_cli_printf_unknown_argument(temp_str);
         }
 
@@ -162,4 +164,4 @@ static const FlipperAppPluginDescriptor plugin_descriptor = {
 
 const FlipperAppPluginDescriptor* totp_cli_update_plugin_ep() {
     return &plugin_descriptor;
-}
+}

+ 3 - 1
services/config/constants.h

@@ -4,7 +4,7 @@
 
 #define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/totp")
 #define CONFIG_FILE_HEADER "Flipper TOTP plugin config file"
-#define CONFIG_FILE_ACTUAL_VERSION (9)
+#define CONFIG_FILE_ACTUAL_VERSION (10)
 
 #define TOTP_CONFIG_KEY_TIMEZONE "Timezone"
 #define TOTP_CONFIG_KEY_TOKEN_NAME "TokenName"
@@ -13,6 +13,8 @@
 #define TOTP_CONFIG_KEY_TOKEN_DIGITS "TokenDigits"
 #define TOTP_CONFIG_KEY_TOKEN_DURATION "TokenDuration"
 #define TOTP_CONFIG_KEY_TOKEN_AUTOMATION_FEATURES "TokenAutomationFeatures"
+#define TOTP_CONFIG_KEY_TOKEN_TYPE "TokenType"
+#define TOTP_CONFIG_KEY_TOKEN_COUNTER "TokenCounter"
 #define TOTP_CONFIG_KEY_CRYPTO_VERIFY "Crypto"
 #define TOTP_CONFIG_KEY_SALT "Salt"
 #define TOTP_CONFIG_KEY_PINSET "PinIsSet"

+ 21 - 1
services/config/migrations/common_migration.c

@@ -182,6 +182,26 @@ bool totp_config_migrate_to_latest(
                     &default_automation_features,
                     1);
             }
+
+            if(current_version > 9) {
+                flipper_format_read_string(
+                    fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_TYPE, temp_str);
+                flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_TYPE, temp_str);
+                flipper_format_read_string(
+                    fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_COUNTER, temp_str);
+                flipper_format_write_string(
+                    fff_data_file, TOTP_CONFIG_KEY_TOKEN_COUNTER, temp_str);
+            } else {
+                const uint32_t default_token_type = TokenTypeTOTP;
+                flipper_format_write_uint32(
+                    fff_data_file, TOTP_CONFIG_KEY_TOKEN_TYPE, &default_token_type, 1);
+                const uint64_t default_counter = 0;
+                flipper_format_write_hex(
+                    fff_data_file,
+                    TOTP_CONFIG_KEY_TOKEN_COUNTER,
+                    (const uint8_t*)&default_counter,
+                    sizeof(default_counter));
+            }
         }
 
         Stream* stream = flipper_format_get_raw_stream(fff_data_file);
@@ -196,4 +216,4 @@ bool totp_config_migrate_to_latest(
 
     furi_string_free(temp_str);
     return result;
-}
+}

+ 77 - 1
services/config/token_info_iterator.c

@@ -199,6 +199,19 @@ static bool
             break;
         }
 
+        tmp_uint32 = token_info->type;
+        if(!flipper_format_write_uint32(temp_ff, TOTP_CONFIG_KEY_TOKEN_TYPE, &tmp_uint32, 1)) {
+            break;
+        }
+
+        if(!flipper_format_write_hex(
+               temp_ff,
+               TOTP_CONFIG_KEY_TOKEN_COUNTER,
+               (uint8_t*)&token_info->counter,
+               sizeof(token_info->counter))) {
+            break;
+        }
+
         Stream* temp_stream = flipper_format_get_raw_stream(temp_ff);
 
         if(!stream_rewind(temp_stream)) {
@@ -398,6 +411,54 @@ TotpIteratorUpdateTokenResult totp_token_info_iterator_update_current_token(
     return result;
 }
 
+TotpIteratorUpdateTokenResult
+    totp_token_info_iterator_current_token_inc_counter(TokenInfoIteratorContext* context) {
+    if(!seek_to_token(context->current_index, context)) {
+        return TotpIteratorUpdateTokenResultFileUpdateFailed;
+    }
+
+    Stream* stream = flipper_format_get_raw_stream(context->config_file);
+
+    size_t offset_start = stream_tell(stream);
+
+    TokenInfo* token_info = context->current_token;
+    token_info->counter++;
+
+    char buffer[sizeof(TOTP_CONFIG_KEY_TOKEN_COUNTER) + 1];
+    bool found = false;
+    while(!found) {
+        if(!stream_seek_to_char(stream, '\n', StreamDirectionForward)) {
+            break;
+        }
+
+        size_t buffer_read_size;
+        if((buffer_read_size = stream_read(stream, (uint8_t*)&buffer[0], sizeof(buffer))) == 0) {
+            break;
+        }
+
+        if(!stream_seek(stream, -(int32_t)buffer_read_size, StreamOffsetFromCurrent)) {
+            break;
+        }
+
+        if(strncmp(buffer, "\n" TOTP_CONFIG_KEY_TOKEN_COUNTER ":", sizeof(buffer)) == 0) {
+            found = true;
+        }
+    }
+
+    TotpIteratorUpdateTokenResult result = TotpIteratorUpdateTokenResultFileUpdateFailed;
+    if(found && stream_seek(stream, 1, StreamOffsetFromCurrent) &&
+       flipper_format_write_hex(
+           context->config_file,
+           TOTP_CONFIG_KEY_TOKEN_COUNTER,
+           (uint8_t*)&token_info->counter,
+           sizeof(token_info->counter))) {
+        result = TotpIteratorUpdateTokenResultSuccess;
+    }
+
+    stream_seek(stream, offset_start, StreamOffsetFromStart);
+    return result;
+}
+
 TotpIteratorUpdateTokenResult totp_token_info_iterator_add_new_token(
     TokenInfoIteratorContext* context,
     TOTP_ITERATOR_UPDATE_TOKEN_ACTION update,
@@ -521,6 +582,21 @@ bool totp_token_info_iterator_go_to(TokenInfoIteratorContext* context, size_t to
         tokenInfo->automation_features = TokenAutomationFeatureNone;
     }
 
+    if(flipper_format_read_uint32(
+           context->config_file, TOTP_CONFIG_KEY_TOKEN_TYPE, &temp_data32, 1)) {
+        tokenInfo->type = temp_data32;
+    } else {
+        tokenInfo->type = TokenTypeTOTP;
+    }
+
+    if(!flipper_format_read_hex(
+           context->config_file,
+           TOTP_CONFIG_KEY_TOKEN_COUNTER,
+           (uint8_t*)&tokenInfo->counter,
+           sizeof(tokenInfo->counter))) {
+        tokenInfo->counter = 0;
+    }
+
     stream_seek(stream, original_offset, StreamOffsetFromStart);
 
     if(token_update_needed && !totp_token_info_iterator_save_current_token_info_changes(context)) {
@@ -549,4 +625,4 @@ void totp_token_info_iterator_attach_to_config_file(
     context->config_file = config_file;
     Stream* stream = flipper_format_get_raw_stream(context->config_file);
     stream_seek(stream, context->last_seek_offset, StreamOffsetFromStart);
-}
+}

+ 8 - 0
services/config/token_info_iterator.h

@@ -70,6 +70,14 @@ TotpIteratorUpdateTokenResult totp_token_info_iterator_update_current_token(
     TOTP_ITERATOR_UPDATE_TOKEN_ACTION update,
     const void* update_context);
 
+/**
+ * @brief Increments token counter
+ * @param context token info iterator context
+ * @return \c true if operation succeeded; \c false otherwise
+ */
+TotpIteratorUpdateTokenResult
+    totp_token_info_iterator_current_token_inc_counter(TokenInfoIteratorContext* context);
+
 /**
  * @brief Adds new token info to the end of the list using given update action
  * @param context token info iterator context

+ 8 - 0
services/totp/totp.c

@@ -72,6 +72,14 @@ uint64_t totp_at(
         algo, plain_secret, plain_secret_length, totp_timecode(interval, for_time_adjusted));
 }
 
+uint64_t hotp_at(
+    TOTP_ALGO algo,
+    const uint8_t* plain_secret,
+    size_t plain_secret_length,
+    uint64_t counter) {
+    return otp_generate(algo, plain_secret, plain_secret_length, counter);
+}
+
 static int totp_algo_common(
     int type,
     const uint8_t* key,

+ 15 - 1
services/totp/totp.h

@@ -37,7 +37,7 @@ extern const TOTP_ALGO TOTP_ALGO_SHA256;
 extern const TOTP_ALGO TOTP_ALGO_SHA512;
 
 /**
- * @brief Generates a OTP key using the totp algorithm.
+ * @brief Generates a TOTP key using the totp algorithm.
  * @param algo hashing algorithm to be used
  * @param plain_secret plain token secret
  * @param plain_secret_length plain token secret length
@@ -53,3 +53,17 @@ uint64_t totp_at(
     uint64_t for_time,
     float timezone,
     uint8_t interval);
+
+/**
+ * @brief Generates a HOTP key using the hotp algorithm.
+ * @param algo hashing algorithm to be used
+ * @param plain_secret plain token secret
+ * @param plain_secret_length plain token secret length
+ * @param counter the HOTP counter
+ * @return HOTP code if code was successfully generated; 0 otherwise
+ */
+uint64_t hotp_at(
+    TOTP_ALGO algo,
+    const uint8_t* plain_secret,
+    size_t plain_secret_length,
+    uint64_t counter);

+ 35 - 1
types/token_info.c

@@ -3,6 +3,7 @@
 #include <base32.h>
 #include <base64.h>
 #include <memset_s.h>
+#include <inttypes.h>
 #include "common.h"
 #include "../services/crypto/crypto_facade.h"
 
@@ -183,6 +184,37 @@ bool token_info_set_automation_feature_from_str(TokenInfo* token_info, const Fur
     return false;
 }
 
+bool token_info_set_token_type_from_str(TokenInfo* token_info, const FuriString* str) {
+    if(furi_string_cmpi_str(str, TOKEN_TYPE_TOTP_NAME) == 0) {
+        token_info->type = TokenTypeTOTP;
+        return true;
+    }
+
+    if(furi_string_cmpi_str(str, TOKEN_TYPE_HOTP_NAME) == 0) {
+        token_info->type = TokenTypeHOTP;
+        return true;
+    }
+
+    return false;
+}
+
+const char* token_info_get_type_as_cstr(const TokenInfo* token_info) {
+    switch(token_info->type) {
+    case TokenTypeTOTP:
+        return TOKEN_TYPE_TOTP_NAME;
+    case TokenTypeHOTP:
+        return TOKEN_TYPE_HOTP_NAME;
+    default:
+        break;
+    }
+
+    return NULL;
+}
+
+bool token_info_set_token_counter_from_str(TokenInfo* token_info, const FuriString* str) {
+    return sscanf(furi_string_get_cstr(str), "%" PRIu64, &token_info->counter) == 1;
+}
+
 TokenInfo* token_info_clone(const TokenInfo* src) {
     TokenInfo* clone = token_info_alloc();
     memcpy(clone, src, sizeof(TokenInfo));
@@ -203,5 +235,7 @@ void token_info_set_defaults(TokenInfo* token_info) {
     token_info->digits = TokenDigitsCountDefault;
     token_info->duration = TokenDurationDefault;
     token_info->automation_features = TokenAutomationFeatureNone;
+    token_info->type = TokenTypeTOTP;
+    token_info->counter = 0;
     furi_string_reset(token_info->name);
-}
+}

+ 54 - 2
types/token_info.h

@@ -15,6 +15,8 @@
 #define TOKEN_AUTOMATION_FEATURE_ENTER_AT_THE_END_NAME "enter"
 #define TOKEN_AUTOMATION_FEATURE_TAB_AT_THE_END_NAME "tab"
 #define TOKEN_AUTOMATION_FEATURE_TYPE_SLOWER_NAME "slower"
+#define TOKEN_TYPE_TOTP_NAME "totp"
+#define TOKEN_TYPE_HOTP_NAME "hotp"
 
 #ifdef __cplusplus
 extern "C" {
@@ -25,6 +27,7 @@ typedef uint8_t TokenDigitsCount;
 typedef uint8_t TokenDuration;
 typedef uint8_t TokenAutomationFeature;
 typedef uint8_t PlainTokenSecretEncoding;
+typedef uint8_t TokenType;
 
 /**
  * @brief Hashing algorithm to be used to generate token
@@ -147,6 +150,22 @@ enum PlainTokenSecretEncodings {
     PlainTokenSecretEncodingBase64 = 1
 };
 
+/**
+ * @brief Token types
+ */
+enum TokenTypes {
+
+    /**
+     * @brief Time-based One-time Password token type
+     */
+    TokenTypeTOTP = 0,
+
+    /**
+     * @brief HMAC-Based One-Time Password token type
+     */
+    TokenTypeHOTP = 1
+};
+
 /**
  * @brief TOTP token information
  */
@@ -185,6 +204,16 @@ typedef struct {
      * @brief Token input automation features
      */
     TokenAutomationFeature automation_features;
+
+    /**
+     * @brief Token type
+     */
+    TokenType type;
+
+    /**
+     * @brief HOTP counter
+     */
+    uint64_t counter;
 } TokenInfo;
 
 /**
@@ -248,8 +277,8 @@ bool token_info_set_algo_from_str(TokenInfo* token_info, const FuriString* str);
 bool token_info_set_algo_from_int(TokenInfo* token_info, uint8_t algo_code);
 
 /**
- * @brief Gets token hahsing algorithm name as C-string
- * @param token_info instance which token hahsing algorithm name should be returned
+ * @brief Gets token hashing algorithm name as C-string
+ * @param token_info instance which token hashing algorithm name should be returned
  * @return token hashing algorithm name as C-string
  */
 const char* token_info_get_algo_as_cstr(const TokenInfo* token_info);
@@ -262,6 +291,29 @@ const char* token_info_get_algo_as_cstr(const TokenInfo* token_info);
  */
 bool token_info_set_automation_feature_from_str(TokenInfo* token_info, const FuriString* str);
 
+/**
+ * @brief Sets token type from \c str value
+ * @param token_info instance whichs token type should be updated
+ * @param str desired token type
+ * @return \c true if token type has been set; \c false otherwise
+ */
+bool token_info_set_token_type_from_str(TokenInfo* token_info, const FuriString* str);
+
+/**
+ * @brief Gets token type as C-string
+ * @param token_info instance which token type should be returned
+ * @return token type as C-string
+ */
+const char* token_info_get_type_as_cstr(const TokenInfo* token_info);
+
+/**
+ * @brief Sets token counter from \c str value
+ * @param token_info instance whichs token counter should be updated
+ * @param str desired token counter
+ * @return \c true if token counter has been set; \c false otherwise
+ */
+bool token_info_set_token_counter_from_str(TokenInfo* token_info, const FuriString* str);
+
 /**
  * @brief Clones \c TokenInfo instance
  * @param src instance to clone

+ 95 - 39
ui/scenes/add_new_token/totp_scene_add_new_token.c

@@ -12,6 +12,8 @@
 
 char* TOKEN_ALGO_LIST[] = {"SHA1", "SHA256", "SHA512", "Steam"};
 char* TOKEN_DIGITS_TEXT_LIST[] = {"5 digits", "6 digits", "8 digits"};
+char* TOKEN_TYPE_LIST[] = {"Time-based (TOTP)", "Counter-based (HOTP)"};
+
 TokenDigitsCount TOKEN_DIGITS_VALUE_LIST[] = {
     TokenDigitsCountFive,
     TokenDigitsCountSix,
@@ -20,9 +22,10 @@ TokenDigitsCount TOKEN_DIGITS_VALUE_LIST[] = {
 typedef enum {
     TokenNameTextBox,
     TokenSecretTextBox,
+    TokenTypeSelect,
     TokenAlgoSelect,
     TokenLengthSelect,
-    TokenDurationSelect,
+    TokenDurationOrCounterSelect,
     ConfirmButton,
 } Control;
 
@@ -37,7 +40,10 @@ typedef struct {
     TokenHashAlgo algo;
     uint8_t digits_count_index;
     uint8_t duration;
+    char* initial_counter;
+    size_t initial_counter_length;
     FuriString* duration_text;
+    TokenType type;
 } SceneState;
 
 struct TotpAddContext {
@@ -45,7 +51,10 @@ struct TotpAddContext {
     const CryptoSettings* crypto_settings;
 };
 
-enum TotpIteratorUpdateTokenResultsEx { TotpIteratorUpdateTokenResultInvalidSecret = 1 };
+enum TotpIteratorUpdateTokenResultsEx {
+    TotpIteratorUpdateTokenResultInvalidSecret = 1,
+    TotpIteratorUpdateTokenResultInvalidCounter = 2
+};
 
 static void update_duration_text(SceneState* scene_state) {
     furi_string_printf(scene_state->duration_text, "%d sec.", scene_state->duration);
@@ -53,6 +62,10 @@ static void update_duration_text(SceneState* scene_state) {
 
 static TotpIteratorUpdateTokenResult add_token_handler(TokenInfo* tokenInfo, const void* context) {
     const struct TotpAddContext* context_t = context;
+    if(sscanf(context_t->scene_state->initial_counter, "%" PRIu64, &tokenInfo->counter) != 1) {
+        return TotpIteratorUpdateTokenResultInvalidCounter;
+    }
+
     if(!token_info_set_secret(
            tokenInfo,
            context_t->scene_state->token_secret,
@@ -69,6 +82,7 @@ static TotpIteratorUpdateTokenResult add_token_handler(TokenInfo* tokenInfo, con
     tokenInfo->algo = context_t->scene_state->algo;
     tokenInfo->digits = TOKEN_DIGITS_VALUE_LIST[context_t->scene_state->digits_count_index];
     tokenInfo->duration = context_t->scene_state->duration;
+    tokenInfo->type = context_t->scene_state->type;
 
     return TotpIteratorUpdateTokenResultSuccess;
 }
@@ -93,6 +107,33 @@ static void ask_user_input(
     }
 }
 
+static void update_screen_y_offset(SceneState* scene_state) {
+    if(scene_state->selected_control > TokenLengthSelect) {
+        scene_state->screen_y_offset = 68;
+    } else if(scene_state->selected_control > TokenAlgoSelect) {
+        scene_state->screen_y_offset = 51;
+    } else if(scene_state->selected_control > TokenTypeSelect) {
+        scene_state->screen_y_offset = 34;
+    } else if(scene_state->selected_control > TokenSecretTextBox) {
+        scene_state->screen_y_offset = 17;
+    } else {
+        scene_state->screen_y_offset = 0;
+    }
+}
+
+static void
+    show_invalid_field_message(const PluginState* plugin_state, Control control, const char* text) {
+    DialogMessage* message = dialog_message_alloc();
+    SceneState* scene_state = plugin_state->current_scene_state;
+    dialog_message_set_buttons(message, "Back", NULL, NULL);
+    dialog_message_set_text(
+        message, text, SCREEN_WIDTH_CENTER, SCREEN_HEIGHT_CENTER, AlignCenter, AlignCenter);
+    dialog_message_show(plugin_state->dialogs_app, message);
+    dialog_message_free(message);
+    scene_state->selected_control = control;
+    update_screen_y_offset(scene_state);
+}
+
 void totp_scene_add_new_token_activate(PluginState* plugin_state) {
     SceneState* scene_state = malloc(sizeof(SceneState));
     furi_check(scene_state != NULL);
@@ -101,6 +142,8 @@ void totp_scene_add_new_token_activate(PluginState* plugin_state) {
     scene_state->token_name_length = strlen(scene_state->token_name);
     scene_state->token_secret = "Secret";
     scene_state->token_secret_length = strlen(scene_state->token_secret);
+    scene_state->initial_counter = "Counter";
+    scene_state->initial_counter_length = strlen(scene_state->initial_counter);
 
     scene_state->screen_y_offset = 0;
 
@@ -108,6 +151,7 @@ void totp_scene_add_new_token_activate(PluginState* plugin_state) {
 
     scene_state->duration = TokenDurationDefault;
     scene_state->duration_text = furi_string_alloc();
+    scene_state->type = TokenTypeTOTP;
     update_duration_text(scene_state);
 }
 
@@ -124,33 +168,50 @@ void totp_scene_add_new_token_render(Canvas* const canvas, const PluginState* pl
         27 - scene_state->screen_y_offset,
         scene_state->token_secret,
         scene_state->selected_control == TokenSecretTextBox);
+
     ui_control_select_render(
         canvas,
         0,
         44 - scene_state->screen_y_offset,
         SCREEN_WIDTH,
-        TOKEN_ALGO_LIST[scene_state->algo],
-        scene_state->selected_control == TokenAlgoSelect);
+        TOKEN_TYPE_LIST[scene_state->type],
+        scene_state->selected_control == TokenTypeSelect);
+
     ui_control_select_render(
         canvas,
         0,
         61 - scene_state->screen_y_offset,
         SCREEN_WIDTH,
-        TOKEN_DIGITS_TEXT_LIST[scene_state->digits_count_index],
-        scene_state->selected_control == TokenLengthSelect);
-
+        TOKEN_ALGO_LIST[scene_state->algo],
+        scene_state->selected_control == TokenAlgoSelect);
     ui_control_select_render(
         canvas,
         0,
         78 - scene_state->screen_y_offset,
         SCREEN_WIDTH,
-        furi_string_get_cstr(scene_state->duration_text),
-        scene_state->selected_control == TokenDurationSelect);
+        TOKEN_DIGITS_TEXT_LIST[scene_state->digits_count_index],
+        scene_state->selected_control == TokenLengthSelect);
+
+    if(scene_state->type == TokenTypeTOTP) {
+        ui_control_select_render(
+            canvas,
+            0,
+            95 - scene_state->screen_y_offset,
+            SCREEN_WIDTH,
+            furi_string_get_cstr(scene_state->duration_text),
+            scene_state->selected_control == TokenDurationOrCounterSelect);
+    } else {
+        ui_control_text_box_render(
+            canvas,
+            95 - scene_state->screen_y_offset,
+            scene_state->initial_counter,
+            scene_state->selected_control == TokenDurationOrCounterSelect);
+    }
 
     ui_control_button_render(
         canvas,
         SCREEN_WIDTH_CENTER - 24,
-        101 - scene_state->screen_y_offset,
+        119 - scene_state->screen_y_offset,
         48,
         13,
         "Confirm",
@@ -164,18 +225,6 @@ void totp_scene_add_new_token_render(Canvas* const canvas, const PluginState* pl
     canvas_set_font(canvas, FontSecondary);
 }
 
-void update_screen_y_offset(SceneState* scene_state) {
-    if(scene_state->selected_control > TokenLengthSelect) {
-        scene_state->screen_y_offset = 51;
-    } else if(scene_state->selected_control > TokenAlgoSelect) {
-        scene_state->screen_y_offset = 34;
-    } else if(scene_state->selected_control > TokenSecretTextBox) {
-        scene_state->screen_y_offset = 17;
-    } else {
-        scene_state->screen_y_offset = 0;
-    }
-}
-
 bool totp_scene_add_new_token_handle_event(
     const PluginEvent* const event,
     PluginState* plugin_state) {
@@ -216,10 +265,14 @@ bool totp_scene_add_new_token_handle_event(
             } else if(scene_state->selected_control == TokenLengthSelect) {
                 totp_roll_value_uint8_t(
                     &scene_state->digits_count_index, 1, 0, 2, RollOverflowBehaviorRoll);
-            } else if(scene_state->selected_control == TokenDurationSelect) {
+            } else if(
+                scene_state->selected_control == TokenDurationOrCounterSelect &&
+                scene_state->type == TokenTypeTOTP) {
                 totp_roll_value_uint8_t(
                     &scene_state->duration, 15, 15, 255, RollOverflowBehaviorStop);
                 update_duration_text(scene_state);
+            } else if(scene_state->selected_control == TokenTypeSelect) {
+                totp_roll_value_uint8_t(&scene_state->type, 1, 0, 1, RollOverflowBehaviorRoll);
             }
             break;
         case InputKeyLeft:
@@ -233,10 +286,14 @@ bool totp_scene_add_new_token_handle_event(
             } else if(scene_state->selected_control == TokenLengthSelect) {
                 totp_roll_value_uint8_t(
                     &scene_state->digits_count_index, -1, 0, 2, RollOverflowBehaviorRoll);
-            } else if(scene_state->selected_control == TokenDurationSelect) {
+            } else if(
+                scene_state->selected_control == TokenDurationOrCounterSelect &&
+                scene_state->type == TokenTypeTOTP) {
                 totp_roll_value_uint8_t(
                     &scene_state->duration, -15, 15, 255, RollOverflowBehaviorStop);
                 update_duration_text(scene_state);
+            } else if(scene_state->selected_control == TokenTypeSelect) {
+                totp_roll_value_uint8_t(&scene_state->type, -1, 0, 1, RollOverflowBehaviorRoll);
             }
             break;
         case InputKeyOk:
@@ -247,7 +304,7 @@ bool totp_scene_add_new_token_handle_event(
         default:
             break;
         }
-    } else if(event->input.type == InputTypeRelease && event->input.key == InputKeyOk) {
+    } else if(event->input.type == InputTypeShort && event->input.key == InputKeyOk) {
         switch(scene_state->selected_control) {
         case TokenNameTextBox:
             ask_user_input(
@@ -267,7 +324,14 @@ bool totp_scene_add_new_token_handle_event(
             break;
         case TokenLengthSelect:
             break;
-        case TokenDurationSelect:
+        case TokenDurationOrCounterSelect:
+            if(scene_state->type == TokenTypeHOTP) {
+                ask_user_input(
+                    plugin_state,
+                    "Initial counter",
+                    &scene_state->initial_counter,
+                    &scene_state->initial_counter_length);
+            }
             break;
         case ConfirmButton: {
             struct TotpAddContext add_context = {
@@ -280,19 +344,11 @@ bool totp_scene_add_new_token_handle_event(
             if(add_result == TotpIteratorUpdateTokenResultSuccess) {
                 totp_scene_director_activate_scene(plugin_state, TotpSceneGenerateToken);
             } else if(add_result == TotpIteratorUpdateTokenResultInvalidSecret) {
-                DialogMessage* message = dialog_message_alloc();
-                dialog_message_set_buttons(message, "Back", NULL, NULL);
-                dialog_message_set_text(
-                    message,
-                    "Token secret is invalid",
-                    SCREEN_WIDTH_CENTER,
-                    SCREEN_HEIGHT_CENTER,
-                    AlignCenter,
-                    AlignCenter);
-                dialog_message_show(plugin_state->dialogs_app, message);
-                dialog_message_free(message);
-                scene_state->selected_control = TokenSecretTextBox;
-                update_screen_y_offset(scene_state);
+                show_invalid_field_message(
+                    plugin_state, TokenSecretTextBox, "Token secret is invalid");
+            } else if(add_result == TotpIteratorUpdateTokenResultInvalidCounter) {
+                show_invalid_field_message(
+                    plugin_state, TokenDurationOrCounterSelect, "Initial counter is invalid");
             } else if(add_result == TotpIteratorUpdateTokenResultFileUpdateFailed) {
                 totp_dialogs_config_updating_error(plugin_state);
             }

+ 1 - 1
ui/scenes/app_settings/totp_app_settings.c

@@ -345,7 +345,7 @@ bool totp_scene_app_settings_handle_event(
             break;
         }
     } else if(
-        event->input.type == InputTypeRelease && event->input.key == InputKeyOk &&
+        event->input.type == InputTypeShort && event->input.key == InputKeyOk &&
         scene_state->selected_control == ConfirmButton) {
         plugin_state->timezone_offset =
             (float)scene_state->tz_offset_hours + (float)scene_state->tz_offset_minutes / 60.0f;

+ 1 - 1
ui/scenes/authenticate/totp_scene_authenticate.c

@@ -84,7 +84,7 @@ bool totp_scene_authenticate_handle_event(
     }
 
     SceneState* scene_state = plugin_state->current_scene_state;
-    if(event->input.type == InputTypePress) {
+    if(event->input.type == InputTypeShort) {
         switch(event->input.key) {
         case InputKeyUp:
             if(scene_state->code_length < MAX_CODE_LENGTH) {

+ 33 - 9
ui/scenes/generate_token/totp_scene_generate_token.c

@@ -255,8 +255,8 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
     const SceneState* scene_state = (SceneState*)plugin_state->current_scene_state;
 
     canvas_set_font(canvas, FontPrimary);
-    const char* token_name_cstr =
-        furi_string_get_cstr(totp_token_info_iterator_get_current_token(iterator_context)->name);
+    const TokenInfo* token_info = totp_token_info_iterator_get_current_token(iterator_context);
+    const char* token_name_cstr = furi_string_get_cstr(token_info->name);
     uint16_t token_name_width = canvas_string_width(canvas, token_name_cstr);
     if(SCREEN_WIDTH - token_name_width > 18) {
         canvas_draw_str_aligned(
@@ -277,12 +277,21 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_
 
     draw_totp_code(canvas, plugin_state);
 
-    canvas_draw_box(
-        canvas,
-        scene_state->ui_precalculated_dimensions.progress_bar_x,
-        SCREEN_HEIGHT - PROGRESS_BAR_MARGIN - PROGRESS_BAR_HEIGHT,
-        scene_state->ui_precalculated_dimensions.progress_bar_width,
-        PROGRESS_BAR_HEIGHT);
+    if(token_info->type == TokenTypeTOTP) {
+        canvas_draw_box(
+            canvas,
+            scene_state->ui_precalculated_dimensions.progress_bar_x,
+            SCREEN_HEIGHT - PROGRESS_BAR_MARGIN - PROGRESS_BAR_HEIGHT,
+            scene_state->ui_precalculated_dimensions.progress_bar_width,
+            PROGRESS_BAR_HEIGHT);
+    } else {
+        char buffer[21];
+        snprintf(&buffer[0], sizeof(buffer), "%" PRIu64, token_info->counter);
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(
+            canvas, SCREEN_WIDTH_CENTER, SCREEN_HEIGHT - 5, AlignCenter, AlignCenter, buffer);
+    }
+
     if(totp_token_info_iterator_get_total_count(iterator_context) > 1) {
         canvas_draw_icon(canvas, 0, SCREEN_HEIGHT_CENTER - 24, &I_totp_arrow_left_8x9);
         canvas_draw_icon(
@@ -359,6 +368,21 @@ bool totp_scene_generate_token_handle_event(
                 scene_state->notification_app,
                 get_notification_sequence_automation(plugin_state, scene_state));
             return true;
+        } else if(event->input.key == InputKeyOk) {
+            TokenInfoIteratorContext* iterator_context =
+                totp_config_get_token_iterator_context(plugin_state);
+            const TokenInfo* token_info =
+                totp_token_info_iterator_get_current_token(iterator_context);
+            if(token_info->type == TokenTypeHOTP) {
+                scene_state = (SceneState*)plugin_state->current_scene_state;
+                totp_token_info_iterator_current_token_inc_counter(iterator_context);
+                totp_generate_code_worker_notify(
+                    scene_state->generate_code_worker_context,
+                    TotpGenerateCodeWorkerEventForceUpdate);
+                notification_message(
+                    scene_state->notification_app,
+                    get_notification_sequence_new_token(plugin_state, scene_state));
+            }
         }
 #endif
     } else if(event->input.type == InputTypePress || event->input.type == InputTypeRepeat) {
@@ -404,7 +428,7 @@ bool totp_scene_generate_token_handle_event(
         default:
             break;
         }
-    } else if(event->input.type == InputTypeRelease && event->input.key == InputKeyOk) {
+    } else if(event->input.type == InputTypeShort && event->input.key == InputKeyOk) {
         totp_scene_director_activate_scene(plugin_state, TotpSceneTokenMenu);
     }
 

+ 1 - 1
ui/scenes/token_menu/totp_scene_token_menu.c

@@ -125,7 +125,7 @@ bool totp_scene_token_menu_handle_event(const PluginEvent* const event, PluginSt
         default:
             break;
         }
-    } else if(event->input.type == InputTypeRelease && event->input.key == InputKeyOk) {
+    } else if(event->input.type == InputTypeShort && event->input.key == InputKeyOk) {
         switch(scene_state->selected_control) {
         case AddNewToken: {
 #ifdef TOTP_UI_ADD_NEW_TOKEN_ENABLED

+ 1 - 1
version.h

@@ -1,5 +1,5 @@
 #pragma once
 
 #define TOTP_APP_VERSION_MAJOR (5)
-#define TOTP_APP_VERSION_MINOR (0)
+#define TOTP_APP_VERSION_MINOR (5)
 #define TOTP_APP_VERSION_PATCH (0)

+ 22 - 11
workers/generate_totp_code/generate_totp_code.c

@@ -72,17 +72,23 @@ static void generate_totp_code(
         uint8_t* key = totp_crypto_decrypt(
             token_info->token, token_info->token_length, context->crypto_settings, &key_length);
 
-        int_token_to_str(
-            totp_at(
+        uint64_t otp_code;
+        if(token_info->type == TokenTypeTOTP) {
+            otp_code = totp_at(
                 get_totp_algo_impl(token_info->algo),
                 key,
                 key_length,
                 current_ts,
                 context->timezone_offset,
-                token_info->duration),
-            context->code_buffer,
-            token_info->digits,
-            token_info->algo);
+                token_info->duration);
+        } else if(token_info->type == TokenTypeHOTP) {
+            otp_code = hotp_at(
+                get_totp_algo_impl(token_info->algo), key, key_length, token_info->counter);
+        } else {
+            furi_crash("Unknown token type");
+        }
+
+        int_token_to_str(otp_code, context->code_buffer, token_info->digits, token_info->algo);
         memset_s(key, key_length, 0, key_length);
         free(key);
     } else {
@@ -116,14 +122,18 @@ static int32_t totp_generate_worker_callback(void* context) {
             continue;
         }
 
-        uint32_t curr_ts = furi_hal_rtc_get_timestamp();
+        const bool is_time_based = token_info->type == TokenTypeTOTP;
+
+        uint32_t curr_ts = is_time_based ? furi_hal_rtc_get_timestamp() : 0;
 
         bool time_left = false;
         if(flags & TotpGenerateCodeWorkerEventForceUpdate ||
-           (time_left = (curr_ts % token_info->duration) == 0)) {
+           (is_time_based && (time_left = (curr_ts % token_info->duration) == 0))) {
             if(furi_mutex_acquire(t_context->code_buffer_sync, FuriWaitForever) == FuriStatusOk) {
                 generate_totp_code(t_context, token_info, curr_ts);
-                curr_ts = furi_hal_rtc_get_timestamp();
+                if(is_time_based) {
+                    curr_ts = furi_hal_rtc_get_timestamp();
+                }
                 furi_mutex_release(t_context->code_buffer_sync);
                 if(t_context->on_new_code_generated_handler != NULL) {
                     (*(t_context->on_new_code_generated_handler))(
@@ -132,7 +142,8 @@ static int32_t totp_generate_worker_callback(void* context) {
             }
         }
 
-        if(t_context->on_code_lifetime_changed_handler != NULL) {
+        if(t_context->on_code_lifetime_changed_handler != NULL &&
+           token_info->type == TokenTypeTOTP) {
             (*(t_context->on_code_lifetime_changed_handler))(
                 (float)(token_info->duration - curr_ts % token_info->duration) /
                     (float)token_info->duration,
@@ -196,4 +207,4 @@ void totp_generate_code_worker_set_lifetime_changed_handler(
     furi_check(context != NULL);
     context->on_code_lifetime_changed_handler = on_code_lifetime_changed_handler;
     context->on_code_lifetime_changed_handler_context = on_code_lifetime_changed_handler_context;
-}
+}

+ 2 - 2
workers/generate_totp_code/generate_totp_code.h

@@ -27,7 +27,7 @@ enum TotGenerateCodeWorkerEvents {
     TotpGenerateCodeWorkerEventStop = 0b01,
 
     /**
-     * @brief Trigger token input automation
+     * @brief Triggers OTP code generation
      */
     TotpGenerateCodeWorkerEventForceUpdate = 0b10
 };
@@ -83,4 +83,4 @@ void totp_generate_code_worker_set_code_generated_handler(
 void totp_generate_code_worker_set_lifetime_changed_handler(
     TotpGenerateCodeWorkerContext* context,
     TOTP_CODE_LIFETIME_CHANGED_HANDLER on_code_lifetime_changed_handler,
-    void* on_code_lifetime_changed_handler_context);
+    void* on_code_lifetime_changed_handler_context);