Procházet zdrojové kódy

Add totp from https://github.com/akopachov/flipper-zero_authenticator

git-subtree-dir: totp
git-subtree-mainline: 01a4a4aaab7e996cc9d6b0e7b732c3d0b9c3ef0f
git-subtree-split: b082440d697ad086eead58a135d02ba0cec0049c
Willy-JL před 2 roky
rodič
revize
2a1ace251e
100 změnil soubory, kde provedl 5340 přidání a 0 odebrání
  1. 191 0
      totp/.clang-format
  2. binární
      totp/.flipcorg/banner.png
  3. binární
      totp/.flipcorg/gallery/1.png
  4. binární
      totp/.flipcorg/gallery/2.png
  5. binární
      totp/.flipcorg/gallery/3.png
  6. binární
      totp/.flipcorg/gallery/4.png
  7. binární
      totp/.flipcorg/gallery/5.png
  8. binární
      totp/.flipcorg/gallery/6.png
  9. binární
      totp/.flipcorg/gallery/7.png
  10. 1 0
      totp/.gitsubtree
  11. 13 0
      totp/app_api_interface.h
  12. 19 0
      totp/app_api_table.cpp
  13. 68 0
      totp/app_api_table_i.h
  14. 171 0
      totp/application.fam
  15. 57 0
      totp/assets/cli/cli_help.txt
  16. binární
      totp/assets/fonts/00.font
  17. binární
      totp/assets/fonts/01.font
  18. binární
      totp/assets/fonts/02.font
  19. binární
      totp/assets/fonts/03.font
  20. binární
      totp/assets/fonts/04.font
  21. binární
      totp/assets/fonts/05.font
  22. binární
      totp/assets/fonts/06.font
  23. binární
      totp/assets/fonts/07.font
  24. binární
      totp/assets/fonts/08.font
  25. binární
      totp/assets/fonts/09.font
  26. 175 0
      totp/cli/cli.c
  27. 19 0
      totp/cli/cli.h
  28. 65 0
      totp/cli/cli_helpers.h
  29. 13 0
      totp/cli/cli_plugin_interface.h
  30. 95 0
      totp/cli/cli_shared_methods.c
  31. 62 0
      totp/cli/cli_shared_methods.h
  32. 173 0
      totp/cli/plugins/automation/automation.c
  33. 4 0
      totp/cli/plugins/automation/meta.h
  34. 95 0
      totp/cli/plugins/delete/delete.c
  35. 5 0
      totp/cli/plugins/delete/meta.h
  36. 140 0
      totp/cli/plugins/details/details.c
  37. 37 0
      totp/cli/plugins/details/formatters/table/details_output_formatter_table.c
  38. 16 0
      totp/cli/plugins/details/formatters/table/details_output_formatter_table.h
  39. 34 0
      totp/cli/plugins/details/formatters/tsv/details_output_formatter_tsv.c
  40. 16 0
      totp/cli/plugins/details/formatters/tsv/details_output_formatter_tsv.h
  41. 5 0
      totp/cli/plugins/details/meta.h
  42. 42 0
      totp/cli/plugins/help/help.c
  43. 6 0
      totp/cli/plugins/help/meta.h
  44. 23 0
      totp/cli/plugins/list/formatters/table/list_output_formatter_table.c
  45. 9 0
      totp/cli/plugins/list/formatters/table/list_output_formatter_table.h
  46. 20 0
      totp/cli/plugins/list/formatters/tsv/list_output_formatter_tsv.c
  47. 9 0
      totp/cli/plugins/list/formatters/tsv/list_output_formatter_tsv.h
  48. 82 0
      totp/cli/plugins/list/list.c
  49. 5 0
      totp/cli/plugins/list/meta.h
  50. 128 0
      totp/cli/plugins/modify/add/add.c
  51. 6 0
      totp/cli/plugins/modify/add/meta.h
  52. 182 0
      totp/cli/plugins/modify/common.c
  53. 124 0
      totp/cli/plugins/modify/common.h
  54. 4 0
      totp/cli/plugins/modify/update/meta.h
  55. 167 0
      totp/cli/plugins/modify/update/update.c
  56. 5 0
      totp/cli/plugins/move/meta.h
  57. 75 0
      totp/cli/plugins/move/move.c
  58. 4 0
      totp/cli/plugins/notification/meta.h
  59. 97 0
      totp/cli/plugins/notification/notification.c
  60. 4 0
      totp/cli/plugins/pin/meta.h
  61. 179 0
      totp/cli/plugins/pin/pin.c
  62. 4 0
      totp/cli/plugins/reset/meta.h
  63. 43 0
      totp/cli/plugins/reset/reset.c
  64. 5 0
      totp/cli/plugins/timezone/meta.h
  65. 49 0
      totp/cli/plugins/timezone/timezone.c
  66. 4 0
      totp/cli/plugins/version/meta.h
  67. 27 0
      totp/cli/plugins/version/version.c
  68. 45 0
      totp/config/app/config.h
  69. 34 0
      totp/config/wolfssl/config.h
  70. binární
      totp/images/DolphinCommon_56x48.png
  71. binární
      totp/images/hid_ble_31x9.png
  72. binární
      totp/images/hid_usb_31x9.png
  73. binární
      totp/images/totp_arrow_left_8x9.png
  74. binární
      totp/images/totp_arrow_right_8x9.png
  75. 60 0
      totp/lib/base32/base32.c
  76. 40 0
      totp/lib/base32/base32.h
  77. 73 0
      totp/lib/base64/base64.c
  78. 14 0
      totp/lib/base64/base64.h
  79. 22 0
      totp/lib/polyfills/memset_s.c
  80. 31 0
      totp/lib/polyfills/memset_s.h
  81. 11 0
      totp/lib/polyfills/strnlen.c
  82. 6 0
      totp/lib/polyfills/strnlen.h
  83. 28 0
      totp/lib/roll_value/roll_value.c
  84. 59 0
      totp/lib/roll_value/roll_value.h
  85. 16 0
      totp/lib/timezone_utils/timezone_utils.c
  86. 18 0
      totp/lib/timezone_utils/timezone_utils.h
  87. 1 0
      totp/lib/wolfssl
  88. 739 0
      totp/services/config/config.c
  89. 112 0
      totp/services/config/config.h
  90. 3 0
      totp/services/config/config_file_context.h
  91. 26 0
      totp/services/config/constants.h
  92. 219 0
      totp/services/config/migrations/common_migration.c
  93. 13 0
      totp/services/config/migrations/common_migration.h
  94. 628 0
      totp/services/config/token_info_iterator.c
  95. 139 0
      totp/services/config/token_info_iterator.h
  96. 4 0
      totp/services/convert/convert.h
  97. 23 0
      totp/services/crypto/common_types.h
  98. 14 0
      totp/services/crypto/constants.h
  99. 118 0
      totp/services/crypto/crypto_facade.c
  100. 67 0
      totp/services/crypto/crypto_facade.h

+ 191 - 0
totp/.clang-format

@@ -0,0 +1,191 @@
+---
+Language:        Cpp
+AccessModifierOffset: -4
+AlignAfterOpenBracket: AlwaysBreak
+AlignArrayOfStructures: None
+AlignConsecutiveMacros: None
+AlignConsecutiveAssignments: None
+AlignConsecutiveBitFields: None
+AlignConsecutiveDeclarations: None
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments: false
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortEnumsOnASingleLine: true
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLoopsOnASingleLine: true
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+AttributeMacros:
+  - __capability
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  AfterExternBlock: false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: true
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit:     99
+CommentPragmas:  '^ IWYU pragma:'
+QualifierAlignment: Leave
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+PackConstructorInitializers: BinPack
+BasedOnStyle:    ''
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+AllowAllConstructorInitializersOnNextLine: true
+FixNamespaceComments: false
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentRequires:  false
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+LambdaBodyIndentation: Signature
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakString: 10
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+PenaltyIndentedWhitespace: 0
+PointerAlignment: Left
+PPIndentWidth:   -1
+ReferenceAlignment: Pointer
+ReflowComments:  false
+RemoveBracesLLVM: false
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SortIncludes:    Never
+SortJavaStaticImport: Before
+SortUsingDeclarations: false
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: Never
+SpaceBeforeParensOptions:
+  AfterControlStatements: false
+  AfterForeachMacros: false
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   false
+  AfterOverloadedOperator: false
+  BeforeNonEmptyParentheses: false
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+BitFieldColonSpacing: Both
+Standard:        c++03
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        4
+UseCRLF:         false
+UseTab:          Never
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+  - NS_SWIFT_NAME
+  - CF_SWIFT_NAME
+...
+

binární
totp/.flipcorg/banner.png


binární
totp/.flipcorg/gallery/1.png


binární
totp/.flipcorg/gallery/2.png


binární
totp/.flipcorg/gallery/3.png


binární
totp/.flipcorg/gallery/4.png


binární
totp/.flipcorg/gallery/5.png


binární
totp/.flipcorg/gallery/6.png


binární
totp/.flipcorg/gallery/7.png


+ 1 - 0
totp/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/akopachov/flipper-zero_authenticator master totp

+ 13 - 0
totp/app_api_interface.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <flipper_application/api_hashtable/api_hashtable.h>
+
+/* 
+ * Resolver interface with private application's symbols. 
+ * Implementation is contained in app_api_table.c
+ */
+#ifdef __cplusplus
+extern "C" const ElfApiInterface* const application_api_interface;
+#else
+extern const ElfApiInterface* const application_api_interface;
+#endif

+ 19 - 0
totp/app_api_table.cpp

@@ -0,0 +1,19 @@
+#include <flipper_application/api_hashtable/api_hashtable.h>
+#include <flipper_application/api_hashtable/compilesort.hpp>
+#include "app_api_interface.h"
+#include "app_api_table_i.h"
+
+static_assert(!has_hash_collisions(app_api_table), "Detected API method hash collision!");
+
+constexpr HashtableApiInterface applicaton_hashtable_api_interface{
+    {
+        .api_version_major = 0,
+        .api_version_minor = 0,
+        .resolver_callback = &elf_resolve_from_hashtable,
+    },
+    .table_cbegin = app_api_table.cbegin(),
+    .table_cend = app_api_table.cend(),
+};
+
+extern "C" const ElfApiInterface* const application_api_interface =
+    &applicaton_hashtable_api_interface;

+ 68 - 0
totp/app_api_table_i.h

@@ -0,0 +1,68 @@
+#include <stdbool.h>
+#include <cli/cli.h>
+#include <lib/print/wrappers.h>
+#include <lib/toolbox/args.h>
+#include <memset_s.h>
+#include "services/crypto/crypto_facade.h"
+#include "ui/scene_director.h"
+#include "services/config/config.h"
+#include "cli/cli_helpers.h"
+#include "workers/bt_type_code/bt_type_code.h"
+
+static constexpr auto app_api_table = sort(create_array_t<sym_entry>(
+    API_METHOD(memset_s, errno_t, (void*, rsize_t, int, rsize_t)),
+    API_METHOD(totp_scene_director_activate_scene, void, (PluginState* const, Scene)),
+    API_METHOD(totp_scene_director_force_redraw, void, (PluginState* const)),
+    API_METHOD(totp_config_file_update_timezone_offset, bool, (const PluginState*)),
+    API_METHOD(totp_config_file_reset, void, (PluginState* const)),
+    API_METHOD(
+        totp_config_get_token_iterator_context,
+        TokenInfoIteratorContext*,
+        (const PluginState*)),
+    API_METHOD(totp_config_file_backup, char*, (const PluginState*)),
+    API_METHOD(
+        totp_config_file_update_encryption,
+        bool,
+        (PluginState*, uint8_t, const uint8_t*, uint8_t)),
+    API_METHOD(totp_config_file_update_automation_method, bool, (const PluginState*)),
+    API_METHOD(totp_config_file_update_notification_method, bool, (const PluginState*)),
+    API_METHOD(totp_token_info_iterator_get_total_count, size_t, (const TokenInfoIteratorContext*)),
+    API_METHOD(
+        totp_token_info_iterator_get_current_token_index,
+        size_t,
+        (const TokenInfoIteratorContext*)),
+    API_METHOD(totp_token_info_iterator_go_to, bool, (TokenInfoIteratorContext*, size_t)),
+    API_METHOD(
+        totp_token_info_iterator_get_current_token,
+        const TokenInfo*,
+        (const TokenInfoIteratorContext*)),
+    API_METHOD(
+        totp_token_info_iterator_add_new_token,
+        TotpIteratorUpdateTokenResult,
+        (TokenInfoIteratorContext*, TOTP_ITERATOR_UPDATE_TOKEN_ACTION, const void*)),
+    API_METHOD(
+        totp_token_info_iterator_update_current_token,
+        TotpIteratorUpdateTokenResult,
+        (TokenInfoIteratorContext*, TOTP_ITERATOR_UPDATE_TOKEN_ACTION, const void*)),
+    API_METHOD(
+        totp_token_info_iterator_move_current_token_info,
+        bool,
+        (TokenInfoIteratorContext*, size_t)),
+    API_METHOD(
+        totp_token_info_iterator_remove_current_token_info,
+        bool,
+        (TokenInfoIteratorContext*)),
+    API_METHOD(token_info_get_algo_as_cstr, const char*, (const TokenInfo*)),
+    API_METHOD(token_info_set_algo_from_str, bool, (TokenInfo*, const FuriString*)),
+    API_METHOD(token_info_set_digits_from_int, bool, (TokenInfo*, uint8_t)),
+    API_METHOD(token_info_set_duration_from_int, bool, (TokenInfo*, uint8_t)),
+    API_METHOD(token_info_set_automation_feature_from_str, bool, (TokenInfo*, const FuriString*)),
+    API_METHOD(
+        token_info_set_secret,
+        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(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*))));

+ 171 - 0
totp/application.fam

@@ -0,0 +1,171 @@
+App(
+    appid="totp",
+    name="Authenticator",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="totp_app",
+    cdefines=["APP_TOTP"],
+    requires=["gui", "cli", "dialogs", "storage", "input", "notification", "bt"],
+    stack_size=2 * 1024,
+    order=20,
+    fap_version="5.50",
+    fap_author="Alexander Kopachov (@akopachov)",
+    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",
+    fap_icon="totp_10px.png",
+    fap_file_assets="assets",
+    fap_private_libs=[
+        Lib(
+            name="base32",
+        ),
+        Lib(
+            name="base64",
+        ),
+        Lib(
+            name="timezone_utils",
+        ),
+        Lib(
+            name="polyfills",
+        ),
+        Lib(
+            name="roll_value",
+        ),
+        Lib(
+            name="wolfssl",
+            sources=[
+                "wolfcrypt/src/pwdbased.c",
+                "wolfcrypt/src/hmac.c",
+                "wolfcrypt/src/hash.c",
+                "wolfcrypt/src/sha.c",
+                "wolfcrypt/src/sha256.c",
+                "wolfcrypt/src/sha512.c",
+            ],
+            cflags=["-Wno-error"],
+            cdefines=["HAVE_CONFIG_H"],
+            cincludes=["config/wolfssl"],
+        ),
+    ],
+)
+
+App(
+    appid="totp_cli_timezone_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_timezone_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/timezone/timezone.c", "cli/cli_shared_methods.c"],
+)
+
+App(
+    appid="totp_cli_version_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_version_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/version/version.c"],
+)
+
+App(
+    appid="totp_cli_help_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_help_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/help/help.c"],
+)
+
+App(
+    appid="totp_cli_list_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_list_plugin_ep",
+    requires=["totp"],
+    sources=[
+        "cli/plugins/list/list.c",
+        "cli/cli_shared_methods.c",
+        "cli/plugins/list/formatters/table/list_output_formatter_table.c",
+        "cli/plugins/list/formatters/tsv/list_output_formatter_tsv.c",
+    ],
+)
+
+App(
+    appid="totp_cli_details_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_details_plugin_ep",
+    requires=["totp"],
+    sources=[
+        "cli/plugins/details/details.c",
+        "cli/cli_shared_methods.c",
+        "cli/plugins/details/formatters/table/details_output_formatter_table.c",
+        "cli/plugins/details/formatters/tsv/details_output_formatter_tsv.c",
+    ],
+)
+
+App(
+    appid="totp_cli_add_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_add_plugin_ep",
+    requires=["totp"],
+    sources=[
+        "cli/plugins/modify/add/add.c",
+        "cli/plugins/modify/common.c",
+        "cli/cli_shared_methods.c",
+    ],
+)
+
+App(
+    appid="totp_cli_update_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_update_plugin_ep",
+    requires=["totp"],
+    sources=[
+        "cli/plugins/modify/update/update.c",
+        "cli/plugins/modify/common.c",
+        "cli/cli_shared_methods.c",
+    ],
+)
+
+App(
+    appid="totp_cli_delete_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_delete_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/delete/delete.c", "cli/cli_shared_methods.c"],
+)
+
+App(
+    appid="totp_cli_move_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_move_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/move/move.c", "cli/cli_shared_methods.c"],
+)
+
+App(
+    appid="totp_cli_reset_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_reset_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/reset/reset.c", "cli/cli_shared_methods.c"],
+)
+
+App(
+    appid="totp_cli_pin_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_pin_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/pin/pin.c", "cli/cli_shared_methods.c"],
+)
+
+App(
+    appid="totp_cli_automation_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_automation_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/automation/automation.c", "cli/cli_shared_methods.c"],
+)
+
+App(
+    appid="totp_cli_notification_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="totp_cli_notification_plugin_ep",
+    requires=["totp"],
+    sources=["cli/plugins/notification/notification.c", "cli/cli_shared_methods.c"],
+)

+ 57 - 0
totp/assets/cli/cli_help.txt

@@ -0,0 +1,57 @@
+Usage:
+  totp (help | h | ?)
+  totp version
+  totp (list | ls)
+  totp (lsattr | cat) <index>
+  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>]
+  totp notify [<notification>...]
+  totp (timezone | tz) [<timezone>]
+  totp reset
+  totp automation [-k <layout>] [<automation>...]
+
+Commands:
+  help, h, ?       Show command usage help
+  version          Get application version
+  list, ls         List all available tokens
+  lsattr, cat      Displays token details
+  add, mk, new     Add new token
+  update           Update existing token
+  delete, rm       Delete existing token
+  move, mv         Move token
+  pin              Set\change\remove PIN
+  notify           Get or set notification method
+  timezone, tz     Get or set current timezone
+  reset            Reset application to default settings
+  automation       Get or set automation settings
+
+Arguments:
+  name          Token name
+  index         Token index in the list
+  new_index     New token index in the list
+  notification  Notification method to be set. Must be one of: none, sound, vibro
+  timezone      Timezone offset in hours to be set
+  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. 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
+                 # enter - Type <Enter> key at the end of token input automation
+                 # tab - Type <Tab> key at the end of token input automation
+                 # slower - Type slower
+  -n <name>      Token name
+  -s             Update token secret
+  -f             Force command to do not ask user for interactive confirmation
+  -c <slot>      New crypto key slot. Must be between 12 and 100
+  -k <layout>    Automation keyboard layout. Must be one of: QWERTY, AZERTY, QWERTZ
+  

binární
totp/assets/fonts/00.font


binární
totp/assets/fonts/01.font


binární
totp/assets/fonts/02.font


binární
totp/assets/fonts/03.font


binární
totp/assets/fonts/04.font


binární
totp/assets/fonts/05.font


binární
totp/assets/fonts/06.font


binární
totp/assets/fonts/07.font


binární
totp/assets/fonts/08.font


binární
totp/assets/fonts/09.font


+ 175 - 0
totp/cli/cli.c

@@ -0,0 +1,175 @@
+#include "cli.h"
+#include <lib/toolbox/args.h>
+#include <flipper_application/flipper_application.h>
+#include <flipper_application/plugins/composite_resolver.h>
+#include <loader/firmware_api/firmware_api.h>
+#include "cli_helpers.h"
+#include "plugins/timezone/meta.h"
+#include "plugins/list/meta.h"
+#include "plugins/modify/add/meta.h"
+#include "plugins/modify/update/meta.h"
+#include "plugins/delete/meta.h"
+#include "plugins/help/meta.h"
+#include "plugins/move/meta.h"
+#include "plugins/pin/meta.h"
+#include "plugins/notification/meta.h"
+#include "plugins/reset/meta.h"
+#include "plugins/automation/meta.h"
+#include "plugins/details/meta.h"
+#include "plugins/version/meta.h"
+#include "cli_plugin_interface.h"
+#include "../app_api_interface.h"
+
+struct TotpCliContext {
+    PluginState* plugin_state;
+    CompositeApiResolver* plugin_api_resolver;
+};
+
+static void totp_cli_print_unknown_command(const FuriString* unknown_command) {
+    TOTP_CLI_PRINTF_ERROR(
+        "Command \"%s\" is unknown. Use \"" TOTP_CLI_COMMAND_HELP
+        "\" command to get list of available commands.",
+        furi_string_get_cstr(unknown_command));
+}
+
+static void run_external_cli_plugin_handler(
+    const char* handler_name,
+    TotpCliContext* cli_context,
+    FuriString* args,
+    Cli* cli) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperApplication* plugin_app = flipper_application_alloc(
+        storage, composite_api_resolver_get(cli_context->plugin_api_resolver));
+    do {
+        FuriString* full_handler_path =
+            furi_string_alloc_printf(EXT_PATH("apps_data/totp/plugins/%s.fal"), handler_name);
+        FlipperApplicationPreloadStatus preload_res =
+            flipper_application_preload(plugin_app, furi_string_get_cstr(full_handler_path));
+        furi_string_free(full_handler_path);
+
+        if(preload_res != FlipperApplicationPreloadStatusSuccess) {
+            TOTP_CLI_PRINTF_ERROR("Failed to preload plugin. Code: %d\r\n", preload_res);
+            break;
+        }
+
+        if(!flipper_application_is_plugin(plugin_app)) {
+            TOTP_CLI_PRINTF_ERROR("Plugin file is not a library\r\n");
+            break;
+        }
+
+        FlipperApplicationLoadStatus load_status = flipper_application_map_to_memory(plugin_app);
+        if(load_status != FlipperApplicationLoadStatusSuccess) {
+            TOTP_CLI_PRINTF_ERROR("Failed to load plugin file. Code %d\r\n", load_status);
+            break;
+        }
+
+        const FlipperAppPluginDescriptor* app_descriptor =
+            flipper_application_plugin_get_descriptor(plugin_app);
+
+        if(strcmp(app_descriptor->appid, PLUGIN_APP_ID) != 0) {
+            TOTP_CLI_PRINTF_ERROR("Plugin doesn't seems to be a valid TOTP CLI plugin\r\n");
+            break;
+        }
+
+        if(app_descriptor->ep_api_version != PLUGIN_API_VERSION) {
+            TOTP_CLI_PRINTF_ERROR(
+                "Plugin version %" PRIu32 " is not compatible with your app version\r\n",
+                app_descriptor->ep_api_version);
+            break;
+        }
+
+        const CliPlugin* plugin = app_descriptor->entry_point;
+
+        plugin->handle(cli_context->plugin_state, args, cli);
+    } while(false);
+    flipper_application_free(plugin_app);
+
+    furi_record_close(RECORD_STORAGE);
+}
+
+static void totp_cli_handler(Cli* cli, FuriString* args, void* context) {
+    TotpCliContext* cli_context = context;
+
+    FuriString* cmd = furi_string_alloc();
+
+    args_read_string_and_trim(args, cmd);
+
+    const char* external_plugin_name = NULL;
+
+    if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_HELP) == 0 ||
+       furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_HELP_ALT) == 0 ||
+       furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_HELP_ALT2) == 0 || furi_string_empty(cmd)) {
+        external_plugin_name = TOTP_CLI_PLUGIN_HELP_FILE_NAME;
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_ADD) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_ADD_ALT) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_ADD_ALT2) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_ADD_FILE_NAME;
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_LIST) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_LIST_ALT) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_LIST_FILE_NAME;
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DELETE) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DELETE_ALT) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_DELETE_FILE_NAME;
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_TIMEZONE) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_TIMEZONE_ALT) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_TIMEZONE_FILE_NAME;
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_MOVE) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_MOVE_ALT) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_MOVE_FILE_NAME;
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_PIN) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_PIN_FILE_NAME;
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_NOTIFICATION) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_NOTIFICATION_FILE_NAME;
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_AUTOMATION) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_AUTOMATION_FILE_NAME;
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_RESET) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_RESET_FILE_NAME;
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_UPDATE) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_UPDATE_FILE_NAME;
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DETAILS) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DETAILS_ALT) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_DETAILS_FILE_NAME;
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_VERSION) == 0) {
+        external_plugin_name = TOTP_CLI_PLUGIN_VERSION_FILE_NAME;
+    } else {
+        totp_cli_print_unknown_command(cmd);
+    }
+
+    if(external_plugin_name != NULL) {
+        run_external_cli_plugin_handler(external_plugin_name, cli_context, args, cli);
+    }
+
+    furi_string_free(cmd);
+}
+
+TotpCliContext* totp_cli_register_command_handler(PluginState* plugin_state) {
+    Cli* cli = furi_record_open(RECORD_CLI);
+    TotpCliContext* context = malloc(sizeof(TotpCliContext));
+    furi_check(context != NULL);
+    context->plugin_state = plugin_state;
+
+    context->plugin_api_resolver = composite_api_resolver_alloc();
+    composite_api_resolver_add(context->plugin_api_resolver, firmware_api_interface);
+    composite_api_resolver_add(context->plugin_api_resolver, application_api_interface);
+
+    cli_add_command(
+        cli, TOTP_CLI_COMMAND_NAME, CliCommandFlagParallelSafe, totp_cli_handler, context);
+    furi_record_close(RECORD_CLI);
+    return context;
+}
+
+void totp_cli_unregister_command_handler(TotpCliContext* context) {
+    Cli* cli = furi_record_open(RECORD_CLI);
+    cli_delete_command(cli, TOTP_CLI_COMMAND_NAME);
+
+    composite_api_resolver_free(context->plugin_api_resolver);
+
+    furi_record_close(RECORD_CLI);
+    free(context);
+}

+ 19 - 0
totp/cli/cli.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../types/plugin_state.h"
+
+typedef struct TotpCliContext TotpCliContext;
+
+/**
+ * @brief Registers TOTP CLI handler
+ * @param plugin_state application state
+ * @return TOTP CLI context
+ */
+TotpCliContext* totp_cli_register_command_handler(PluginState* plugin_state);
+
+/**
+ * @brief Unregisters TOTP CLI handler
+ * @param context application state
+ */
+void totp_cli_unregister_command_handler(TotpCliContext* context);

+ 65 - 0
totp/cli/cli_helpers.h

@@ -0,0 +1,65 @@
+#pragma once
+
+#include <stdio.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define TOTP_CLI_COMMAND_NAME "totp"
+
+#define TOTP_CLI_COLOR_ERROR "91m"
+#define TOTP_CLI_COLOR_WARNING "93m"
+#define TOTP_CLI_COLOR_SUCCESS "92m"
+#define TOTP_CLI_COLOR_INFO "96m"
+
+#define TOTP_CLI_PRINTF(format, ...) printf(format, ##__VA_ARGS__)
+
+#define TOTP_CLI_PRINTF_COLORFUL(color, format, ...) \
+    TOTP_CLI_PRINTF("\e[%s" format "\e[0m", color, ##__VA_ARGS__)
+
+#define TOTP_CLI_PRINTF_ERROR(format, ...) \
+    TOTP_CLI_PRINTF_COLORFUL(TOTP_CLI_COLOR_ERROR, format, ##__VA_ARGS__)
+#define TOTP_CLI_PRINTF_WARNING(format, ...) \
+    TOTP_CLI_PRINTF_COLORFUL(TOTP_CLI_COLOR_WARNING, format, ##__VA_ARGS__)
+#define TOTP_CLI_PRINTF_SUCCESS(format, ...) \
+    TOTP_CLI_PRINTF_COLORFUL(TOTP_CLI_COLOR_SUCCESS, format, ##__VA_ARGS__)
+#define TOTP_CLI_PRINTF_INFO(format, ...) \
+    TOTP_CLI_PRINTF_COLORFUL(TOTP_CLI_COLOR_INFO, format, ##__VA_ARGS__)
+
+#define TOTP_CLI_LOCK_UI(plugin_state)                                  \
+    Scene __previous_scene = plugin_state->current_scene;               \
+    totp_scene_director_activate_scene(plugin_state, TotpSceneStandby); \
+    totp_scene_director_force_redraw(plugin_state)
+
+#define TOTP_CLI_UNLOCK_UI(plugin_state)                                \
+    totp_scene_director_activate_scene(plugin_state, __previous_scene); \
+    totp_scene_director_force_redraw(plugin_state)
+
+#define TOTP_CLI_PRINT_INVALID_ARGUMENTS() \
+    TOTP_CLI_PRINTF_ERROR(                 \
+        "Invalid command arguments. use \"help\" command to get list of available commands")
+
+#define TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE() \
+    TOTP_CLI_PRINTF_ERROR("An error has occurred during updating config file\r\n")
+
+#define TOTP_CLI_PRINT_ERROR_LOADING_TOKEN_INFO() \
+    TOTP_CLI_PRINTF_ERROR("An error has occurred during loading token information\r\n")
+
+#define TOTP_CLI_PRINT_PROCESSING() TOTP_CLI_PRINTF("Processing, please wait...\r\n")
+
+#define TOTP_CLI_DELETE_LAST_CHAR() \
+    TOTP_CLI_PRINTF("\b \b");       \
+    fflush(stdout)
+
+#define TOTP_CLI_DELETE_CURRENT_LINE() \
+    TOTP_CLI_PRINTF("\33[2K\r");       \
+    fflush(stdout)
+
+#define TOTP_CLI_DELETE_LAST_LINE()    \
+    TOTP_CLI_PRINTF("\033[A\33[2K\r"); \
+    fflush(stdout)
+
+#ifdef __cplusplus
+}
+#endif

+ 13 - 0
totp/cli/cli_plugin_interface.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <cli/cli.h>
+#include <furi/core/string.h>
+#include "../types/plugin_state.h"
+
+#define PLUGIN_APP_ID "totp_cli"
+#define PLUGIN_API_VERSION 1
+
+typedef struct {
+    const char* name;
+    void (*handle)(PluginState*, FuriString*, Cli*);
+} CliPlugin;

+ 95 - 0
totp/cli/cli_shared_methods.c

@@ -0,0 +1,95 @@
+#include "cli_shared_methods.h"
+#include <cli/cli.h>
+#include <lib/toolbox/args.h>
+#include "cli_helpers.h"
+#include "../types/plugin_event.h"
+
+bool totp_cli_ensure_authenticated(const PluginState* plugin_state, Cli* cli) {
+    if(plugin_state->current_scene == TotpSceneAuthentication) {
+        TOTP_CLI_PRINTF("Pleases enter PIN on your flipper device\r\n");
+
+        while((plugin_state->current_scene == TotpSceneAuthentication ||
+               plugin_state->current_scene == TotpSceneNone) &&
+              !cli_cmd_interrupt_received(cli)) {
+            furi_delay_ms(100);
+        }
+
+        TOTP_CLI_DELETE_LAST_LINE();
+
+        if(plugin_state->current_scene == TotpSceneAuthentication || //-V560
+           plugin_state->current_scene == TotpSceneNone) { //-V560
+            TOTP_CLI_PRINTF_INFO("Cancelled by user\r\n");
+            return false;
+        }
+    }
+
+    return true;
+}
+
+void totp_cli_force_close_app(FuriMessageQueue* event_queue) {
+    PluginEvent event = {.type = EventForceCloseApp};
+    furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+bool totp_cli_read_line(Cli* cli, FuriString* out_str, bool mask_user_input) {
+    uint8_t c;
+    while(cli_read(cli, &c, 1) == 1) {
+        if(c == CliSymbolAsciiEsc) {
+            // Some keys generating escape-sequences
+            // We need to ignore them as we care about alpha-numerics only
+            uint8_t c2;
+            cli_read_timeout(cli, &c2, 1, 0);
+            cli_read_timeout(cli, &c2, 1, 0);
+        } else if(c == CliSymbolAsciiETX) {
+            cli_nl();
+            return false;
+        } else if(
+            (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+            c == '/' || c == '=' || c == '+') {
+            if(mask_user_input) {
+                putc('*', stdout);
+            } else {
+                putc(c, stdout);
+            }
+            fflush(stdout);
+            furi_string_push_back(out_str, c);
+        } else if(c == CliSymbolAsciiBackspace || c == CliSymbolAsciiDel) {
+            size_t out_str_size = furi_string_size(out_str);
+            if(out_str_size > 0) {
+                TOTP_CLI_DELETE_LAST_CHAR();
+                furi_string_left(out_str, out_str_size - 1);
+            }
+        } else if(c == CliSymbolAsciiCR) {
+            cli_nl();
+            break;
+        }
+    }
+
+    return true;
+}
+
+bool args_read_uint8_and_trim(FuriString* args, uint8_t* value) {
+    int int_value;
+    if(!args_read_int_and_trim(args, &int_value) || int_value < 0 || int_value > UINT8_MAX) {
+        return false;
+    }
+
+    *value = (uint8_t)int_value;
+    return true;
+}
+
+void furi_string_secure_free(FuriString* str) {
+    for(long i = furi_string_size(str) - 1; i >= 0; i--) {
+        furi_string_set_char(str, i, '\0');
+    }
+
+    furi_string_free(str);
+}
+
+void totp_cli_printf_missed_argument_value(char* arg) {
+    TOTP_CLI_PRINTF_ERROR("Missed or incorrect value for argument \"%s\"\r\n", arg);
+}
+
+void totp_cli_printf_unknown_argument(const FuriString* arg) {
+    TOTP_CLI_PRINTF("Unknown argument \"%s\"\r\n", furi_string_get_cstr(arg));
+}

+ 62 - 0
totp/cli/cli_shared_methods.h

@@ -0,0 +1,62 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../types/plugin_state.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Checks whether user is authenticated and entered correct PIN.
+ *        If user is not authenticated it prompts user to enter correct PIN to authenticate.
+ * @param plugin_state application state
+ * @param cli pointer to the firmware CLI subsystem 
+ * @return \c true if user is already authenticated or successfully authenticated; \c false otherwise
+ */
+bool totp_cli_ensure_authenticated(const PluginState* plugin_state, Cli* cli);
+
+/**
+ * @brief Forces application to be instantly closed
+ * @param event_queue main app queue
+ */
+void totp_cli_force_close_app(FuriMessageQueue* event_queue);
+
+/**
+ * @brief Reads line of characters from console
+ * @param cli pointer to the firmware CLI subsystem 
+ * @param out_str pointer to an output string to put read line to
+ * @param mask_user_input whether to mask input characters in console or not
+ * @return \c true if line successfully read and confirmed; \c false otherwise
+ */
+bool totp_cli_read_line(Cli* cli, FuriString* out_str, bool mask_user_input);
+
+/**
+ * @brief Extracts \c uint8_t value and trims arguments string
+ * @param args arguments string
+ * @param[out] value parsed value
+ * @return \c true if value successfully read and parsed as \c uint8_t ; \c false otherwise
+ */
+bool args_read_uint8_and_trim(FuriString* args, uint8_t* value);
+
+/**
+ * @brief Free \c FuriString instance in a secure manner by clearing it first
+ * @param str instance to free
+ */
+void furi_string_secure_free(FuriString* str);
+
+/**
+ * @brief Prints information about unknown argument
+ * @param arg 
+ */
+void totp_cli_printf_unknown_argument(const FuriString* arg);
+
+/**
+ * @brief Prints information about missed required argument
+ * @param arg 
+ */
+void totp_cli_printf_missed_argument_value(char* arg);
+
+#ifdef __cplusplus
+}
+#endif

+ 173 - 0
totp/cli/plugins/automation/automation.c

@@ -0,0 +1,173 @@
+#include <flipper_application/flipper_application.h>
+#include <lib/toolbox/args.h>
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+#include "../../cli_plugin_interface.h"
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_AUTOMATION_ARG_METHOD "automation"
+#define TOTP_CLI_COMMAND_AUTOMATION_METHOD_NONE "none"
+#define TOTP_CLI_COMMAND_AUTOMATION_METHOD_USB "usb"
+#ifdef TOTP_BADBT_AUTOMATION_ENABLED
+#define TOTP_CLI_COMMAND_AUTOMATION_METHOD_BT "bt"
+#endif
+#define TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTY "QWERTY"
+#define TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_AZERTY "AZERTY"
+#define TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTZ "QWERTZ"
+#define TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT_PREFIX "-k"
+
+static void print_method(AutomationMethod method, const char* color) {
+#ifdef TOTP_BADBT_AUTOMATION_ENABLED
+    bool has_previous_method = false;
+#endif
+    if(method & AutomationMethodBadUsb) {
+        TOTP_CLI_PRINTF_COLORFUL(color, "\"" TOTP_CLI_COMMAND_AUTOMATION_METHOD_USB "\"");
+#ifdef TOTP_BADBT_AUTOMATION_ENABLED
+        has_previous_method = true;
+#endif
+    }
+
+#ifdef TOTP_BADBT_AUTOMATION_ENABLED
+    if(method & AutomationMethodBadBt) {
+        if(has_previous_method) {
+            TOTP_CLI_PRINTF_COLORFUL(color, " and ");
+        }
+
+        TOTP_CLI_PRINTF_COLORFUL(color, "\"" TOTP_CLI_COMMAND_AUTOMATION_METHOD_BT "\"");
+    }
+#endif
+
+    if(method == AutomationMethodNone) {
+        TOTP_CLI_PRINTF_COLORFUL(color, "\"" TOTP_CLI_COMMAND_AUTOMATION_METHOD_NONE "\"");
+    }
+}
+
+static void print_kb_layout(AutomationKeyboardLayout layout, const char* color) {
+    char* layoutToPrint;
+    switch(layout) {
+    case AutomationKeyboardLayoutQWERTY:
+        layoutToPrint = TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTY;
+        break;
+    case AutomationKeyboardLayoutAZERTY:
+        layoutToPrint = TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_AZERTY;
+        break;
+    case AutomationKeyboardLayoutQWERTZ:
+        layoutToPrint = TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTZ;
+        break;
+    default:
+        furi_crash("Unknown automation keyboard layout");
+        break;
+    }
+
+    TOTP_CLI_PRINTF_COLORFUL(color, "%s", layoutToPrint);
+}
+
+static bool
+    parse_automation_keyboard_layout(const FuriString* str, AutomationKeyboardLayout* out) {
+    bool result = true;
+    if(furi_string_cmpi_str(str, TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTY) == 0) {
+        *out = AutomationKeyboardLayoutQWERTY;
+    } else if(furi_string_cmpi_str(str, TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_AZERTY) == 0) {
+        *out = AutomationKeyboardLayoutAZERTY;
+    } else if(furi_string_cmpi_str(str, TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTZ) == 0) {
+        *out = AutomationKeyboardLayoutQWERTZ;
+    } else {
+        result = false;
+    }
+
+    return result;
+}
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    FuriString* temp_str = furi_string_alloc();
+    bool new_method_provided = false;
+    AutomationMethod new_method = AutomationMethodNone;
+    AutomationKeyboardLayout new_kb_layout = plugin_state->automation_kb_layout;
+    bool args_valid = true;
+    while(args_read_string_and_trim(args, temp_str)) {
+        if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_AUTOMATION_METHOD_NONE) == 0) {
+            new_method_provided = true;
+            new_method = AutomationMethodNone;
+        } else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_AUTOMATION_METHOD_USB) == 0) {
+            new_method_provided = true;
+            new_method |= AutomationMethodBadUsb;
+        }
+#ifdef TOTP_BADBT_AUTOMATION_ENABLED
+        else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_AUTOMATION_METHOD_BT) == 0) {
+            new_method_provided = true;
+            new_method |= AutomationMethodBadBt;
+        }
+#endif
+        else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT_PREFIX) == 0) {
+            if(!args_read_string_and_trim(args, temp_str) ||
+               !parse_automation_keyboard_layout(temp_str, &new_kb_layout)) {
+                args_valid = false;
+                break;
+            }
+        } else {
+            args_valid = false;
+            break;
+        }
+    }
+
+    do {
+        if(!args_valid) {
+            TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+            break;
+        }
+
+        if(new_method_provided) {
+            TOTP_CLI_LOCK_UI(plugin_state);
+
+            plugin_state->automation_method = new_method;
+            plugin_state->automation_kb_layout = new_kb_layout;
+            if(totp_config_file_update_automation_method(plugin_state)) {
+                TOTP_CLI_PRINTF_SUCCESS("Automation method is set to ");
+                print_method(new_method, TOTP_CLI_COLOR_SUCCESS);
+                TOTP_CLI_PRINTF_SUCCESS(" (");
+                print_kb_layout(plugin_state->automation_kb_layout, TOTP_CLI_COLOR_SUCCESS);
+                TOTP_CLI_PRINTF_SUCCESS(")");
+                cli_nl();
+            } else {
+                TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE();
+            }
+
+#ifdef TOTP_BADBT_AUTOMATION_ENABLED
+            if(!(new_method & AutomationMethodBadBt) &&
+               plugin_state->bt_type_code_worker_context != NULL) {
+                totp_bt_type_code_worker_free(plugin_state->bt_type_code_worker_context);
+                plugin_state->bt_type_code_worker_context = NULL;
+            }
+#endif
+
+            TOTP_CLI_UNLOCK_UI(plugin_state);
+        } else {
+            TOTP_CLI_PRINTF_INFO("Current automation method is ");
+            print_method(plugin_state->automation_method, TOTP_CLI_COLOR_INFO);
+            TOTP_CLI_PRINTF_INFO(" (");
+            print_kb_layout(plugin_state->automation_kb_layout, TOTP_CLI_COLOR_INFO);
+            TOTP_CLI_PRINTF_INFO(")");
+            cli_nl();
+        }
+    } while(false);
+
+    furi_string_free(temp_str);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Automation", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_automation_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 4 - 0
totp/cli/plugins/automation/meta.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_AUTOMATION "automation"
+#define TOTP_CLI_PLUGIN_AUTOMATION_FILE_NAME "totp_cli_automation_plugin"

+ 95 - 0
totp/cli/plugins/delete/delete.c

@@ -0,0 +1,95 @@
+#include <flipper_application/flipper_application.h>
+#include <lib/toolbox/args.h>
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+#include "../../cli_plugin_interface.h"
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+
+#define TOTP_CLI_COMMAND_DELETE_ARG_FORCE_PREFIX "-f"
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    TokenInfoIteratorContext* iterator_context =
+        totp_config_get_token_iterator_context(plugin_state);
+
+    int token_number;
+    if(!args_read_int_and_trim(args, &token_number) || token_number <= 0 ||
+       (size_t)token_number > totp_token_info_iterator_get_total_count(iterator_context)) {
+        TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+        return;
+    }
+
+    FuriString* temp_str = furi_string_alloc();
+    bool confirm_needed = true;
+    if(args_read_string_and_trim(args, temp_str)) {
+        if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_DELETE_ARG_FORCE_PREFIX) == 0) {
+            confirm_needed = false;
+        } else {
+            totp_cli_printf_unknown_argument(temp_str);
+            TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+            furi_string_free(temp_str);
+            return;
+        }
+    }
+    furi_string_free(temp_str);
+
+    TOTP_CLI_LOCK_UI(plugin_state);
+
+    size_t original_token_index =
+        totp_token_info_iterator_get_current_token_index(iterator_context);
+    totp_token_info_iterator_go_to(iterator_context, token_number - 1);
+    const TokenInfo* token_info = totp_token_info_iterator_get_current_token(iterator_context);
+    const char* token_info_name = furi_string_get_cstr(token_info->name);
+
+    bool confirmed = !confirm_needed;
+    if(confirm_needed) {
+        TOTP_CLI_PRINTF_WARNING("WARNING!\r\n");
+        TOTP_CLI_PRINTF_WARNING(
+            "TOKEN \"%s\" WILL BE PERMANENTLY DELETED WITHOUT ABILITY TO RECOVER IT.\r\n",
+            token_info_name);
+        TOTP_CLI_PRINTF_WARNING("Confirm? [y/n]\r\n");
+        fflush(stdout);
+        char user_pick;
+        do {
+            user_pick = tolower(cli_getc(cli));
+        } while(user_pick != 'y' && user_pick != 'n' && user_pick != CliSymbolAsciiCR &&
+                user_pick != CliSymbolAsciiETX && user_pick != CliSymbolAsciiEsc);
+
+        confirmed = user_pick == 'y' || user_pick == CliSymbolAsciiCR;
+    }
+
+    if(confirmed) {
+        TOTP_CLI_PRINT_PROCESSING();
+        if(totp_token_info_iterator_remove_current_token_info(iterator_context)) {
+            TOTP_CLI_DELETE_LAST_LINE();
+            TOTP_CLI_PRINTF_SUCCESS(
+                "Token \"%s\" has been successfully deleted\r\n", token_info_name);
+            totp_token_info_iterator_go_to(iterator_context, 0);
+        } else {
+            TOTP_CLI_DELETE_LAST_LINE();
+            TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE();
+            totp_token_info_iterator_go_to(iterator_context, original_token_index);
+        }
+    } else {
+        TOTP_CLI_PRINTF_INFO("User has not confirmed\r\n");
+        totp_token_info_iterator_go_to(iterator_context, original_token_index);
+    }
+
+    TOTP_CLI_UNLOCK_UI(plugin_state);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Delete", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_delete_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 5 - 0
totp/cli/plugins/delete/meta.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_DELETE "delete"
+#define TOTP_CLI_COMMAND_DELETE_ALT "rm"
+#define TOTP_CLI_PLUGIN_DELETE_FILE_NAME "totp_cli_delete_plugin"

+ 140 - 0
totp/cli/plugins/details/details.c

@@ -0,0 +1,140 @@
+#include <flipper_application/flipper_application.h>
+#include <lib/toolbox/args.h>
+#include "../../../services/config/config.h"
+#include "../../../services/config/constants.h"
+#include "../../../types/token_info.h"
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+#include "../../cli_plugin_interface.h"
+#include "../../../ui/scene_director.h"
+#include "formatters/table/details_output_formatter_table.h"
+#include "formatters/tsv/details_output_formatter_tsv.h"
+
+typedef void (*TOTP_CLI_DETAILS_HEADER_FORMATTER)();
+typedef void (*TOTP_CLI_DETAILS_FOOTER_FORMATTER)();
+typedef void (*TOTP_CLI_DETAILS_AUTOMATION_FEATURE_ITEM_FORMATTER)(
+    const char* key,
+    const char* feature,
+    bool* header_printed);
+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;
+    const TOTP_CLI_DETAILS_FOOTER_FORMATTER footer_formatter;
+    const TOTP_CLI_DETAILS_AUTOMATION_FEATURE_ITEM_FORMATTER automation_feature_item_formatter;
+    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[] = {
+    {.header_formatter = &details_output_formatter_print_header_table,
+     .footer_formatter = &details_output_formatter_print_footer_table,
+     .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,
+     .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,
+     .uint64t_formatter = &details_output_formatter_print_uint64t_tsv},
+};
+
+static void print_automation_features(
+    const TokenInfo* token_info,
+    const TotpCliDetailsFormatter* formatter) {
+    bool header_printed = false;
+    const char* AUTOMATION_FEATURES_PRINT_KEY = "Automation features";
+    if(token_info->automation_features == TokenAutomationFeatureNone) {
+        (*formatter->automation_feature_item_formatter)(
+            AUTOMATION_FEATURES_PRINT_KEY, "None", &header_printed);
+        return;
+    }
+
+    if(token_info->automation_features & TokenAutomationFeatureEnterAtTheEnd) {
+        (*formatter->automation_feature_item_formatter)(
+            AUTOMATION_FEATURES_PRINT_KEY, "Type <Enter> key at the end", &header_printed);
+    }
+
+    if(token_info->automation_features & TokenAutomationFeatureTabAtTheEnd) {
+        (*formatter->automation_feature_item_formatter)(
+            AUTOMATION_FEATURES_PRINT_KEY, "Type <Tab> key at the end", &header_printed);
+    }
+
+    if(token_info->automation_features & TokenAutomationFeatureTypeSlower) {
+        (*formatter->automation_feature_item_formatter)(
+            AUTOMATION_FEATURES_PRINT_KEY, "Type slower", &header_printed);
+    }
+}
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    int token_number;
+    TokenInfoIteratorContext* iterator_context =
+        totp_config_get_token_iterator_context(plugin_state);
+    if(!args_read_int_and_trim(args, &token_number) || token_number <= 0 ||
+       (size_t)token_number > totp_token_info_iterator_get_total_count(iterator_context)) {
+        TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+        return;
+    }
+
+    const TotpCliDetailsFormatter* formatter = &available_formatters[0];
+    FuriString* arg = furi_string_alloc();
+    if(args_read_string_and_trim(args, arg) && furi_string_cmpi_str(arg, "--tsv") == 0) {
+        formatter = &available_formatters[1];
+    }
+
+    furi_string_free(arg);
+
+    TOTP_CLI_LOCK_UI(plugin_state);
+
+    size_t original_token_index =
+        totp_token_info_iterator_get_current_token_index(iterator_context);
+    if(totp_token_info_iterator_go_to(iterator_context, token_number - 1)) {
+        const TokenInfo* token_info = totp_token_info_iterator_get_current_token(iterator_context);
+
+        (*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);
+        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 {
+        TOTP_CLI_PRINT_ERROR_LOADING_TOKEN_INFO();
+    }
+
+    totp_token_info_iterator_go_to(iterator_context, original_token_index);
+
+    TOTP_CLI_UNLOCK_UI(plugin_state);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Details", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_details_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 37 - 0
totp/cli/plugins/details/formatters/table/details_output_formatter_table.c

@@ -0,0 +1,37 @@
+#include "details_output_formatter_table.h"
+#include <inttypes.h>
+#include "../../../../cli_helpers.h"
+
+void details_output_formatter_print_header_table() {
+    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");
+}
+
+void details_output_formatter_print_automation_feature_table(
+    const char* key,
+    const char* feature,
+    bool* header_printed) {
+    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 | %-29.29s |\r\n", key, value);
+}
+
+void details_output_formatter_print_uint8t_table(const char* key, uint8_t 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 | %-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);
+}

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

@@ -0,0 +1,16 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+void details_output_formatter_print_header_table();
+void details_output_formatter_print_footer_table();
+void details_output_formatter_print_automation_feature_table(
+    const char* key,
+    const char* feature,
+    bool* header_printed);
+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);

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

@@ -0,0 +1,34 @@
+#include "details_output_formatter_tsv.h"
+#include <inttypes.h>
+#include "../../../../cli_helpers.h"
+
+void details_output_formatter_print_header_tsv() {
+    TOTP_CLI_PRINTF("%s\t%s\r\n", "Property", "Value");
+}
+
+void details_output_formatter_print_footer_tsv() {
+}
+
+void details_output_formatter_print_automation_feature_tsv(
+    const char* key,
+    const char* feature,
+    bool* header_printed) {
+    TOTP_CLI_PRINTF("%s\t%s\r\n", *header_printed ? "" : key, feature);
+    *header_printed = true;
+}
+
+void details_output_formatter_print_cstr_tsv(const char* key, const char* value) {
+    TOTP_CLI_PRINTF("%s\t%s\r\n", key, value);
+}
+
+void details_output_formatter_print_uint8t_tsv(const char* key, uint8_t value) {
+    TOTP_CLI_PRINTF("%s\t%" PRIu8 "\r\n", key, 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);
+}

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

@@ -0,0 +1,16 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+void details_output_formatter_print_header_tsv();
+void details_output_formatter_print_footer_tsv();
+void details_output_formatter_print_automation_feature_tsv(
+    const char* key,
+    const char* feature,
+    bool* header_printed);
+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);

+ 5 - 0
totp/cli/plugins/details/meta.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_DETAILS "lsattr"
+#define TOTP_CLI_COMMAND_DETAILS_ALT "cat"
+#define TOTP_CLI_PLUGIN_DETAILS_FILE_NAME "totp_cli_details_plugin"

+ 42 - 0
totp/cli/plugins/help/help.c

@@ -0,0 +1,42 @@
+#include <flipper_application/flipper_application.h>
+#include <storage/storage.h>
+#include <toolbox/stream/stream.h>
+#include <toolbox/stream/file_stream.h>
+#include "../../cli_helpers.h"
+#include "../../cli_plugin_interface.h"
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    UNUSED(args);
+    UNUSED(cli);
+    UNUSED(plugin_state);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    Stream* stream = file_stream_alloc(storage);
+
+    if(file_stream_open(
+           stream, EXT_PATH("apps_assets/totp/cli/cli_help.txt"), FSAM_READ, FSOM_OPEN_EXISTING)) {
+        uint8_t buffer[32U];
+        size_t bytes_read;
+        while((bytes_read = stream_read(stream, &buffer[0], sizeof(buffer))) > 0) {
+            cli_write(cli, &buffer[0], bytes_read);
+        }
+    }
+
+    file_stream_close(stream);
+    stream_free(stream);
+
+    furi_record_close(RECORD_STORAGE);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Help", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_help_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 6 - 0
totp/cli/plugins/help/meta.h

@@ -0,0 +1,6 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_HELP "help"
+#define TOTP_CLI_COMMAND_HELP_ALT "h"
+#define TOTP_CLI_COMMAND_HELP_ALT2 "?"
+#define TOTP_CLI_PLUGIN_HELP_FILE_NAME "totp_cli_help_plugin"

+ 23 - 0
totp/cli/plugins/list/formatters/table/list_output_formatter_table.c

@@ -0,0 +1,23 @@
+#include "list_output_formatter_table.h"
+#include <inttypes.h>
+#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", "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 " | %-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_get_type_as_cstr(token_info));
+}
+
+void list_output_formatter_print_footer_table() {
+    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+------+\r\n");
+}

+ 9 - 0
totp/cli/plugins/list/formatters/table/list_output_formatter_table.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "../../../../../types/token_info.h"
+
+void list_output_formatter_print_header_table();
+
+void list_output_formatter_print_body_item_table(size_t index, const TokenInfo* token_info);
+
+void list_output_formatter_print_footer_table();

+ 20 - 0
totp/cli/plugins/list/formatters/tsv/list_output_formatter_tsv.c

@@ -0,0 +1,20 @@
+#include "list_output_formatter_tsv.h"
+#include <inttypes.h>
+#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", "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%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_get_type_as_cstr(token_info));
+}
+
+void list_output_formatter_print_footer_tsv() {
+}

+ 9 - 0
totp/cli/plugins/list/formatters/tsv/list_output_formatter_tsv.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "../../../../../types/token_info.h"
+
+void list_output_formatter_print_header_tsv();
+
+void list_output_formatter_print_body_item_tsv(size_t index, const TokenInfo* token_info);
+
+void list_output_formatter_print_footer_tsv();

+ 82 - 0
totp/cli/plugins/list/list.c

@@ -0,0 +1,82 @@
+#include <flipper_application/flipper_application.h>
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+#include "../../cli_plugin_interface.h"
+
+#include <lib/toolbox/args.h>
+#include "../../../types/token_info.h"
+#include "../../../services/config/constants.h"
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+#include "formatters/table/list_output_formatter_table.h"
+#include "formatters/tsv/list_output_formatter_tsv.h"
+
+typedef void (*TOTP_CLI_LIST_HEADER_FORMATTER)();
+typedef void (*TOTP_CLI_LIST_FOOTER_FORMATTER)();
+typedef void (*TOTP_CLI_LIST_BODY_ITEM_FORMATTER)(size_t index, const TokenInfo* token_info);
+
+typedef struct {
+    const TOTP_CLI_LIST_HEADER_FORMATTER header_formatter;
+    const TOTP_CLI_LIST_FOOTER_FORMATTER footer_formatter;
+    const TOTP_CLI_LIST_BODY_ITEM_FORMATTER body_item_formatter;
+} TotpCliListFormatter;
+
+static const TotpCliListFormatter available_formatters[] = {
+    {.header_formatter = &list_output_formatter_print_header_table,
+     .body_item_formatter = &list_output_formatter_print_body_item_table,
+     .footer_formatter = &list_output_formatter_print_footer_table},
+
+    {.header_formatter = &list_output_formatter_print_header_tsv,
+     .body_item_formatter = &list_output_formatter_print_body_item_tsv,
+     .footer_formatter = &list_output_formatter_print_footer_tsv}};
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    TokenInfoIteratorContext* iterator_context =
+        totp_config_get_token_iterator_context(plugin_state);
+    size_t total_count = totp_token_info_iterator_get_total_count(iterator_context);
+    if(total_count <= 0) {
+        TOTP_CLI_PRINTF("There are no tokens");
+        return;
+    }
+
+    const TotpCliListFormatter* formatter = &available_formatters[0];
+    FuriString* arg = furi_string_alloc();
+    if(args_read_string_and_trim(args, arg) && furi_string_cmpi_str(arg, "--tsv") == 0) {
+        formatter = &available_formatters[1];
+    }
+
+    furi_string_free(arg);
+
+    TOTP_CLI_LOCK_UI(plugin_state);
+
+    size_t original_index = totp_token_info_iterator_get_current_token_index(iterator_context);
+
+    (*formatter->header_formatter)();
+    for(size_t i = 0; i < total_count; i++) {
+        totp_token_info_iterator_go_to(iterator_context, i);
+        const TokenInfo* token_info = totp_token_info_iterator_get_current_token(iterator_context);
+        (*formatter->body_item_formatter)(i, token_info);
+    }
+
+    (*formatter->footer_formatter)();
+
+    totp_token_info_iterator_go_to(iterator_context, original_index);
+
+    TOTP_CLI_UNLOCK_UI(plugin_state);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: List", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_list_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 5 - 0
totp/cli/plugins/list/meta.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_LIST "list"
+#define TOTP_CLI_COMMAND_LIST_ALT "ls"
+#define TOTP_CLI_PLUGIN_LIST_FILE_NAME "totp_cli_list_plugin"

+ 128 - 0
totp/cli/plugins/modify/add/add.c

@@ -0,0 +1,128 @@
+#include <flipper_application/flipper_application.h>
+#include <lib/toolbox/args.h>
+#include "../../../cli_helpers.h"
+#include "../../../cli_shared_methods.h"
+#include "../../../cli_plugin_interface.h"
+#include "../../../../types/token_info.h"
+#include "../../../../services/config/config.h"
+#include "../../../../services/convert/convert.h"
+#include "../../../../ui/scene_director.h"
+#include "../common.h"
+
+struct TotpAddContext {
+    FuriString* args;
+    Cli* cli;
+    const CryptoSettings* crypto_settings;
+};
+
+enum TotpIteratorUpdateTokenResultsEx {
+    TotpIteratorUpdateTokenResultInvalidSecret = 1,
+    TotpIteratorUpdateTokenResultCancelled = 2,
+    TotpIteratorUpdateTokenResultInvalidArguments = 3
+};
+
+static TotpIteratorUpdateTokenResult
+    add_token_handler(TokenInfo* token_info, const void* context) {
+    const struct TotpAddContext* context_t = context;
+
+    // Reading token name
+    if(!args_read_probably_quoted_string_and_trim(context_t->args, token_info->name)) {
+        return TotpIteratorUpdateTokenResultInvalidArguments;
+    }
+
+    FuriString* temp_str = furi_string_alloc();
+
+    // Read optional arguments
+    bool mask_user_input = true;
+    PlainTokenSecretEncoding token_secret_encoding = PlainTokenSecretEncodingBase32;
+    while(args_read_string_and_trim(context_t->args, temp_str)) {
+        bool parsed = false;
+        if(!totp_cli_try_read_algo(token_info, temp_str, context_t->args, &parsed) &&
+           !totp_cli_try_read_digits(token_info, temp_str, context_t->args, &parsed) &&
+           !totp_cli_try_read_duration(token_info, temp_str, context_t->args, &parsed) &&
+           !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) &&
+           !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);
+        }
+
+        if(!parsed) {
+            furi_string_free(temp_str);
+            return TotpIteratorUpdateTokenResultInvalidArguments;
+        }
+    }
+
+    // Reading token secret
+    furi_string_reset(temp_str);
+    TOTP_CLI_PRINTF("Enter token secret and confirm with [ENTER]:\r\n");
+    if(!totp_cli_read_line(context_t->cli, temp_str, mask_user_input)) {
+        TOTP_CLI_DELETE_LAST_LINE();
+        furi_string_secure_free(temp_str);
+        return TotpIteratorUpdateTokenResultCancelled;
+    }
+
+    TOTP_CLI_DELETE_LAST_LINE();
+
+    bool secret_set = token_info_set_secret(
+        token_info,
+        furi_string_get_cstr(temp_str),
+        furi_string_size(temp_str),
+        token_secret_encoding,
+        context_t->crypto_settings);
+
+    furi_string_secure_free(temp_str);
+
+    if(!secret_set) {
+        return TotpIteratorUpdateTokenResultInvalidSecret;
+    }
+
+    return TotpIteratorUpdateTokenResultSuccess;
+}
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    TokenInfoIteratorContext* iterator_context =
+        totp_config_get_token_iterator_context(plugin_state);
+
+    TOTP_CLI_LOCK_UI(plugin_state);
+
+    struct TotpAddContext add_context = {
+        .args = args, .cli = cli, .crypto_settings = &plugin_state->crypto_settings};
+    TotpIteratorUpdateTokenResult add_result =
+        totp_token_info_iterator_add_new_token(iterator_context, &add_token_handler, &add_context);
+
+    if(add_result == TotpIteratorUpdateTokenResultSuccess) {
+        TOTP_CLI_PRINTF_SUCCESS(
+            "Token \"%s\" has been successfully added\r\n",
+            furi_string_get_cstr(
+                totp_token_info_iterator_get_current_token(iterator_context)->name));
+    } else if(add_result == TotpIteratorUpdateTokenResultCancelled) {
+        TOTP_CLI_PRINTF_INFO("Cancelled by user\r\n");
+    } else if(add_result == TotpIteratorUpdateTokenResultInvalidArguments) {
+        TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+    } else if(add_result == TotpIteratorUpdateTokenResultInvalidSecret) {
+        TOTP_CLI_PRINTF_ERROR("Token secret seems to be invalid and can not be parsed\r\n");
+    } else if(add_result == TotpIteratorUpdateTokenResultFileUpdateFailed) {
+        TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE();
+    }
+
+    TOTP_CLI_UNLOCK_UI(plugin_state);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Add", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_add_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 6 - 0
totp/cli/plugins/modify/add/meta.h

@@ -0,0 +1,6 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_ADD "add"
+#define TOTP_CLI_COMMAND_ADD_ALT "mk"
+#define TOTP_CLI_COMMAND_ADD_ALT2 "new"
+#define TOTP_CLI_PLUGIN_ADD_FILE_NAME "totp_cli_add_plugin"

+ 182 - 0
totp/cli/plugins/modify/common.c

@@ -0,0 +1,182 @@
+#include "common.h"
+#include <lib/toolbox/args.h>
+#include "stdint.h"
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+
+bool totp_cli_try_read_algo(TokenInfo* token_info, FuriString* arg, FuriString* args, bool* parsed) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_ALGO_PREFIX) == 0) {
+        if(!args_read_string_and_trim(args, arg)) {
+            totp_cli_printf_missed_argument_value(TOTP_CLI_COMMAND_ARG_ALGO_PREFIX);
+        } else if(!token_info_set_algo_from_str(token_info, arg)) {
+            TOTP_CLI_PRINTF_ERROR(
+                "\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ARG_ALGO_PREFIX
+                "\"\r\n",
+                furi_string_get_cstr(arg));
+        } else {
+            *parsed = true;
+        }
+
+        return true;
+    }
+
+    return false;
+}
+
+bool totp_cli_try_read_digits(
+    TokenInfo* token_info,
+    const FuriString* arg,
+    FuriString* args,
+    bool* parsed) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX) == 0) {
+        uint8_t digit_value;
+        if(!args_read_uint8_and_trim(args, &digit_value)) {
+            totp_cli_printf_missed_argument_value(TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX);
+        } else if(!token_info_set_digits_from_int(token_info, digit_value)) {
+            TOTP_CLI_PRINTF_ERROR(
+                "\"%" PRIu8
+                "\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX
+                "\"\r\n",
+                digit_value);
+        } else {
+            *parsed = true;
+        }
+
+        return true;
+    }
+
+    return false;
+}
+
+bool totp_cli_try_read_duration(
+    TokenInfo* token_info,
+    const FuriString* arg,
+    FuriString* args,
+    bool* parsed) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_DURATION_PREFIX) == 0) {
+        uint8_t duration_value;
+        if(!args_read_uint8_and_trim(args, &duration_value)) {
+            totp_cli_printf_missed_argument_value(TOTP_CLI_COMMAND_ARG_DURATION_PREFIX);
+        } else if(!token_info_set_duration_from_int(token_info, duration_value)) {
+            TOTP_CLI_PRINTF_ERROR(
+                "\"%" PRIu8
+                "\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ARG_DURATION_PREFIX
+                "\"\r\n",
+                duration_value);
+        } else {
+            *parsed = true;
+        }
+
+        return true;
+    }
+
+    return false;
+}
+
+bool totp_cli_try_read_automation_features(
+    TokenInfo* token_info,
+    FuriString* arg,
+    FuriString* args,
+    bool* parsed) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX) == 0) {
+        if(!args_read_string_and_trim(args, arg)) {
+            totp_cli_printf_missed_argument_value(TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX);
+        } else if(!token_info_set_automation_feature_from_str(token_info, arg)) {
+            TOTP_CLI_PRINTF_ERROR(
+                "\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX
+                "\"\r\n",
+                furi_string_get_cstr(arg));
+        } else {
+            *parsed = true;
+        }
+
+        return true;
+    }
+
+    return false;
+}
+
+bool totp_cli_try_read_unsecure_flag(const FuriString* arg, bool* parsed, bool* unsecure_flag) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_UNSECURE_PREFIX) == 0) {
+        *unsecure_flag = false;
+        *parsed = true;
+        return true;
+    }
+
+    return false;
+}
+
+bool totp_cli_try_read_plain_token_secret_encoding(
+    FuriString* arg,
+    FuriString* args,
+    bool* parsed,
+    PlainTokenSecretEncoding* secret_encoding) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX) == 0) {
+        if(!args_read_string_and_trim(args, arg)) {
+            totp_cli_printf_missed_argument_value(TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX);
+        } else {
+            if(furi_string_cmpi_str(arg, PLAIN_TOKEN_ENCODING_BASE32_NAME) == 0) {
+                *secret_encoding = PlainTokenSecretEncodingBase32;
+                *parsed = true;
+            } else if(furi_string_cmpi_str(arg, PLAIN_TOKEN_ENCODING_BASE64_NAME) == 0) {
+                *secret_encoding = PlainTokenSecretEncodingBase64;
+                *parsed = true;
+            } else {
+                TOTP_CLI_PRINTF_ERROR(
+                    "\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX
+                    "\"\r\n",
+                    furi_string_get_cstr(arg));
+            }
+        }
+
+        return true;
+    }
+
+    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;
+}

+ 124 - 0
totp/cli/plugins/modify/common.h

@@ -0,0 +1,124 @@
+#pragma once
+#include <stdlib.h>
+#include "../../../types/token_info.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define TOTP_CLI_COMMAND_ARG_NAME_PREFIX "-n"
+#define TOTP_CLI_COMMAND_ARG_ALGO_PREFIX "-a"
+#define TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX "-d"
+#define TOTP_CLI_COMMAND_ARG_UNSECURE_PREFIX "-u"
+#define TOTP_CLI_COMMAND_ARG_DURATION_PREFIX "-l"
+#define TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX "-b"
+#define TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX "-e"
+#define TOTP_CLI_COMMAND_ARG_TYPE_PREFIX "-t"
+#define TOTP_CLI_COMMAND_ARG_COUNTER_PREFIX "-i"
+
+/**
+ * @brief Tries to read token hashing algo
+ * @param token_info token info to set parsed algo to if successfully read and parsed
+ * @param arg argument to parse
+ * @param args rest of arguments
+ * @param[out] parsed will be set to \c true if token hashing algo sucecssfully read and parsed; \c false otherwise
+ * @return \c true if \c arg represents token hashing algo argument; \c false otherwise
+ */
+bool totp_cli_try_read_algo(TokenInfo* token_info, FuriString* arg, FuriString* args, bool* parsed);
+
+/**
+ * @brief Tries to read token digits count
+ * @param token_info token info to set parsed digits count to if successfully read and parsed
+ * @param arg argument to parse
+ * @param args rest of arguments
+ * @param[out] parsed will be set to \c true if token digits count sucecssfully read and parsed; \c false otherwise
+ * @return \c true if \c arg represents token digits count argument; \c false otherwise
+ */
+bool totp_cli_try_read_digits(
+    TokenInfo* token_info,
+    const FuriString* arg,
+    FuriString* args,
+    bool* parsed);
+
+/**
+ * @brief Tries to read token duration
+ * @param token_info token info to set parsed duration to if successfully read and parsed
+ * @param arg argument to parse
+ * @param args rest of arguments
+ * @param[out] parsed will be set to \c true if token duration sucecssfully read and parsed; \c false otherwise
+ * @return \c true if \c arg represents token duration argument; \c false otherwise
+ */
+bool totp_cli_try_read_duration(
+    TokenInfo* token_info,
+    const FuriString* arg,
+    FuriString* args,
+    bool* parsed);
+
+/**
+ * @brief Tries to read token automation features
+ * @param token_info token info to set parsed automation features to if successfully read and parsed
+ * @param arg argument to parse
+ * @param args rest of arguments
+ * @param[out] parsed will be set to \c true if token automation features sucecssfully read and parsed; \c false otherwise
+ * @return \c true if \c arg represents token automation features argument; \c false otherwise
+ */
+bool totp_cli_try_read_automation_features(
+    TokenInfo* token_info,
+    FuriString* arg,
+    FuriString* args,
+    bool* parsed);
+
+/**
+ * @brief Tries to read unsecure flag
+ * @param arg argument to parse
+ * @param[out] parsed will be set to \c true if unsecure flag sucecssfully read and parsed; \c false otherwise
+ * @param[out] unsecure_flag will be set to parsed unsecure flag state if read and parsed successfully
+ * @return \c true if \c arg represents unsecure flag argument; \c false otherwise
+ */
+bool totp_cli_try_read_unsecure_flag(const FuriString* arg, bool* parsed, bool* unsecure_flag);
+
+/**
+ * @brief Tries to read plain token secret encoding
+ * @param arg argument to parse
+ * @param args rest of arguments
+ * @param[out] parsed will be set to \c true if plain token secret encoding sucecssfully read and parsed; \c false otherwise
+ * @param[out] secret_encoding will be set to parsed plain token secret encoding if read and parsed successfully
+ * @return \c true if \c arg represents plain token secret encoding argument; \c false otherwise
+ */
+bool totp_cli_try_read_plain_token_secret_encoding(
+    FuriString* arg,
+    FuriString* args,
+    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

+ 4 - 0
totp/cli/plugins/modify/update/meta.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_UPDATE "update"
+#define TOTP_CLI_PLUGIN_UPDATE_FILE_NAME "totp_cli_update_plugin"

+ 167 - 0
totp/cli/plugins/modify/update/update.c

@@ -0,0 +1,167 @@
+#include <flipper_application/flipper_application.h>
+#include <lib/toolbox/args.h>
+#include "../../../cli_helpers.h"
+#include "../../../cli_shared_methods.h"
+#include "../../../cli_plugin_interface.h"
+#include "../../../../types/token_info.h"
+#include "../../../../services/config/config.h"
+#include "../../../../services/convert/convert.h"
+#include "../../../../ui/scene_director.h"
+#include "../common.h"
+
+#define TOTP_CLI_COMMAND_UPDATE_ARG_SECRET_PREFIX "-s"
+
+struct TotpUpdateContext {
+    FuriString* args;
+    Cli* cli;
+    const CryptoSettings* crypto_settings;
+};
+
+enum TotpIteratorUpdateTokenResultsEx {
+    TotpIteratorUpdateTokenResultInvalidSecret = 1,
+    TotpIteratorUpdateTokenResultCancelled = 2,
+    TotpIteratorUpdateTokenResultInvalidArguments = 3
+};
+
+static bool totp_cli_try_read_name(
+    TokenInfo* token_info,
+    const FuriString* arg,
+    FuriString* args,
+    bool* parsed) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_ARG_NAME_PREFIX) == 0) {
+        if(!args_read_probably_quoted_string_and_trim(args, token_info->name) ||
+           furi_string_empty(token_info->name)) {
+            totp_cli_printf_missed_argument_value(TOTP_CLI_COMMAND_ARG_NAME_PREFIX);
+        } else {
+            *parsed = true;
+        }
+
+        return true;
+    }
+
+    return false;
+}
+
+static bool totp_cli_try_read_change_secret_flag(const FuriString* arg, bool* parsed, bool* flag) {
+    if(furi_string_cmpi_str(arg, TOTP_CLI_COMMAND_UPDATE_ARG_SECRET_PREFIX) == 0) {
+        *flag = true;
+        *parsed = true;
+        return true;
+    }
+
+    return false;
+}
+
+static TotpIteratorUpdateTokenResult
+    update_token_handler(TokenInfo* token_info, const void* context) {
+    const struct TotpUpdateContext* context_t = context;
+
+    // Read optional arguments
+    FuriString* temp_str = furi_string_alloc();
+    bool mask_user_input = true;
+    bool update_token_secret = false;
+    PlainTokenSecretEncoding token_secret_encoding = PlainTokenSecretEncodingBase32;
+    while(args_read_string_and_trim(context_t->args, temp_str)) {
+        bool parsed = false;
+        if(!totp_cli_try_read_name(token_info, temp_str, context_t->args, &parsed) &&
+           !totp_cli_try_read_algo(token_info, temp_str, context_t->args, &parsed) &&
+           !totp_cli_try_read_digits(token_info, temp_str, context_t->args, &parsed) &&
+           !totp_cli_try_read_duration(token_info, temp_str, context_t->args, &parsed) &&
+           !totp_cli_try_read_unsecure_flag(temp_str, &parsed, &mask_user_input) &&
+           !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) &&
+           !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);
+        }
+
+        if(!parsed) {
+            furi_string_free(temp_str);
+            return TotpIteratorUpdateTokenResultInvalidArguments;
+        }
+    }
+
+    if(update_token_secret) {
+        // Reading token secret
+        furi_string_reset(temp_str);
+        TOTP_CLI_PRINTF("Enter token secret and confirm with [ENTER]:\r\n");
+        bool token_secret_read = totp_cli_read_line(context_t->cli, temp_str, mask_user_input);
+        TOTP_CLI_DELETE_LAST_LINE();
+        if(!token_secret_read) {
+            furi_string_secure_free(temp_str);
+            return TotpIteratorUpdateTokenResultCancelled;
+        }
+
+        if(!token_info_set_secret(
+               token_info,
+               furi_string_get_cstr(temp_str),
+               furi_string_size(temp_str),
+               token_secret_encoding,
+               context_t->crypto_settings)) {
+            furi_string_secure_free(temp_str);
+            return TotpIteratorUpdateTokenResultInvalidSecret;
+        }
+    }
+
+    furi_string_secure_free(temp_str);
+
+    return TotpIteratorUpdateTokenResultSuccess;
+}
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    TokenInfoIteratorContext* iterator_context =
+        totp_config_get_token_iterator_context(plugin_state);
+
+    int token_number;
+    if(!args_read_int_and_trim(args, &token_number) || token_number <= 0 ||
+       (size_t)token_number > totp_token_info_iterator_get_total_count(iterator_context)) {
+        TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+        return;
+    }
+
+    TOTP_CLI_LOCK_UI(plugin_state);
+
+    size_t previous_index = totp_token_info_iterator_get_current_token_index(iterator_context);
+    totp_token_info_iterator_go_to(iterator_context, token_number - 1);
+
+    struct TotpUpdateContext update_context = {
+        .args = args, .cli = cli, .crypto_settings = &plugin_state->crypto_settings};
+    TotpIteratorUpdateTokenResult update_result = totp_token_info_iterator_update_current_token(
+        iterator_context, &update_token_handler, &update_context);
+
+    if(update_result == TotpIteratorUpdateTokenResultSuccess) {
+        TOTP_CLI_PRINTF_SUCCESS(
+            "Token \"%s\" has been successfully updated\r\n",
+            furi_string_get_cstr(
+                totp_token_info_iterator_get_current_token(iterator_context)->name));
+    } else if(update_result == TotpIteratorUpdateTokenResultInvalidArguments) {
+        TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+    } else if(update_result == TotpIteratorUpdateTokenResultCancelled) {
+        TOTP_CLI_PRINTF_INFO("Cancelled by user\r\n");
+    } else if(update_result == TotpIteratorUpdateTokenResultInvalidSecret) {
+        TOTP_CLI_PRINTF_ERROR("Token secret seems to be invalid and can not be parsed\r\n");
+    } else if(update_result == TotpIteratorUpdateTokenResultFileUpdateFailed) {
+        TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE();
+    }
+
+    totp_token_info_iterator_go_to(iterator_context, previous_index);
+    TOTP_CLI_UNLOCK_UI(plugin_state);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Update", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_update_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 5 - 0
totp/cli/plugins/move/meta.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_MOVE "move"
+#define TOTP_CLI_COMMAND_MOVE_ALT "mv"
+#define TOTP_CLI_PLUGIN_MOVE_FILE_NAME "totp_cli_move_plugin"

+ 75 - 0
totp/cli/plugins/move/move.c

@@ -0,0 +1,75 @@
+#include <flipper_application/flipper_application.h>
+#include <lib/toolbox/args.h>
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+#include "../../cli_plugin_interface.h"
+#include "../../../types/token_info.h"
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    int token_number;
+    TokenInfoIteratorContext* iterator_context =
+        totp_config_get_token_iterator_context(plugin_state);
+    size_t total_count = totp_token_info_iterator_get_total_count(iterator_context);
+    if(!args_read_int_and_trim(args, &token_number) || token_number < 1 ||
+       (size_t)token_number > total_count) {
+        TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+        return;
+    }
+
+    int new_token_number = 0;
+
+    if(!args_read_int_and_trim(args, &new_token_number) || new_token_number < 1 ||
+       (size_t)new_token_number > total_count) {
+        TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+        return;
+    }
+
+    if(token_number == new_token_number) {
+        TOTP_CLI_PRINTF_ERROR("New token number matches current token number\r\n");
+        return;
+    }
+
+    TOTP_CLI_LOCK_UI(plugin_state);
+
+    size_t token_index = token_number - 1;
+    size_t new_token_index = new_token_number - 1;
+
+    size_t original_token_index =
+        totp_token_info_iterator_get_current_token_index(iterator_context);
+
+    TOTP_CLI_PRINT_PROCESSING();
+
+    if(totp_token_info_iterator_go_to(iterator_context, token_index) &&
+       totp_token_info_iterator_move_current_token_info(iterator_context, new_token_index)) {
+        TOTP_CLI_DELETE_LAST_LINE();
+        TOTP_CLI_PRINTF_SUCCESS(
+            "Token \"%s\" has been successfully updated\r\n",
+            furi_string_get_cstr(
+                totp_token_info_iterator_get_current_token(iterator_context)->name));
+    } else {
+        TOTP_CLI_DELETE_LAST_LINE();
+        TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE();
+    }
+
+    totp_token_info_iterator_go_to(iterator_context, original_token_index);
+
+    TOTP_CLI_UNLOCK_UI(plugin_state);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Move", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_move_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 4 - 0
totp/cli/plugins/notification/meta.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_NOTIFICATION "notify"
+#define TOTP_CLI_PLUGIN_NOTIFICATION_FILE_NAME "totp_cli_notification_plugin"

+ 97 - 0
totp/cli/plugins/notification/notification.c

@@ -0,0 +1,97 @@
+#include <flipper_application/flipper_application.h>
+#include <lib/toolbox/args.h>
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+#include "../../cli_plugin_interface.h"
+
+#define TOTP_CLI_COMMAND_NOTIFICATION_METHOD_NONE "none"
+#define TOTP_CLI_COMMAND_NOTIFICATION_METHOD_SOUND "sound"
+#define TOTP_CLI_COMMAND_NOTIFICATION_METHOD_VIBRO "vibro"
+
+static void
+    totp_cli_command_notification_print_method(NotificationMethod method, const char* color) {
+    bool has_previous_method = false;
+    if(method & NotificationMethodSound) {
+        TOTP_CLI_PRINTF_COLORFUL(color, "\"" TOTP_CLI_COMMAND_NOTIFICATION_METHOD_SOUND "\"");
+        has_previous_method = true;
+    }
+    if(method & NotificationMethodVibro) {
+        if(has_previous_method) {
+            TOTP_CLI_PRINTF_COLORFUL(color, " and ");
+        }
+
+        TOTP_CLI_PRINTF_COLORFUL(color, "\"" TOTP_CLI_COMMAND_NOTIFICATION_METHOD_VIBRO "\"");
+    }
+    if(method == NotificationMethodNone) {
+        TOTP_CLI_PRINTF_COLORFUL(color, "\"" TOTP_CLI_COMMAND_NOTIFICATION_METHOD_NONE "\"");
+    }
+}
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    FuriString* temp_str = furi_string_alloc();
+    bool new_method_provided = false;
+    NotificationMethod new_method = NotificationMethodNone;
+    bool args_valid = true;
+    while(args_read_string_and_trim(args, temp_str)) {
+        if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_NOTIFICATION_METHOD_NONE) == 0) {
+            new_method_provided = true;
+            new_method = NotificationMethodNone;
+        } else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_NOTIFICATION_METHOD_SOUND) == 0) {
+            new_method_provided = true;
+            new_method |= NotificationMethodSound;
+        } else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_NOTIFICATION_METHOD_VIBRO) == 0) {
+            new_method_provided = true;
+            new_method |= NotificationMethodVibro;
+        } else {
+            args_valid = false;
+            break;
+        }
+    }
+
+    do {
+        if(!args_valid) {
+            TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+            break;
+        }
+
+        if(new_method_provided) {
+            TOTP_CLI_LOCK_UI(plugin_state);
+
+            plugin_state->notification_method = new_method;
+            if(totp_config_file_update_notification_method(plugin_state)) {
+                TOTP_CLI_PRINTF_SUCCESS("Notification method is set to ");
+                totp_cli_command_notification_print_method(new_method, TOTP_CLI_COLOR_SUCCESS);
+                cli_nl();
+            } else {
+                TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE();
+            }
+
+            TOTP_CLI_UNLOCK_UI(plugin_state);
+        } else {
+            TOTP_CLI_PRINTF_INFO("Current notification method is ");
+            totp_cli_command_notification_print_method(
+                plugin_state->notification_method, TOTP_CLI_COLOR_INFO);
+            cli_nl();
+        }
+    } while(false);
+
+    furi_string_free(temp_str);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Notification", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_notification_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 4 - 0
totp/cli/plugins/pin/meta.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_PIN "pin"
+#define TOTP_CLI_PLUGIN_PIN_FILE_NAME "totp_cli_pin_plugin"

+ 179 - 0
totp/cli/plugins/pin/pin.c

@@ -0,0 +1,179 @@
+#include <flipper_application/flipper_application.h>
+#include <lib/toolbox/args.h>
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+#include "../../cli_plugin_interface.h"
+#include "../../../types/token_info.h"
+#include "../../../types/user_pin_codes.h"
+#include "../../../services/config/config.h"
+#include "../../../services/crypto/crypto_facade.h"
+#include "../../../ui/scene_director.h"
+#include "../../../lib/polyfills/memset_s.h"
+
+#define TOTP_CLI_COMMAND_PIN_COMMAND_SET "set"
+#define TOTP_CLI_COMMAND_PIN_COMMAND_REMOVE "remove"
+#define TOTP_CLI_COMMAND_PIN_ARG_NEW_CRYPTO_KEY_SLOT_PREFIX "-c"
+
+static inline uint8_t totp_cli_key_to_pin_code(uint8_t key) {
+    uint8_t code = 0;
+    switch(key) {
+    case 0x44: // left
+        code = PinCodeArrowLeft;
+        break;
+    case 0x41: // up
+        code = PinCodeArrowUp;
+        break;
+    case 0x43: // right
+        code = PinCodeArrowRight;
+        break;
+    case 0x42: // down
+        code = PinCodeArrowDown;
+        break;
+    default:
+        break;
+    }
+
+    return code;
+}
+
+static bool totp_cli_read_pin(Cli* cli, uint8_t* pin, uint8_t* pin_length) {
+    TOTP_CLI_PRINTF("Enter new PIN (use arrow keys on your keyboard): ");
+    fflush(stdout);
+    uint8_t c;
+    *pin_length = 0;
+    while(cli_read(cli, &c, 1) == 1) {
+        if(c == CliSymbolAsciiEsc) {
+            uint8_t c2;
+            uint8_t c3;
+            if(cli_read_timeout(cli, &c2, 1, 0) == 1 && cli_read_timeout(cli, &c3, 1, 0) == 1 &&
+               c2 == 0x5b) {
+                uint8_t code = totp_cli_key_to_pin_code(c3);
+                if(code > 0) {
+                    pin[*pin_length] = code;
+                    *pin_length = *pin_length + 1;
+                    putc('*', stdout);
+                    fflush(stdout);
+                }
+            }
+        } else if(c == CliSymbolAsciiETX) {
+            TOTP_CLI_DELETE_CURRENT_LINE();
+            TOTP_CLI_PRINTF_INFO("Cancelled by user\r\n");
+            return false;
+        } else if(c == CliSymbolAsciiBackspace || c == CliSymbolAsciiDel) {
+            if(*pin_length > 0) {
+                *pin_length = *pin_length - 1;
+                pin[*pin_length] = 0;
+                TOTP_CLI_DELETE_LAST_CHAR();
+            }
+        } else if(c == CliSymbolAsciiCR) {
+            cli_nl();
+            break;
+        }
+    }
+
+    TOTP_CLI_DELETE_LAST_LINE();
+    return true;
+}
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    UNUSED(plugin_state);
+    FuriString* temp_str = furi_string_alloc();
+
+    bool do_change = false;
+    bool do_remove = false;
+    uint8_t crypto_key_slot = plugin_state->crypto_settings.crypto_key_slot;
+
+    bool arguments_parsed = true;
+    while(args_read_string_and_trim(args, temp_str)) {
+        if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_PIN_COMMAND_SET) == 0) {
+            do_change = true;
+        } else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_PIN_COMMAND_REMOVE) == 0) {
+            do_remove = true;
+        } else if(
+            furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_PIN_ARG_NEW_CRYPTO_KEY_SLOT_PREFIX) ==
+            0) {
+            if(!args_read_uint8_and_trim(args, &crypto_key_slot) ||
+               !totp_crypto_check_key_slot(crypto_key_slot)) {
+                TOTP_CLI_PRINTF_ERROR("Slot \"%" PRIu8 "\" can not be used\r\n", crypto_key_slot);
+                arguments_parsed = false;
+                break;
+            }
+        } else {
+            TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+            arguments_parsed = false;
+            break;
+        }
+    }
+
+    if(!(do_change || do_remove) || (do_change && do_remove)) {
+        TOTP_CLI_PRINT_INVALID_ARGUMENTS();
+        arguments_parsed = false;
+    }
+
+    if(arguments_parsed && totp_cli_ensure_authenticated(plugin_state, cli)) {
+        TOTP_CLI_LOCK_UI(plugin_state);
+        do {
+            uint8_t new_pin[CRYPTO_IV_LENGTH];
+            memset(&new_pin[0], 0, CRYPTO_IV_LENGTH);
+            uint8_t new_pin_length = 0;
+            if(do_change) {
+                if(!totp_cli_read_pin(cli, &new_pin[0], &new_pin_length)) {
+                    memset_s(&new_pin[0], CRYPTO_IV_LENGTH, 0, CRYPTO_IV_LENGTH);
+                    break;
+                }
+            } else if(do_remove) {
+                new_pin_length = 0;
+                memset(&new_pin[0], 0, CRYPTO_IV_LENGTH);
+            }
+
+            char* backup_path = totp_config_file_backup(plugin_state);
+            if(backup_path != NULL) {
+                TOTP_CLI_PRINTF_WARNING("Backup conf file %s has been created\r\n", backup_path);
+                TOTP_CLI_PRINTF_WARNING(
+                    "Once you make sure everything is fine and works as expected, please delete this backup file\r\n");
+                free(backup_path);
+            } else {
+                memset_s(&new_pin[0], CRYPTO_IV_LENGTH, 0, CRYPTO_IV_LENGTH);
+                TOTP_CLI_PRINTF_ERROR(
+                    "An error has occurred during taking backup of config file\r\n");
+                break;
+            }
+
+            TOTP_CLI_PRINTF("Encrypting...\r\n");
+
+            bool update_result = totp_config_file_update_encryption(
+                plugin_state, crypto_key_slot, new_pin, new_pin_length);
+
+            memset_s(&new_pin[0], CRYPTO_IV_LENGTH, 0, CRYPTO_IV_LENGTH);
+
+            TOTP_CLI_DELETE_LAST_LINE();
+
+            if(update_result) {
+                if(do_change) {
+                    TOTP_CLI_PRINTF_SUCCESS("PIN has been successfully changed\r\n");
+                } else if(do_remove) {
+                    TOTP_CLI_PRINTF_SUCCESS("PIN has been successfully removed\r\n");
+                }
+            } else {
+                TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE();
+            }
+
+        } while(false);
+
+        TOTP_CLI_UNLOCK_UI(plugin_state);
+    }
+
+    furi_string_free(temp_str);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: PIN", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_pin_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 4 - 0
totp/cli/plugins/reset/meta.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_RESET "reset"
+#define TOTP_CLI_PLUGIN_RESET_FILE_NAME "totp_cli_reset_plugin"

+ 43 - 0
totp/cli/plugins/reset/reset.c

@@ -0,0 +1,43 @@
+#include <flipper_application/flipper_application.h>
+#include "../../cli_helpers.h"
+#include "../../cli_shared_methods.h"
+#include "../../cli_plugin_interface.h"
+#include "../../../ui/scene_director.h"
+#include "../../../services/config/config.h"
+
+#define TOTP_CLI_RESET_CONFIRMATION_KEYWORD "YES"
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    UNUSED(args);
+    TOTP_CLI_LOCK_UI(plugin_state);
+    TOTP_CLI_PRINTF_WARNING(
+        "As a result of reset all the settings and tokens will be permanently lost.\r\n");
+    TOTP_CLI_PRINTF_WARNING("Do you really want to reset application?\r\n");
+    TOTP_CLI_PRINTF_WARNING("Type \"" TOTP_CLI_RESET_CONFIRMATION_KEYWORD
+                            "\" and hit <ENTER> to confirm:\r\n");
+    FuriString* temp_str = furi_string_alloc();
+    bool is_confirmed = totp_cli_read_line(cli, temp_str, false) &&
+                        furi_string_cmpi_str(temp_str, TOTP_CLI_RESET_CONFIRMATION_KEYWORD) == 0;
+    furi_string_free(temp_str);
+    if(is_confirmed) {
+        totp_config_file_reset(plugin_state);
+        TOTP_CLI_PRINTF_SUCCESS("Application has been successfully reset to default.\r\n");
+        TOTP_CLI_PRINTF_SUCCESS("Now application will be closed to apply all the changes.\r\n");
+        totp_cli_force_close_app(plugin_state->event_queue);
+    } else {
+        TOTP_CLI_PRINTF_INFO("Action was not confirmed by user\r\n");
+        TOTP_CLI_UNLOCK_UI(plugin_state);
+    }
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Reset", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_reset_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 5 - 0
totp/cli/plugins/timezone/meta.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_TIMEZONE "timezone"
+#define TOTP_CLI_COMMAND_TIMEZONE_ALT "tz"
+#define TOTP_CLI_PLUGIN_TIMEZONE_FILE_NAME "totp_cli_timezone_plugin"

+ 49 - 0
totp/cli/plugins/timezone/timezone.c

@@ -0,0 +1,49 @@
+#include <lib/toolbox/args.h>
+#include <flipper_application/flipper_application.h>
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+#include "../../cli_helpers.h"
+#include "../../cli_plugin_interface.h"
+#include "../../cli_shared_methods.h"
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    UNUSED(args);
+    UNUSED(plugin_state);
+    if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
+        return;
+    }
+
+    FuriString* temp_str = furi_string_alloc();
+    if(args_read_string_and_trim(args, temp_str)) {
+        char* strtof_endptr;
+        float tz = strtof(furi_string_get_cstr(temp_str), &strtof_endptr);
+        if(*strtof_endptr == 0 && tz >= -12.75f && tz <= 12.75f) {
+            TOTP_CLI_LOCK_UI(plugin_state);
+            plugin_state->timezone_offset = tz;
+            if(totp_config_file_update_timezone_offset(plugin_state)) {
+                TOTP_CLI_PRINTF_SUCCESS("Timezone is set to %f\r\n", (double)tz);
+            } else {
+                TOTP_CLI_PRINT_ERROR_UPDATING_CONFIG_FILE();
+            }
+            TOTP_CLI_UNLOCK_UI(plugin_state);
+        } else {
+            TOTP_CLI_PRINTF_ERROR("Invalid timezone offset\r\n");
+        }
+    } else {
+        TOTP_CLI_PRINTF_INFO(
+            "Current timezone offset is %f\r\n", (double)plugin_state->timezone_offset);
+    }
+    furi_string_free(temp_str);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Timezone", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_timezone_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 4 - 0
totp/cli/plugins/version/meta.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#define TOTP_CLI_COMMAND_VERSION "version"
+#define TOTP_CLI_PLUGIN_VERSION_FILE_NAME "totp_cli_version_plugin"

+ 27 - 0
totp/cli/plugins/version/version.c

@@ -0,0 +1,27 @@
+#include <flipper_application/flipper_application.h>
+#include "../../cli_helpers.h"
+#include "../../cli_plugin_interface.h"
+#include "../../../version.h"
+
+static void handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    UNUSED(args);
+    UNUSED(cli);
+    UNUSED(plugin_state);
+    TOTP_CLI_PRINTF(
+        "%" PRIu8 ".%" PRIu8 ".%" PRIu8 "\r\n",
+        TOTP_APP_VERSION_MAJOR,
+        TOTP_APP_VERSION_MINOR,
+        TOTP_APP_VERSION_PATCH);
+}
+
+static const CliPlugin plugin = {.name = "TOTP CLI Plugin: Version", .handle = &handle};
+
+static const FlipperAppPluginDescriptor plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &plugin,
+};
+
+const FlipperAppPluginDescriptor* totp_cli_version_plugin_ep() {
+    return &plugin_descriptor;
+}

+ 45 - 0
totp/config/app/config.h

@@ -0,0 +1,45 @@
+// List of compatible firmwares
+#define TOTP_FIRMWARE_OFFICIAL_STABLE (1)
+#define TOTP_FIRMWARE_OFFICIAL_DEV (2)
+#define TOTP_FIRMWARE_XTREME_UL (3)
+// End of list
+
+#if __has_include("ufbt_def.h")
+#include "ufbt_def.h"
+#endif
+
+#ifndef TOTP_TARGET_FIRMWARE
+#if defined(TARGET_FIRMWARE_OFFICIAL) || defined(FW_ORIGIN_Official)
+#define TOTP_TARGET_FIRMWARE TOTP_FIRMWARE_OFFICIAL_STABLE
+#elif defined(TARGET_FIRMWARE_UNLEASHED) || defined(FW_ORIGIN_Unleashed)
+#define TOTP_TARGET_FIRMWARE TOTP_FIRMWARE_XTREME_UL
+#elif defined(TARGET_FIRMWARE_XTREME) || defined(FW_ORIGIN_Xtreme)
+#define TOTP_TARGET_FIRMWARE TOTP_FIRMWARE_XTREME_UL
+#else
+#define TOTP_TARGET_FIRMWARE TOTP_FIRMWARE_OFFICIAL_STABLE
+#endif
+#endif
+
+// Application automatic lock timeout if user IDLE. (ticks)
+#ifndef TOTP_AUTO_LOCK_IDLE_TIMEOUT_SEC
+#define TOTP_AUTO_LOCK_IDLE_TIMEOUT_SEC (60)
+#endif
+
+// Enables\disables Bluetooth token input automation
+#ifndef TOTP_NO_BADBT_AUTOMATION
+#define TOTP_BADBT_AUTOMATION_ENABLED
+#endif
+
+// Enables\disables backward compatibility with crypto algorithms v1
+// #define TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+
+// Enables\disables backward compatibility with crypto algorithms v2
+#ifndef TOTP_NO_OBSOLETE_CRYPTO_V2_COMPATIBILITY
+#define TOTP_OBSOLETE_CRYPTO_V2_COMPATIBILITY_ENABLED
+#endif
+
+// Enables\disables "Add new token" UI
+// If disabled it will print a link to wiki page
+#ifndef TOTP_UI_NO_ADD_NEW_TOKEN
+#define TOTP_UI_ADD_NEW_TOKEN_ENABLED
+#endif

+ 34 - 0
totp/config/wolfssl/config.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#define NO_OLD_SHA_NAMES
+#define WOLFCRYPT_ONLY
+#define NO_SIG_WRAPPER
+#define NO_AES
+#define NO_AES_CBC
+#define NO_DES3
+#define NO_DSA
+#define NO_RSA
+#define NO_DH
+#define NO_RC4
+#define NO_MD4
+#define NO_MD5
+#define NO_PKCS12
+#define NO_PKCS8
+#define WC_NO_RNG
+#define NO_FILESYSTEM
+#define NO_WRITEV
+#define NO_MAIN_DRIVER
+#define NO_DEV_RANDOM
+#define WOLFSSL_SHA512
+#define WOLFSSL_NOSHA512_224
+#define WOLFSSL_NOSHA512_256
+#define USE_SLOW_SHA512
+#define USE_SLOW_SHA256
+#define USE_SLOW_SHA
+#define NO_CERTS
+#define NO_WOLFSSL_MEMORY
+#define WOLFSSL_NO_PEM
+#define NO_PSK
+#define NO_ERROR_STRINGS
+#define NO_OLD_TLS
+#define SINGLE_THREADED

binární
totp/images/DolphinCommon_56x48.png


binární
totp/images/hid_ble_31x9.png


binární
totp/images/hid_usb_31x9.png


binární
totp/images/totp_arrow_left_8x9.png


binární
totp/images/totp_arrow_right_8x9.png


+ 60 - 0
totp/lib/base32/base32.c

@@ -0,0 +1,60 @@
+// Base32 implementation
+//
+// Copyright 2010 Google Inc.
+// Author: Markus Gutschke
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "base32.h"
+
+size_t base32_decode(const uint8_t* encoded, uint8_t* result, size_t bufSize) {
+    int buffer = 0;
+    int bitsLeft = 0;
+    size_t count = 0;
+    for(const uint8_t* ptr = encoded; count < bufSize && *ptr; ++ptr) {
+        uint8_t ch = *ptr;
+        if(ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-') {
+            continue;
+        }
+        buffer <<= 5;
+
+        // Deal with commonly mistyped characters
+        if(ch == '0') {
+            ch = 'O';
+        } else if(ch == '1') {
+            ch = 'L';
+        } else if(ch == '8') {
+            ch = 'B';
+        }
+
+        // Look up one base32 digit
+        if((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
+            ch = (ch & 0x1F) - 1;
+        } else if(ch >= '2' && ch <= '7') {
+            ch -= '2' - 26;
+        } else {
+            return 0;
+        }
+
+        buffer |= ch;
+        bitsLeft += 5;
+        if(bitsLeft >= 8) {
+            result[count++] = buffer >> (bitsLeft - 8);
+            bitsLeft -= 8;
+        }
+    }
+    if(count < bufSize) {
+        result[count] = '\000';
+    }
+    return count;
+}

+ 40 - 0
totp/lib/base32/base32.h

@@ -0,0 +1,40 @@
+// Base32 implementation
+//
+// Copyright 2010 Google Inc.
+// Author: Markus Gutschke
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Encode and decode from base32 encoding using the following alphabet:
+//   ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
+// This alphabet is documented in RFC 4648/3548
+//
+// We allow white-space and hyphens, but all other characters are considered
+// invalid.
+//
+// All functions return the number of output bytes or -1 on error. If the
+// output buffer is too small, the result will silently be truncated.
+
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * @brief Decodes Base-32 encoded bytes into plain bytes.
+ * @param encoded Base-32 encoded bytes
+ * @param[out] result result output buffer
+ * @param bufSize result output buffer size
+ * @return Decoded result length in bytes if successfully decoded; \c 0 otherwise
+ */
+size_t base32_decode(const uint8_t* encoded, uint8_t* result, size_t bufSize);

+ 73 - 0
totp/lib/base64/base64.c

@@ -0,0 +1,73 @@
+/*
+ * Base64 encoding/decoding (RFC1341)
+ * Copyright (c) 2005, Jouni Malinen <j@w1.fi>
+ * Modified and optimized for Flipepr Zero device purposes by Alex Kopachov (@akopachov)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * Alternatively, this software may be distributed under the terms of BSD
+ * license.
+ *
+ */
+
+#include "base64.h"
+#include <string.h>
+
+static const uint8_t dtable[] = {0x3e, 0x80, 0x80, 0x80, 0x3f, 0x34, 0x35, 0x36, 0x37, 0x38,
+                                 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x80, 0x80, 0x80, 0x0,  0x80,
+                                 0x80, 0x80, 0x0,  0x1,  0x2,  0x3,  0x4,  0x5,  0x6,  0x7,
+                                 0x8,  0x9,  0xa,  0xb,  0xc,  0xd,  0xe,  0xf,  0x10, 0x11,
+                                 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x80, 0x80,
+                                 0x80, 0x80, 0x80, 0x80, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+                                 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29,
+                                 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33};
+
+static uint8_t get_dtable_value(uint8_t index) {
+    return (index < 43 || index > 122) ? 0x80 : dtable[index - 43];
+}
+
+uint8_t* base64_decode(const uint8_t* src, size_t len, size_t* out_len, size_t* out_size) {
+    uint8_t* out;
+    uint8_t* pos;
+    uint8_t in[4];
+    uint8_t block[4];
+    uint8_t tmp;
+    size_t i;
+    size_t count;
+    size_t olen;
+
+    count = 0;
+    for(i = 0; i < len; i++) {
+        if(get_dtable_value(src[i]) != 0x80) count++;
+    }
+
+    if(count == 0 || count % 4) return NULL;
+    olen = count / 4 * 3;
+    pos = out = malloc(olen);
+    *out_size = olen;
+    if(out == NULL) return NULL;
+    count = 0;
+    for(i = 0; i < len; i++) {
+        tmp = get_dtable_value(src[i]);
+        if(tmp == 0x80) continue;
+        in[count] = src[i];
+        block[count] = tmp;
+        count++;
+        if(count == 4) {
+            *pos++ = (block[0] << 2) | (block[1] >> 4);
+            *pos++ = (block[1] << 4) | (block[2] >> 2);
+            *pos++ = (block[2] << 6) | block[3];
+            count = 0;
+        }
+    }
+    if(pos > out) {
+        if(in[2] == '=')
+            pos -= 2;
+        else if(in[3] == '=')
+            pos--;
+    }
+    *out_len = pos - out;
+    return out;
+}

+ 14 - 0
totp/lib/base64/base64.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <stdlib.h>
+#include <stdint.h>
+
+/**
+ * @brief Decodes Base-64 encoded bytes into plain bytes.
+ * @param src Base-64 encoded bytes
+ * @param len Base-64 encoded bytes count
+ * @param[out] out_len decoded buffer length
+ * @param[out] out_size decoded buffer allocated size
+ * @return Decoded result buffer if successfully decoded; \c NULL otherwise
+ */
+uint8_t* base64_decode(const uint8_t* src, size_t len, size_t* out_len, size_t* out_size);

+ 22 - 0
totp/lib/polyfills/memset_s.c

@@ -0,0 +1,22 @@
+#include "memset_s.h"
+
+#define RSIZE_MAX 0x7fffffffffffffffUL
+
+errno_t memset_s(void* s, rsize_t smax, int c, rsize_t n) {
+    if(!s || smax > RSIZE_MAX) {
+        return EINVAL;
+    }
+
+    errno_t violation_present = 0;
+    if(n > smax) {
+        n = smax;
+        violation_present = EINVAL;
+    }
+
+    volatile unsigned char* v = s;
+    for(rsize_t i = 0u; i < n; ++i) {
+        *v++ = (unsigned char)c;
+    }
+
+    return violation_present;
+}

+ 31 - 0
totp/lib/polyfills/memset_s.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <errno.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef _RSIZE_T_DECLARED
+typedef uint64_t rsize_t;
+#define _RSIZE_T_DECLARED
+#endif
+#ifndef _ERRNOT_DECLARED
+typedef int16_t errno_t; //-V677
+#define _ERRNOT_DECLARED
+#endif
+
+/**
+ * @brief Copies the value \p c into each of the first \p n characters of the object pointed to by \p s.
+ * @param s pointer to the object to fill
+ * @param smax size of the destination object
+ * @param c fill byte
+ * @param n number of bytes to fill
+ * @return \c 0 on success; non-zero otherwise
+ */
+errno_t memset_s(void* s, rsize_t smax, int c, rsize_t n);
+
+#ifdef __cplusplus
+}
+#endif

+ 11 - 0
totp/lib/polyfills/strnlen.c

@@ -0,0 +1,11 @@
+#include "strnlen.h"
+
+size_t strnlen(const char* s, size_t maxlen) {
+    size_t len;
+
+    for(len = 0; len < maxlen; len++, s++) {
+        if(!*s) break;
+    }
+
+    return len;
+}

+ 6 - 0
totp/lib/polyfills/strnlen.h

@@ -0,0 +1,6 @@
+#pragma once
+#pragma weak strnlen
+
+#include <stddef.h>
+
+size_t strnlen(const char* s, size_t maxlen);

+ 28 - 0
totp/lib/roll_value/roll_value.c

@@ -0,0 +1,28 @@
+#include "roll_value.h"
+
+#define TOTP_ROLL_VALUE_FN(type, step_type)                            \
+    TOTP_ROLL_VALUE_FN_HEADER(type, step_type) {                       \
+        type v = *value;                                               \
+        if(step > 0 && v > max - step) {                               \
+            if(overflow_behavior == RollOverflowBehaviorRoll) {        \
+                v = min;                                               \
+            } else if(overflow_behavior == RollOverflowBehaviorStop) { \
+                v = max;                                               \
+            }                                                          \
+        } else if(step < 0 && v < min - step) {                        \
+            if(overflow_behavior == RollOverflowBehaviorRoll) {        \
+                v = max;                                               \
+            } else if(overflow_behavior == RollOverflowBehaviorStop) { \
+                v = min;                                               \
+            }                                                          \
+        } else {                                                       \
+            v += step;                                                 \
+        }                                                              \
+        *value = v;                                                    \
+    }
+
+TOTP_ROLL_VALUE_FN(int8_t, int8_t)
+
+TOTP_ROLL_VALUE_FN(uint8_t, int8_t)
+
+TOTP_ROLL_VALUE_FN(size_t, int16_t);

+ 59 - 0
totp/lib/roll_value/roll_value.h

@@ -0,0 +1,59 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+typedef uint8_t TotpRollValueOverflowBehavior;
+
+enum TotpRollValueOverflowBehaviors {
+    /**
+     * @brief Do not change value if it reached constraint
+     */
+    RollOverflowBehaviorStop,
+
+    /**
+     * @brief Set value to opposite constraint value if it reached constraint
+     */
+    RollOverflowBehaviorRoll
+};
+
+#define TOTP_ROLL_VALUE_FN_HEADER(type, step_type) \
+    void totp_roll_value_##type(                   \
+        type* value,                               \
+        step_type step,                            \
+        type min,                                  \
+        type max,                                  \
+        TotpRollValueOverflowBehavior overflow_behavior)
+
+/**
+ * @brief Rolls \c int8_t \p value using \p min and \p max as an value constraints with \p step step.
+ *        When value reaches constraint value \p overflow_behavior defines what to do next.
+ * @param[in,out] value value to roll
+ * @param step step to be used to change value
+ * @param min minimal possible value
+ * @param max maximum possible value
+ * @param overflow_behavior defines what to do when value reaches constraint value
+ */
+TOTP_ROLL_VALUE_FN_HEADER(int8_t, int8_t);
+
+/**
+ * @brief Rolls \c uint8_t \p value using \p min and \p max as an value constraints with \p step step.
+ *        When value reaches constraint value \p overflow_behavior defines what to do next.
+ * @param[in,out] value value to roll
+ * @param step step to be used to change value
+ * @param min minimal possible value
+ * @param max maximum possible value
+ * @param overflow_behavior defines what to do when value reaches constraint value
+ */
+TOTP_ROLL_VALUE_FN_HEADER(uint8_t, int8_t);
+
+/**
+ * @brief Rolls \c size_t \p value using \p min and \p max as an value constraints with \p step step.
+ *        When value reaches constraint value \p overflow_behavior defines what to do next.
+ * @param[in,out] value value to roll
+ * @param step step to be used to change value
+ * @param min minimal possible value
+ * @param max maximum possible value
+ * @param overflow_behavior defines what to do when value reaches constraint value
+ */
+TOTP_ROLL_VALUE_FN_HEADER(size_t, int16_t);

+ 16 - 0
totp/lib/timezone_utils/timezone_utils.c

@@ -0,0 +1,16 @@
+#include "timezone_utils.h"
+
+int32_t timezone_offset_from_hours(float hours) {
+    return hours * 3600.0f;
+}
+
+uint64_t timezone_offset_apply(uint64_t time, int32_t offset) {
+    uint64_t for_time_adjusted;
+    if(offset > 0) {
+        for_time_adjusted = time - offset;
+    } else {
+        for_time_adjusted = time + (-offset);
+    }
+
+    return for_time_adjusted;
+}

+ 18 - 0
totp/lib/timezone_utils/timezone_utils.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#include <inttypes.h>
+
+/**
+ * @brief Calculates timezone offset in seconds given timezone offset in hours.
+ * @param hours timezone offset in hours
+ * @return Timezone offset in seconds.
+ */
+int32_t timezone_offset_from_hours(float hours);
+
+/**
+ * @brief Applies timezone offset to a given time.
+ * @param time time to apply offset to.
+ * @param offset timezone offset in seconds.
+ * @return Time with timezone offset applied.
+ */
+uint64_t timezone_offset_apply(uint64_t time, int32_t offset);

+ 1 - 0
totp/lib/wolfssl

@@ -0,0 +1 @@
+Subproject commit 3b3c175af0e993ffaae251871421e206cc41963f

+ 739 - 0
totp/services/config/config.c

@@ -0,0 +1,739 @@
+#include "config.h"
+#include <stdlib.h>
+#include <string.h>
+#include <flipper_format/flipper_format.h>
+#include <furi_hal_rtc.h>
+#include <flipper_format/flipper_format_i.h>
+#include <flipper_format/flipper_format_stream.h>
+#include <memset_s.h>
+#include "../../types/common.h"
+#include "../../types/token_info.h"
+#include "../../config/app/config.h"
+#include "../crypto/crypto_facade.h"
+#include "../crypto/constants.h"
+#include "migrations/common_migration.h"
+
+#define CONFIG_FILE_PATH CONFIG_FILE_DIRECTORY_PATH "/totp.conf"
+#define CONFIG_FILE_BACKUP_DIR CONFIG_FILE_DIRECTORY_PATH "/backups"
+#define CONFIG_FILE_BACKUP_BASE_PATH CONFIG_FILE_BACKUP_DIR "/totp.conf"
+
+struct ConfigFileContext {
+    /**
+     * @brief Config file reference
+     */
+    FlipperFormat* config_file;
+
+    /**
+     * @brief Storage reference
+     */
+    Storage* storage;
+
+    /**
+     * @brief Token list iterator context 
+     */
+    TokenInfoIteratorContext* token_info_iterator_context;
+};
+
+/**
+ * @brief Opens storage record
+ * @return Storage record
+ */
+static Storage* totp_open_storage() {
+    return furi_record_open(RECORD_STORAGE);
+}
+
+/**
+ * @brief Closes storage record
+ */
+static void totp_close_storage() {
+    furi_record_close(RECORD_STORAGE);
+}
+
+/**
+ * @brief Closes config file
+ * @param file config file reference
+ */
+static void totp_close_config_file(FlipperFormat* file) {
+    if(file == NULL) return;
+    flipper_format_file_close(file);
+    flipper_format_free(file);
+}
+
+/**
+ * @brief Tries to take a config file backup
+ * @param storage storage record
+ * @return backup path if backup successfully taken; \c NULL otherwise
+ */
+static char* totp_config_file_backup_i(Storage* storage) {
+    if(!storage_dir_exists(storage, CONFIG_FILE_BACKUP_DIR) &&
+       !storage_simply_mkdir(storage, CONFIG_FILE_BACKUP_DIR)) {
+        return NULL;
+    }
+
+    FuriHalRtcDateTime current_datetime;
+    furi_hal_rtc_get_datetime(&current_datetime);
+
+    uint8_t backup_path_size = sizeof(CONFIG_FILE_BACKUP_BASE_PATH) + 14;
+    char* backup_path = malloc(backup_path_size);
+    furi_check(backup_path != NULL);
+    memcpy(backup_path, CONFIG_FILE_BACKUP_BASE_PATH, sizeof(CONFIG_FILE_BACKUP_BASE_PATH));
+    uint16_t i = 1;
+    bool backup_file_exists;
+    do {
+        snprintf(
+            backup_path,
+            backup_path_size,
+            CONFIG_FILE_BACKUP_BASE_PATH ".%4" PRIu16 "%02" PRIu8 "%02" PRIu8 "-%" PRIu16,
+            current_datetime.year,
+            current_datetime.month,
+            current_datetime.day,
+            i);
+        i++;
+    } while((backup_file_exists = storage_common_exists(storage, backup_path)) && i <= 9999);
+
+    if(backup_file_exists ||
+       storage_common_copy(storage, CONFIG_FILE_PATH, backup_path) != FSE_OK) {
+        FURI_LOG_E(LOGGING_TAG, "Unable to take a backup");
+        free(backup_path);
+        return NULL;
+    }
+
+    FURI_LOG_I(LOGGING_TAG, "Took config file backup to %s", backup_path);
+    return backup_path;
+}
+
+/**
+ * @brief Opens or creates TOTP application standard config file
+ * @param storage storage record to use
+ * @param[out] file opened config file
+ * @return Config file open result
+ */
+static bool totp_open_config_file(Storage* storage, FlipperFormat** file) {
+    FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
+
+    bool conf_file_exists = storage_common_stat(storage, CONFIG_FILE_PATH, NULL) == FSE_OK;
+    if(!conf_file_exists &&
+       storage_common_stat(storage, EXT_PATH("authenticator"), NULL) == FSE_OK) {
+        FURI_LOG_I(LOGGING_TAG, "Application catalog needs to be migrated");
+        FS_Error migration_result =
+            storage_common_migrate(storage, EXT_PATH("authenticator"), CONFIG_FILE_DIRECTORY_PATH);
+        FURI_LOG_I(LOGGING_TAG, "Migrated catalog. Result code: %d", (int)migration_result);
+        conf_file_exists = storage_common_stat(storage, CONFIG_FILE_PATH, NULL) == FSE_OK;
+    }
+
+    if(conf_file_exists) {
+        FURI_LOG_D(LOGGING_TAG, "Config file %s found", CONFIG_FILE_PATH);
+        if(!flipper_format_file_open_existing(fff_data_file, CONFIG_FILE_PATH)) {
+            FURI_LOG_E(LOGGING_TAG, "Error opening existing file %s", CONFIG_FILE_PATH);
+            totp_close_config_file(fff_data_file);
+            return false;
+        }
+    } else {
+        if(storage_common_stat(storage, CONFIG_FILE_DIRECTORY_PATH, NULL) == FSE_NOT_EXIST) {
+            FURI_LOG_D(LOGGING_TAG, "Config file directory doesn't exist. Will create new");
+            if(!storage_simply_mkdir(storage, CONFIG_FILE_DIRECTORY_PATH)) {
+                FURI_LOG_E(
+                    LOGGING_TAG,
+                    "Error creating config file directory %s",
+                    CONFIG_FILE_DIRECTORY_PATH);
+                return false;
+            }
+        }
+
+        FURI_LOG_D(LOGGING_TAG, "Config file %s is not found. Will create new.", CONFIG_FILE_PATH);
+
+        if(!flipper_format_file_open_new(fff_data_file, CONFIG_FILE_PATH)) {
+            totp_close_config_file(fff_data_file);
+            FURI_LOG_E(LOGGING_TAG, "Error creating new file %s", CONFIG_FILE_PATH);
+            return false;
+        }
+
+        flipper_format_write_header_cstr(
+            fff_data_file, CONFIG_FILE_HEADER, CONFIG_FILE_ACTUAL_VERSION);
+
+        uint32_t tmp_uint32 = CRYPTO_LATEST_VERSION;
+        flipper_format_write_uint32(fff_data_file, TOTP_CONFIG_KEY_CRYPTO_VERSION, &tmp_uint32, 1);
+
+        tmp_uint32 = DEFAULT_CRYPTO_KEY_SLOT;
+        flipper_format_write_uint32(
+            fff_data_file, TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT, &tmp_uint32, 1);
+
+        flipper_format_write_comment_cstr(
+            fff_data_file,
+            "Config file format specification can be found here: https://t.ly/zwQjE");
+
+        float tmp_tz = 0;
+        flipper_format_write_float(fff_data_file, TOTP_CONFIG_KEY_TIMEZONE, &tmp_tz, 1);
+
+        tmp_uint32 = NotificationMethodSound | NotificationMethodVibro;
+        flipper_format_write_uint32(
+            fff_data_file, TOTP_CONFIG_KEY_NOTIFICATION_METHOD, &tmp_uint32, 1);
+
+        tmp_uint32 = AutomationMethodBadUsb;
+        flipper_format_write_uint32(
+            fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_METHOD, &tmp_uint32, 1);
+
+        tmp_uint32 = AutomationKeyboardLayoutQWERTY;
+        flipper_format_write_uint32(
+            fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, &tmp_uint32, 1);
+
+        tmp_uint32 = 0; //-V1048
+        flipper_format_write_uint32(fff_data_file, TOTP_CONFIG_KEY_FONT, &tmp_uint32, 1);
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            totp_close_config_file(fff_data_file);
+            FURI_LOG_E(LOGGING_TAG, "Rewind error");
+            return false;
+        }
+    }
+
+    *file = fff_data_file;
+    return true;
+}
+
+char* totp_config_file_backup(const PluginState* plugin_state) {
+    if(plugin_state->config_file_context == NULL) return NULL;
+
+    totp_close_config_file(plugin_state->config_file_context->config_file);
+
+    char* result = totp_config_file_backup_i(plugin_state->config_file_context->storage);
+
+    totp_open_config_file(
+        plugin_state->config_file_context->storage,
+        &plugin_state->config_file_context->config_file);
+
+    totp_token_info_iterator_attach_to_config_file(
+        plugin_state->config_file_context->token_info_iterator_context,
+        plugin_state->config_file_context->config_file);
+
+    return result;
+}
+
+bool totp_config_file_update_timezone_offset(const PluginState* plugin_state) {
+    FlipperFormat* file = plugin_state->config_file_context->config_file;
+    flipper_format_rewind(file);
+    bool update_result = false;
+
+    do {
+        if(!flipper_format_insert_or_update_float(
+               file, TOTP_CONFIG_KEY_TIMEZONE, &plugin_state->timezone_offset, 1)) {
+            break;
+        }
+
+        update_result = true;
+    } while(false);
+
+    return update_result;
+}
+
+bool totp_config_file_update_notification_method(const PluginState* plugin_state) {
+    FlipperFormat* file = plugin_state->config_file_context->config_file;
+    flipper_format_rewind(file);
+    bool update_result = false;
+
+    do {
+        uint32_t tmp_uint32 = plugin_state->notification_method;
+        if(!flipper_format_insert_or_update_uint32(
+               file, TOTP_CONFIG_KEY_NOTIFICATION_METHOD, &tmp_uint32, 1)) {
+            break;
+        }
+
+        update_result = true;
+    } while(false);
+
+    return update_result;
+}
+
+bool totp_config_file_update_automation_method(const PluginState* plugin_state) {
+    FlipperFormat* file = plugin_state->config_file_context->config_file;
+    flipper_format_rewind(file);
+    bool update_result = false;
+
+    do {
+        uint32_t tmp_uint32 = plugin_state->automation_method;
+        if(!flipper_format_insert_or_update_uint32(
+               file, TOTP_CONFIG_KEY_AUTOMATION_METHOD, &tmp_uint32, 1)) {
+            break;
+        }
+
+        tmp_uint32 = plugin_state->automation_kb_layout;
+        if(!flipper_format_insert_or_update_uint32(
+               file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, &tmp_uint32, 1)) {
+            break;
+        }
+
+        update_result = true;
+    } while(false);
+
+    return update_result;
+}
+
+bool totp_config_file_update_user_settings(const PluginState* plugin_state) {
+    FlipperFormat* file = plugin_state->config_file_context->config_file;
+    flipper_format_rewind(file);
+    bool update_result = false;
+    do {
+        if(!flipper_format_insert_or_update_float(
+               file, TOTP_CONFIG_KEY_TIMEZONE, &plugin_state->timezone_offset, 1)) {
+            break;
+        }
+        uint32_t tmp_uint32 = plugin_state->notification_method;
+        if(!flipper_format_insert_or_update_uint32(
+               file, TOTP_CONFIG_KEY_NOTIFICATION_METHOD, &tmp_uint32, 1)) {
+            break;
+        }
+
+        tmp_uint32 = plugin_state->automation_method;
+        if(!flipper_format_insert_or_update_uint32(
+               file, TOTP_CONFIG_KEY_AUTOMATION_METHOD, &tmp_uint32, 1)) {
+            break;
+        }
+
+        tmp_uint32 = plugin_state->active_font_index;
+        if(!flipper_format_insert_or_update_uint32(file, TOTP_CONFIG_KEY_FONT, &tmp_uint32, 1)) {
+            break;
+        }
+
+        tmp_uint32 = plugin_state->automation_kb_layout;
+        if(!flipper_format_insert_or_update_uint32(
+               file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, &tmp_uint32, 1)) {
+            break;
+        }
+
+        update_result = true;
+    } while(false);
+
+    return update_result;
+}
+
+bool totp_config_file_load(PluginState* const plugin_state) {
+    Storage* storage = totp_open_storage();
+    FlipperFormat* fff_data_file;
+    if(!totp_open_config_file(storage, &fff_data_file)) {
+        totp_close_storage();
+        return false;
+    }
+
+    flipper_format_rewind(fff_data_file);
+
+    bool result = false;
+
+    plugin_state->timezone_offset = 0;
+
+    FuriString* temp_str = furi_string_alloc();
+
+    do {
+        uint32_t file_version;
+        if(!flipper_format_read_header(fff_data_file, temp_str, &file_version)) {
+            FURI_LOG_E(LOGGING_TAG, "Missing or incorrect header");
+            break;
+        }
+
+        if(file_version < CONFIG_FILE_ACTUAL_VERSION) {
+            FURI_LOG_I(
+                LOGGING_TAG,
+                "Obsolete config file version detected. Current version: %" PRIu32
+                "; Actual version: %" PRId16,
+                file_version,
+                CONFIG_FILE_ACTUAL_VERSION);
+            totp_close_config_file(fff_data_file);
+
+            char* backup_path = totp_config_file_backup_i(storage);
+
+            if(backup_path != NULL) {
+                if(totp_open_config_file(storage, &fff_data_file) != true) {
+                    break;
+                }
+
+                FlipperFormat* fff_backup_data_file = flipper_format_file_alloc(storage);
+                if(!flipper_format_file_open_existing(fff_backup_data_file, backup_path)) {
+                    flipper_format_file_close(fff_backup_data_file);
+                    flipper_format_free(fff_backup_data_file);
+                    break;
+                }
+
+                if(totp_config_migrate_to_latest(fff_data_file, fff_backup_data_file)) {
+                    FURI_LOG_I(
+                        LOGGING_TAG,
+                        "Applied migration to version %" PRId16,
+                        CONFIG_FILE_ACTUAL_VERSION);
+                    file_version = CONFIG_FILE_ACTUAL_VERSION;
+                } else {
+                    FURI_LOG_W(
+                        LOGGING_TAG,
+                        "An error occurred during migration to version %" PRId16,
+                        CONFIG_FILE_ACTUAL_VERSION);
+                    break;
+                }
+
+                flipper_format_file_close(fff_backup_data_file);
+                flipper_format_free(fff_backup_data_file);
+                flipper_format_rewind(fff_data_file);
+                free(backup_path);
+            } else {
+                FURI_LOG_E(
+                    LOGGING_TAG,
+                    "An error occurred during taking backup of %s before migration",
+                    CONFIG_FILE_PATH);
+                break;
+            }
+        }
+
+        uint32_t tmp_uint32;
+
+        if(!flipper_format_read_uint32(
+               fff_data_file, TOTP_CONFIG_KEY_CRYPTO_VERSION, &tmp_uint32, 1)) {
+            FURI_LOG_E(LOGGING_TAG, "Missing required " TOTP_CONFIG_KEY_CRYPTO_VERSION "property");
+            break;
+        }
+
+        plugin_state->crypto_settings.crypto_version = tmp_uint32;
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        if(!flipper_format_read_uint32(
+               fff_data_file, TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT, &tmp_uint32, 1)) {
+            FURI_LOG_E(
+                LOGGING_TAG, "Missing required " TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT "property");
+            break;
+        }
+
+        plugin_state->crypto_settings.crypto_key_slot = tmp_uint32;
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        if(!flipper_format_read_hex(
+               fff_data_file,
+               TOTP_CONFIG_KEY_SALT,
+               &plugin_state->crypto_settings.salt[0],
+               CRYPTO_SALT_LENGTH)) {
+            FURI_LOG_D(LOGGING_TAG, "Missing salt");
+        }
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        uint32_t crypto_size;
+        if(flipper_format_get_value_count(
+               fff_data_file, TOTP_CONFIG_KEY_CRYPTO_VERIFY, &crypto_size) &&
+           crypto_size > 0) {
+            plugin_state->crypto_settings.crypto_verify_data =
+                malloc(sizeof(uint8_t) * crypto_size);
+            furi_check(plugin_state->crypto_settings.crypto_verify_data != NULL);
+            plugin_state->crypto_settings.crypto_verify_data_length = crypto_size;
+            if(!flipper_format_read_hex(
+                   fff_data_file,
+                   TOTP_CONFIG_KEY_CRYPTO_VERIFY,
+                   plugin_state->crypto_settings.crypto_verify_data,
+                   crypto_size)) {
+                FURI_LOG_D(LOGGING_TAG, "Missing crypto verify token");
+                free(plugin_state->crypto_settings.crypto_verify_data);
+                plugin_state->crypto_settings.crypto_verify_data = NULL;
+                plugin_state->crypto_settings.crypto_verify_data_length = 0;
+            }
+        } else {
+            plugin_state->crypto_settings.crypto_verify_data = NULL;
+            plugin_state->crypto_settings.crypto_verify_data_length = 0;
+        }
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        if(!flipper_format_read_float(
+               fff_data_file, TOTP_CONFIG_KEY_TIMEZONE, &plugin_state->timezone_offset, 1)) {
+            plugin_state->timezone_offset = 0;
+            FURI_LOG_D(LOGGING_TAG, "Missing timezone offset information, defaulting to 0");
+        }
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        if(!flipper_format_read_bool(
+               fff_data_file,
+               TOTP_CONFIG_KEY_PINSET,
+               &plugin_state->crypto_settings.pin_required,
+               1)) {
+            plugin_state->crypto_settings.pin_required = true;
+        }
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        if(!flipper_format_read_uint32(
+               fff_data_file, TOTP_CONFIG_KEY_NOTIFICATION_METHOD, &tmp_uint32, 1)) {
+            tmp_uint32 = NotificationMethodSound | NotificationMethodVibro;
+        }
+
+        plugin_state->notification_method = tmp_uint32;
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        if(!flipper_format_read_uint32(
+               fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_METHOD, &tmp_uint32, 1)) {
+            tmp_uint32 = AutomationMethodBadUsb;
+        }
+
+        plugin_state->automation_method = tmp_uint32;
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        if(!flipper_format_read_uint32(
+               fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, &tmp_uint32, 1)) {
+            tmp_uint32 = AutomationKeyboardLayoutQWERTY;
+        }
+
+        plugin_state->automation_kb_layout = tmp_uint32;
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
+        if(!flipper_format_read_uint32(fff_data_file, TOTP_CONFIG_KEY_FONT, &tmp_uint32, 1)) {
+            tmp_uint32 = 0;
+        }
+
+        plugin_state->active_font_index = tmp_uint32;
+
+        plugin_state->config_file_context = malloc(sizeof(ConfigFileContext));
+        furi_check(plugin_state->config_file_context != NULL);
+        plugin_state->config_file_context->storage = storage;
+        plugin_state->config_file_context->config_file = fff_data_file;
+        plugin_state->config_file_context->token_info_iterator_context =
+            totp_token_info_iterator_alloc(
+                storage,
+                plugin_state->config_file_context->config_file,
+                &plugin_state->crypto_settings);
+        result = true;
+    } while(false);
+
+    furi_string_free(temp_str);
+    return result;
+}
+
+bool totp_config_file_update_crypto_signatures(const PluginState* plugin_state) {
+    FlipperFormat* config_file = plugin_state->config_file_context->config_file;
+    flipper_format_rewind(config_file);
+    bool update_result = false;
+    do {
+        uint32_t tmp_uint32 = plugin_state->crypto_settings.crypto_version;
+        if(!flipper_format_insert_or_update_uint32(
+               config_file, TOTP_CONFIG_KEY_CRYPTO_VERSION, &tmp_uint32, 1)) {
+            break;
+        }
+
+        tmp_uint32 = plugin_state->crypto_settings.crypto_key_slot;
+        if(!flipper_format_insert_or_update_uint32(
+               config_file, TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT, &tmp_uint32, 1)) {
+            break;
+        }
+
+        if(!flipper_format_insert_or_update_hex(
+               config_file,
+               TOTP_CONFIG_KEY_SALT,
+               &plugin_state->crypto_settings.salt[0],
+               CRYPTO_SALT_LENGTH)) {
+            break;
+        }
+
+        if(!flipper_format_insert_or_update_hex(
+               config_file,
+               TOTP_CONFIG_KEY_CRYPTO_VERIFY,
+               plugin_state->crypto_settings.crypto_verify_data,
+               plugin_state->crypto_settings.crypto_verify_data_length)) {
+            break;
+        }
+
+        if(!flipper_format_insert_or_update_bool(
+               config_file,
+               TOTP_CONFIG_KEY_PINSET,
+               &plugin_state->crypto_settings.pin_required,
+               1)) {
+            break;
+        }
+
+        update_result = true;
+    } while(false);
+
+    return update_result;
+}
+
+void totp_config_file_close(PluginState* const plugin_state) {
+    if(plugin_state->config_file_context == NULL) return;
+    totp_token_info_iterator_free(plugin_state->config_file_context->token_info_iterator_context);
+    totp_close_config_file(plugin_state->config_file_context->config_file);
+    free(plugin_state->config_file_context);
+    plugin_state->config_file_context = NULL;
+    totp_close_storage();
+}
+
+void totp_config_file_reset(PluginState* const plugin_state) {
+    totp_config_file_close(plugin_state);
+    Storage* storage = totp_open_storage();
+    storage_simply_remove(storage, CONFIG_FILE_PATH);
+    totp_close_storage();
+}
+
+bool totp_config_file_update_encryption(
+    PluginState* plugin_state,
+    uint8_t new_crypto_key_slot,
+    const uint8_t* new_pin,
+    uint8_t new_pin_length) {
+    FlipperFormat* config_file = plugin_state->config_file_context->config_file;
+    Stream* stream = flipper_format_get_raw_stream(config_file);
+    size_t original_offset = stream_tell(stream);
+    if(!stream_rewind(stream)) {
+        return false;
+    }
+
+    if(!totp_crypto_check_key_slot(new_crypto_key_slot)) {
+        return false;
+    }
+
+    CryptoSettings old_crypto_settings = plugin_state->crypto_settings;
+
+    memset(&plugin_state->crypto_settings.iv[0], 0, CRYPTO_IV_LENGTH);
+    memset(&plugin_state->crypto_settings.salt[0], 0, CRYPTO_SALT_LENGTH);
+    if(plugin_state->crypto_settings.crypto_verify_data != NULL) {
+        free(plugin_state->crypto_settings.crypto_verify_data);
+        plugin_state->crypto_settings.crypto_verify_data = NULL;
+    }
+
+    plugin_state->crypto_settings.crypto_key_slot = new_crypto_key_slot;
+    plugin_state->crypto_settings.crypto_version = CRYPTO_LATEST_VERSION;
+
+    CryptoSeedIVResult seed_result = totp_crypto_seed_iv(
+        &plugin_state->crypto_settings, new_pin_length > 0 ? new_pin : NULL, new_pin_length);
+    if(seed_result & CryptoSeedIVResultFlagSuccess &&
+       seed_result & CryptoSeedIVResultFlagNewCryptoVerifyData &&
+       !totp_config_file_update_crypto_signatures(plugin_state)) {
+        return false;
+    } else if(seed_result == CryptoSeedIVResultFailed) {
+        return false;
+    }
+
+    char buffer[sizeof(TOTP_CONFIG_KEY_TOKEN_SECRET) + 1];
+    bool result = true;
+
+    while(true) {
+        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)) {
+            result = false;
+            break;
+        }
+
+        if(strncmp(buffer, "\n" TOTP_CONFIG_KEY_TOKEN_SECRET ":", sizeof(buffer)) == 0) {
+            uint32_t secret_bytes_count;
+            if(!flipper_format_get_value_count(
+                   config_file, TOTP_CONFIG_KEY_TOKEN_SECRET, &secret_bytes_count)) {
+                secret_bytes_count = 0;
+            }
+
+            if(secret_bytes_count > 1) {
+                size_t secret_token_start = stream_tell(stream) + 1;
+                uint8_t* encrypted_token = malloc(secret_bytes_count);
+                furi_check(encrypted_token != NULL);
+
+                if(!flipper_format_read_hex(
+                       config_file,
+                       TOTP_CONFIG_KEY_TOKEN_SECRET,
+                       encrypted_token,
+                       secret_bytes_count)) {
+                    result = false;
+                    free(encrypted_token);
+                    break;
+                }
+
+                size_t plain_token_length;
+                uint8_t* plain_token = totp_crypto_decrypt(
+                    encrypted_token, secret_bytes_count, &old_crypto_settings, &plain_token_length);
+
+                free(encrypted_token);
+                size_t encrypted_token_length;
+                encrypted_token = totp_crypto_encrypt(
+                    plain_token,
+                    plain_token_length,
+                    &plugin_state->crypto_settings,
+                    &encrypted_token_length);
+
+                memset_s(plain_token, plain_token_length, 0, plain_token_length);
+                free(plain_token);
+
+                if(!stream_seek(stream, secret_token_start, StreamOffsetFromStart)) {
+                    result = false;
+                    free(encrypted_token);
+                    break;
+                }
+
+                if(!flipper_format_write_hex(
+                       config_file,
+                       TOTP_CONFIG_KEY_TOKEN_SECRET,
+                       encrypted_token,
+                       encrypted_token_length)) {
+                    free(encrypted_token);
+                    result = false;
+                    break;
+                }
+
+                free(encrypted_token);
+            }
+        }
+    }
+
+    stream_seek(stream, original_offset, StreamOffsetFromStart);
+
+    return result;
+}
+
+bool totp_config_file_ensure_latest_encryption(
+    PluginState* plugin_state,
+    const uint8_t* pin,
+    uint8_t pin_length) {
+    bool result = true;
+    if(plugin_state->crypto_settings.crypto_version < CRYPTO_LATEST_VERSION) {
+        FURI_LOG_I(LOGGING_TAG, "Migration to crypto v%d is needed", CRYPTO_LATEST_VERSION);
+        char* backup_path = totp_config_file_backup(plugin_state);
+        if(backup_path != NULL) {
+            free(backup_path);
+            uint8_t crypto_key_slot = plugin_state->crypto_settings.crypto_key_slot;
+            if(!totp_crypto_check_key_slot(crypto_key_slot)) {
+                crypto_key_slot = DEFAULT_CRYPTO_KEY_SLOT;
+            }
+
+            result =
+                totp_config_file_update_encryption(plugin_state, crypto_key_slot, pin, pin_length);
+            FURI_LOG_I(
+                LOGGING_TAG,
+                "Migration to crypto v%d is done. Result: %d",
+                CRYPTO_LATEST_VERSION,
+                result);
+        } else {
+            result = false;
+        }
+    }
+
+    return result;
+}
+
+TokenInfoIteratorContext* totp_config_get_token_iterator_context(const PluginState* plugin_state) {
+    return plugin_state->config_file_context->token_info_iterator_context;
+}

+ 112 - 0
totp/services/config/config.h

@@ -0,0 +1,112 @@
+#pragma once
+
+#include "../../types/plugin_state.h"
+#include "../../types/token_info.h"
+#include "config_file_context.h"
+#include "constants.h"
+#include "token_info_iterator.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef uint8_t TotpConfigFileOpenResult;
+typedef uint8_t TotpConfigFileUpdateResult;
+
+/**
+ * @brief Tries to take a config file backup
+ * @param plugin_state application state
+ * @return backup path if backup successfully taken; \c NULL otherwise
+ */
+char* totp_config_file_backup(const PluginState* plugin_state);
+
+/**
+ * @brief Loads basic information from an application config file into application state without loading all the tokens
+ * @param plugin_state application state
+ * @return Config file open result
+ */
+bool totp_config_file_load(PluginState* const plugin_state);
+
+/**
+ * @brief Updates timezone offset in an application config file
+ * @param plugin_state application state
+ * @return Config file update result
+ */
+bool totp_config_file_update_timezone_offset(const PluginState* plugin_state);
+
+/**
+ * @brief Updates notification method in an application config file
+ * @param plugin_state application state
+ * @return Config file update result
+ */
+bool totp_config_file_update_notification_method(const PluginState* plugin_state);
+
+/**
+ * @brief Updates automation method in an application config file
+ * @param plugin_state application state
+ * @return Config file update result
+ */
+bool totp_config_file_update_automation_method(const PluginState* plugin_state);
+
+/**
+ * @brief Updates application user settings
+ * @param plugin_state application state
+ * @return Config file update result
+ */
+bool totp_config_file_update_user_settings(const PluginState* plugin_state);
+
+/**
+ * @brief Updates crypto signatures information
+ * @param plugin_state application state
+ * @return Config file update result
+ */
+bool totp_config_file_update_crypto_signatures(const PluginState* plugin_state);
+
+/**
+ * @brief Reset all the settings to default
+ * @param plugin_state application state
+ */
+void totp_config_file_reset(PluginState* const plugin_state);
+
+/**
+ * @brief Closes config file and releases all the resources
+ * @param plugin_state application state
+ */
+void totp_config_file_close(PluginState* const plugin_state);
+
+/**
+ * @brief Updates config file encryption by re-encrypting it using new user's PIN and new randomly generated IV
+ * @param plugin_state application state
+ * @param new_crypto_key_slot new crypto key slot to be used
+ * @param new_pin new user's PIN
+ * @param new_pin_length new user's PIN length
+ * @return \c true if config file encryption successfully updated; \c false otherwise
+ */
+bool totp_config_file_update_encryption(
+    PluginState* plugin_state,
+    uint8_t new_crypto_key_slot,
+    const uint8_t* new_pin,
+    uint8_t new_pin_length);
+
+/**
+ * @brief Ensures application config file uses latest encryption and upgrades encryption if needed
+ * @param plugin_state application state
+ * @param pin user's PIN
+ * @param pin_length user's PIN length
+ * @return \c true if operation succeeded; \c false otherwise
+ */
+bool totp_config_file_ensure_latest_encryption(
+    PluginState* plugin_state,
+    const uint8_t* pin,
+    uint8_t pin_length);
+
+/**
+ * @brief Gets token info iterator context
+ * @param plugin_state application state
+ * @return token info iterator context
+ */
+TokenInfoIteratorContext* totp_config_get_token_iterator_context(const PluginState* plugin_state);
+
+#ifdef __cplusplus
+}
+#endif

+ 3 - 0
totp/services/config/config_file_context.h

@@ -0,0 +1,3 @@
+#pragma once
+
+typedef struct ConfigFileContext ConfigFileContext;

+ 26 - 0
totp/services/config/constants.h

@@ -0,0 +1,26 @@
+#pragma once
+
+#include <storage/storage.h>
+
+#define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/totp")
+#define CONFIG_FILE_HEADER "Flipper TOTP plugin config file"
+#define CONFIG_FILE_ACTUAL_VERSION (10)
+
+#define TOTP_CONFIG_KEY_TIMEZONE "Timezone"
+#define TOTP_CONFIG_KEY_TOKEN_NAME "TokenName"
+#define TOTP_CONFIG_KEY_TOKEN_SECRET "TokenSecret"
+#define TOTP_CONFIG_KEY_TOKEN_ALGO "TokenAlgo"
+#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"
+#define TOTP_CONFIG_KEY_NOTIFICATION_METHOD "NotificationMethod"
+#define TOTP_CONFIG_KEY_AUTOMATION_METHOD "AutomationMethod"
+#define TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT "AutomationKbLayout"
+#define TOTP_CONFIG_KEY_FONT "Font"
+#define TOTP_CONFIG_KEY_CRYPTO_VERSION "CryptoVersion"
+#define TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT "CryptoKeySlot"

+ 219 - 0
totp/services/config/migrations/common_migration.c

@@ -0,0 +1,219 @@
+#include "common_migration.h"
+#include "../constants.h"
+#include "../../../types/token_info.h"
+#include "../../../types/automation_kb_layout.h"
+#include <flipper_format/flipper_format_i.h>
+
+#define TOTP_OLD_CONFIG_KEY_BASE_IV "BaseIV"
+
+bool totp_config_migrate_to_latest(
+    FlipperFormat* fff_data_file,
+    FlipperFormat* fff_backup_data_file) {
+    FuriString* temp_str = furi_string_alloc();
+    uint32_t current_version = 0;
+    bool result = false;
+    do {
+        flipper_format_write_header_cstr(
+            fff_data_file, CONFIG_FILE_HEADER, CONFIG_FILE_ACTUAL_VERSION);
+
+        if(!flipper_format_read_header(fff_backup_data_file, temp_str, &current_version)) {
+            break;
+        }
+
+        if(flipper_format_read_string(
+               fff_backup_data_file, TOTP_CONFIG_KEY_CRYPTO_VERSION, temp_str)) {
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_CRYPTO_VERSION, temp_str);
+        } else {
+            uint32_t old_crypto_version = 1;
+            flipper_format_write_uint32(
+                fff_data_file, TOTP_CONFIG_KEY_CRYPTO_VERSION, &old_crypto_version, 1);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(
+               fff_backup_data_file, TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT, temp_str)) {
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT, temp_str);
+        } else {
+            uint32_t default_old_key_slot = 2;
+            flipper_format_write_uint32(
+                fff_data_file, TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT, &default_old_key_slot, 1);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_SALT, temp_str)) {
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_SALT, temp_str);
+        } else if(
+            flipper_format_rewind(fff_backup_data_file) &&
+            flipper_format_read_string(
+                fff_backup_data_file, TOTP_OLD_CONFIG_KEY_BASE_IV, temp_str)) {
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_SALT, temp_str);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(
+               fff_backup_data_file, TOTP_CONFIG_KEY_CRYPTO_VERIFY, temp_str)) {
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_CRYPTO_VERIFY, temp_str);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_TIMEZONE, temp_str)) {
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TIMEZONE, temp_str);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_PINSET, temp_str)) {
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_PINSET, temp_str);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(
+               fff_backup_data_file, TOTP_CONFIG_KEY_NOTIFICATION_METHOD, temp_str)) {
+            flipper_format_write_string(
+                fff_data_file, TOTP_CONFIG_KEY_NOTIFICATION_METHOD, temp_str);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(
+               fff_backup_data_file, TOTP_CONFIG_KEY_AUTOMATION_METHOD, temp_str)) {
+            flipper_format_write_string(
+                fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_METHOD, temp_str);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(fff_backup_data_file, TOTP_CONFIG_KEY_FONT, temp_str)) {
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_FONT, temp_str);
+        } else {
+            uint32_t default_font_index = 0;
+            flipper_format_write_uint32(
+                fff_data_file, TOTP_CONFIG_KEY_FONT, &default_font_index, 1);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        if(flipper_format_read_string(
+               fff_backup_data_file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, temp_str)) {
+            flipper_format_write_string(
+                fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, temp_str);
+        } else {
+            uint32_t default_automation_kb_layout = AutomationKeyboardLayoutQWERTY;
+            flipper_format_write_uint32(
+                fff_data_file,
+                TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT,
+                &default_automation_kb_layout,
+                1);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
+        while(true) {
+            if(!flipper_format_read_string(
+                   fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_NAME, temp_str)) {
+                break;
+            }
+
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_NAME, temp_str);
+
+            flipper_format_read_string(
+                fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_SECRET, temp_str);
+            flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_SECRET, temp_str);
+
+            if(current_version > 1) {
+                flipper_format_read_string(
+                    fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_ALGO, temp_str);
+
+                if(current_version < 5) {
+                    uint32_t algo_as_uint32t = TokenHashAlgoDefault;
+                    if(furi_string_cmpi_str(temp_str, TOKEN_HASH_ALGO_SHA256_NAME) == 0) {
+                        algo_as_uint32t = TokenHashAlgoSha256;
+                    } else if(furi_string_cmpi_str(temp_str, TOKEN_HASH_ALGO_SHA512_NAME) == 0) {
+                        algo_as_uint32t = TokenHashAlgoSha512;
+                    } else if(furi_string_cmpi_str(temp_str, TOKEN_HASH_ALGO_STEAM_NAME) == 0) {
+                        algo_as_uint32t = TokenHashAlgoSteam;
+                    }
+
+                    flipper_format_write_uint32(
+                        fff_data_file, TOTP_CONFIG_KEY_TOKEN_ALGO, &algo_as_uint32t, 1);
+                } else {
+                    flipper_format_write_string(
+                        fff_data_file, TOTP_CONFIG_KEY_TOKEN_ALGO, temp_str);
+                }
+
+                flipper_format_read_string(
+                    fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_DIGITS, temp_str);
+                flipper_format_write_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_DIGITS, temp_str);
+            } else {
+                const uint32_t default_algo = TokenHashAlgoDefault;
+                flipper_format_write_uint32(
+                    fff_data_file, TOTP_CONFIG_KEY_TOKEN_ALGO, &default_algo, 1);
+                const uint32_t default_digits = TokenDigitsCountSix;
+                flipper_format_write_uint32(
+                    fff_data_file, TOTP_CONFIG_KEY_TOKEN_DIGITS, &default_digits, 1);
+            }
+
+            if(current_version > 2) {
+                flipper_format_read_string(
+                    fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_DURATION, temp_str);
+                flipper_format_write_string(
+                    fff_data_file, TOTP_CONFIG_KEY_TOKEN_DURATION, temp_str);
+            } else {
+                const uint32_t default_duration = TokenDurationDefault;
+                flipper_format_write_uint32(
+                    fff_data_file, TOTP_CONFIG_KEY_TOKEN_DURATION, &default_duration, 1);
+            }
+
+            if(current_version > 3) {
+                flipper_format_read_string(
+                    fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_AUTOMATION_FEATURES, temp_str);
+                flipper_format_write_string(
+                    fff_data_file, TOTP_CONFIG_KEY_TOKEN_AUTOMATION_FEATURES, temp_str);
+            } else {
+                const uint32_t default_automation_features = TokenAutomationFeatureNone;
+                flipper_format_write_uint32(
+                    fff_data_file,
+                    TOTP_CONFIG_KEY_TOKEN_AUTOMATION_FEATURES,
+                    &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);
+        size_t current_pos = stream_tell(stream);
+        size_t total_size = stream_size(stream);
+        if(current_pos < total_size) {
+            stream_delete(stream, total_size - current_pos);
+        }
+
+        result = true;
+    } while(false);
+
+    furi_string_free(temp_str);
+    return result;
+}

+ 13 - 0
totp/services/config/migrations/common_migration.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <flipper_format/flipper_format.h>
+
+/**
+ * @brief Migrates config file to the latest version
+ * @param fff_data_file original config file to be migrated
+ * @param fff_backup_data_file backup copy of original config file
+ * @return \c true if operation succeeded; \c false otherwise
+ */
+bool totp_config_migrate_to_latest(
+    FlipperFormat* fff_data_file,
+    FlipperFormat* fff_backup_data_file);

+ 628 - 0
totp/services/config/token_info_iterator.c

@@ -0,0 +1,628 @@
+#include "token_info_iterator.h"
+
+#include <flipper_format/flipper_format_i.h>
+#include <flipper_format/flipper_format_stream.h>
+#include <toolbox/stream/file_stream.h>
+#include "../../types/common.h"
+#include "../../types/crypto_settings.h"
+
+#define CONFIG_FILE_PART_FILE_PATH CONFIG_FILE_DIRECTORY_PATH "/totp.conf.part"
+#define STREAM_COPY_BUFFER_SIZE (128)
+
+struct TokenInfoIteratorContext {
+    size_t total_count;
+    size_t current_index;
+    size_t last_seek_offset;
+    size_t last_seek_index;
+    TokenInfo* current_token;
+    FlipperFormat* config_file;
+    CryptoSettings* crypto_settings;
+    Storage* storage;
+};
+
+static bool
+    flipper_format_seek_to_siblinig_token_start(Stream* stream, StreamDirection direction) {
+    char buffer[sizeof(TOTP_CONFIG_KEY_TOKEN_NAME) + 1];
+    bool found = false;
+    while(!found) {
+        if(!stream_seek_to_char(stream, '\n', direction)) {
+            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_NAME ":", sizeof(buffer)) == 0) {
+            found = true;
+        }
+    }
+
+    return found;
+}
+
+static bool seek_to_token(size_t token_index, TokenInfoIteratorContext* context) {
+    furi_check(context != NULL && context->config_file != NULL);
+    if(token_index >= context->total_count) {
+        return false;
+    }
+
+    Stream* stream = flipper_format_get_raw_stream(context->config_file);
+    long token_index_diff = (long)token_index - (long)context->last_seek_index;
+    size_t token_index_diff_weight = (size_t)labs(token_index_diff);
+    StreamDirection direction = token_index_diff >= 0 ? StreamDirectionForward :
+                                                        StreamDirectionBackward;
+    if(token_index_diff_weight > token_index || context->last_seek_offset == 0) {
+        context->last_seek_offset = 0;
+        context->last_seek_index = 0;
+        token_index_diff = token_index + 1;
+        direction = StreamDirectionForward;
+    } else if(token_index_diff_weight > (context->total_count - token_index - 1)) {
+        context->last_seek_offset = stream_size(stream);
+        context->last_seek_index = context->total_count - 1;
+        token_index_diff = -(long)(context->total_count - token_index);
+        direction = StreamDirectionBackward;
+    }
+
+    if(!stream_seek(stream, context->last_seek_offset, StreamOffsetFromStart)) {
+        return false;
+    }
+
+    if(token_index_diff != 0) {
+        long i = 0;
+        long i_inc = token_index_diff >= 0 ? 1 : -1;
+        do {
+            if(!flipper_format_seek_to_siblinig_token_start(stream, direction)) {
+                break;
+            }
+
+            i += i_inc;
+        } while((i_inc > 0 && i < token_index_diff) || (i_inc < 0 && i > token_index_diff));
+
+        if((i_inc > 0 && i < token_index_diff) || (i_inc < 0 && i > token_index_diff)) {
+            context->last_seek_offset = 0;
+            FURI_LOG_D(LOGGING_TAG, "Was not able to move");
+            return false;
+        }
+
+        context->last_seek_offset = stream_tell(stream);
+        context->last_seek_index = token_index;
+    }
+
+    return true;
+}
+
+static bool stream_insert_stream(Stream* dst, Stream* src) {
+    uint8_t buffer[STREAM_COPY_BUFFER_SIZE];
+    size_t buffer_read_size;
+    while((buffer_read_size = stream_read(src, buffer, sizeof(buffer))) != 0) {
+        if(!stream_insert(dst, buffer, buffer_read_size)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static bool ensure_stream_ends_with_lf(Stream* stream) {
+    uint8_t last_char;
+    size_t original_pos = stream_tell(stream);
+    if(!stream_seek(stream, -1, StreamOffsetFromEnd) || stream_read(stream, &last_char, 1) < 1) {
+        return false;
+    }
+
+    const uint8_t lf = '\n';
+    if(last_char != lf && !stream_write(stream, &lf, 1)) {
+        return false;
+    }
+
+    if(!stream_seek(stream, original_pos, StreamOffsetFromStart)) {
+        return false;
+    }
+
+    return true;
+}
+
+static bool
+    totp_token_info_iterator_save_current_token_info_changes(TokenInfoIteratorContext* context) {
+    bool is_new_token = context->current_index >= context->total_count;
+    Stream* stream = flipper_format_get_raw_stream(context->config_file);
+    if(is_new_token) {
+        if(!ensure_stream_ends_with_lf(stream) ||
+           !flipper_format_seek_to_end(context->config_file)) {
+            return false;
+        }
+    } else {
+        if(!seek_to_token(context->current_index, context)) {
+            return false;
+        }
+    }
+
+    size_t offset_start = stream_tell(stream);
+
+    size_t offset_end;
+    if(is_new_token) {
+        offset_end = offset_start;
+    } else if(context->current_index + 1 >= context->total_count) {
+        offset_end = stream_size(stream);
+    } else if(seek_to_token(context->current_index + 1, context)) {
+        offset_end = stream_tell(stream);
+    } else {
+        return false;
+    }
+
+    FlipperFormat* temp_ff = flipper_format_file_alloc(context->storage);
+    if(!flipper_format_file_open_always(temp_ff, CONFIG_FILE_PART_FILE_PATH)) {
+        flipper_format_free(temp_ff);
+        return false;
+    }
+
+    TokenInfo* token_info = context->current_token;
+    bool result = false;
+
+    do {
+        if(!flipper_format_write_string(temp_ff, TOTP_CONFIG_KEY_TOKEN_NAME, token_info->name)) {
+            break;
+        }
+
+        if(!flipper_format_write_hex(
+               temp_ff,
+               TOTP_CONFIG_KEY_TOKEN_SECRET,
+               token_info->token,
+               token_info->token_length)) {
+            break;
+        }
+
+        uint32_t tmp_uint32 = token_info->algo;
+        if(!flipper_format_write_uint32(temp_ff, TOTP_CONFIG_KEY_TOKEN_ALGO, &tmp_uint32, 1)) {
+            break;
+        }
+
+        tmp_uint32 = token_info->digits;
+        if(!flipper_format_write_uint32(temp_ff, TOTP_CONFIG_KEY_TOKEN_DIGITS, &tmp_uint32, 1)) {
+            break;
+        }
+
+        tmp_uint32 = token_info->duration;
+        if(!flipper_format_write_uint32(temp_ff, TOTP_CONFIG_KEY_TOKEN_DURATION, &tmp_uint32, 1)) {
+            break;
+        }
+
+        tmp_uint32 = token_info->automation_features;
+        if(!flipper_format_write_uint32(
+               temp_ff, TOTP_CONFIG_KEY_TOKEN_AUTOMATION_FEATURES, &tmp_uint32, 1)) {
+            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)) {
+            break;
+        }
+
+        if(!stream_seek(stream, offset_start, StreamOffsetFromStart)) {
+            break;
+        }
+
+        if(offset_end != offset_start && !stream_delete(stream, offset_end - offset_start)) {
+            break;
+        }
+
+        if(!is_new_token && !stream_write_char(stream, '\n')) {
+            break;
+        }
+
+        if(!stream_insert_stream(stream, temp_stream)) {
+            break;
+        }
+
+        if(is_new_token) {
+            context->total_count++;
+        }
+
+        result = true;
+    } while(false);
+
+    flipper_format_free(temp_ff);
+    storage_common_remove(context->storage, CONFIG_FILE_PART_FILE_PATH);
+
+    stream_seek(stream, offset_start, StreamOffsetFromStart);
+    context->last_seek_offset = offset_start;
+    context->last_seek_index = context->current_index;
+
+    return result;
+}
+
+TokenInfoIteratorContext* totp_token_info_iterator_alloc(
+    Storage* storage,
+    FlipperFormat* config_file,
+    CryptoSettings* crypto_settings) {
+    Stream* stream = flipper_format_get_raw_stream(config_file);
+    stream_rewind(stream);
+    size_t tokens_count = 0;
+    while(true) {
+        if(!flipper_format_seek_to_siblinig_token_start(stream, StreamDirectionForward)) {
+            break;
+        }
+
+        tokens_count++;
+    }
+
+    TokenInfoIteratorContext* context = malloc(sizeof(TokenInfoIteratorContext));
+    furi_check(context != NULL);
+
+    context->total_count = tokens_count;
+    context->current_token = token_info_alloc();
+    context->config_file = config_file;
+    context->crypto_settings = crypto_settings;
+    context->storage = storage;
+    return context;
+}
+
+void totp_token_info_iterator_free(TokenInfoIteratorContext* context) {
+    if(context == NULL) return;
+    token_info_free(context->current_token);
+    free(context);
+}
+
+bool totp_token_info_iterator_remove_current_token_info(TokenInfoIteratorContext* context) {
+    if(!seek_to_token(context->current_index, context)) {
+        return false;
+    }
+
+    Stream* stream = flipper_format_get_raw_stream(context->config_file);
+    size_t begin_offset = stream_tell(stream);
+    size_t end_offset;
+    if(!ensure_stream_ends_with_lf(stream)) {
+        return false;
+    }
+
+    if(context->current_index >= context->total_count - 1) {
+        end_offset = stream_size(stream) - 1;
+    } else if(seek_to_token(context->current_index + 1, context)) {
+        end_offset = stream_tell(stream);
+    } else {
+        return false;
+    }
+
+    if(!stream_seek(stream, begin_offset, StreamOffsetFromStart) ||
+       !stream_delete(stream, end_offset - begin_offset)) {
+        return false;
+    }
+
+    context->total_count--;
+    if(context->current_index >= context->total_count) {
+        context->current_index = context->total_count - 1;
+    }
+
+    return true;
+}
+
+bool totp_token_info_iterator_move_current_token_info(
+    TokenInfoIteratorContext* context,
+    size_t new_index) {
+    if(context->current_index == new_index) return true;
+
+    Stream* stream = flipper_format_get_raw_stream(context->config_file);
+
+    if(!ensure_stream_ends_with_lf(stream)) {
+        return false;
+    }
+
+    if(!seek_to_token(context->current_index, context)) {
+        return false;
+    }
+
+    size_t begin_offset = stream_tell(stream);
+    size_t end_offset;
+    if(context->current_index >= context->total_count - 1) {
+        end_offset = stream_size(stream) - 1;
+    } else if(seek_to_token(context->current_index + 1, context)) {
+        end_offset = stream_tell(stream);
+    } else {
+        return false;
+    }
+
+    Stream* temp_stream = file_stream_alloc(context->storage);
+    if(!file_stream_open(
+           temp_stream, CONFIG_FILE_PART_FILE_PATH, FSAM_READ_WRITE, FSOM_CREATE_ALWAYS)) {
+        stream_free(temp_stream);
+        return false;
+    }
+
+    size_t moving_size = end_offset - begin_offset;
+
+    bool result = false;
+    do {
+        if(!stream_seek(stream, begin_offset, StreamOffsetFromStart)) {
+            break;
+        }
+
+        if(stream_copy(stream, temp_stream, moving_size) < moving_size) {
+            break;
+        }
+
+        if(!stream_rewind(temp_stream)) {
+            break;
+        }
+
+        if(!stream_seek(stream, begin_offset, StreamOffsetFromStart)) {
+            break;
+        }
+
+        if(!stream_delete(stream, moving_size)) {
+            break;
+        }
+
+        context->last_seek_offset = 0;
+        context->last_seek_index = 0;
+        if(new_index >= context->total_count - 1) {
+            if(!stream_seek(stream, stream_size(stream) - 1, StreamOffsetFromStart)) {
+                break;
+            }
+        } else if(!seek_to_token(new_index, context)) {
+            break;
+        }
+
+        result = stream_insert_stream(stream, temp_stream);
+    } while(false);
+
+    stream_free(temp_stream);
+    storage_common_remove(context->storage, CONFIG_FILE_PART_FILE_PATH);
+
+    context->last_seek_offset = 0;
+    context->last_seek_index = 0;
+
+    return result;
+}
+
+TotpIteratorUpdateTokenResult totp_token_info_iterator_update_current_token(
+    TokenInfoIteratorContext* context,
+    TOTP_ITERATOR_UPDATE_TOKEN_ACTION update,
+    const void* update_context) {
+    TotpIteratorUpdateTokenResult result = update(context->current_token, update_context);
+    if(result == TotpIteratorUpdateTokenResultSuccess) {
+        if(!totp_token_info_iterator_save_current_token_info_changes(context)) {
+            result = TotpIteratorUpdateTokenResultFileUpdateFailed;
+        }
+
+        return result;
+    }
+
+    totp_token_info_iterator_go_to(context, context->current_index);
+    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,
+    const void* update_context) {
+    size_t previous_index = context->current_index;
+    context->current_index = context->total_count;
+    token_info_set_defaults(context->current_token);
+    TotpIteratorUpdateTokenResult result = update(context->current_token, update_context);
+    if(result == TotpIteratorUpdateTokenResultSuccess &&
+       !totp_token_info_iterator_save_current_token_info_changes(context)) {
+        result = TotpIteratorUpdateTokenResultFileUpdateFailed;
+    }
+
+    if(result != TotpIteratorUpdateTokenResultSuccess) {
+        totp_token_info_iterator_go_to(context, previous_index);
+    }
+
+    return result;
+}
+
+bool totp_token_info_iterator_go_to(TokenInfoIteratorContext* context, size_t token_index) {
+    furi_check(context != NULL);
+    context->current_index = token_index;
+    if(!seek_to_token(context->current_index, context)) {
+        return false;
+    }
+
+    Stream* stream = flipper_format_get_raw_stream(context->config_file);
+    size_t original_offset = stream_tell(stream);
+
+    if(!flipper_format_read_string(
+           context->config_file, TOTP_CONFIG_KEY_TOKEN_NAME, context->current_token->name)) {
+        stream_seek(stream, original_offset, StreamOffsetFromStart);
+        return false;
+    }
+
+    uint32_t secret_bytes_count;
+    if(!flipper_format_get_value_count(
+           context->config_file, TOTP_CONFIG_KEY_TOKEN_SECRET, &secret_bytes_count)) {
+        secret_bytes_count = 0;
+    }
+    TokenInfo* tokenInfo = context->current_token;
+    bool token_update_needed = false;
+    if(tokenInfo->token != NULL) {
+        free(tokenInfo->token);
+        tokenInfo->token_length = 0;
+    }
+
+    if(secret_bytes_count == 1) { // Plain secret key
+        FuriString* temp_str = furi_string_alloc();
+
+        if(flipper_format_read_string(
+               context->config_file, TOTP_CONFIG_KEY_TOKEN_SECRET, temp_str)) {
+            if(token_info_set_secret(
+                   tokenInfo,
+                   furi_string_get_cstr(temp_str),
+                   furi_string_size(temp_str),
+                   PlainTokenSecretEncodingBase32,
+                   context->crypto_settings)) {
+                FURI_LOG_W(
+                    LOGGING_TAG,
+                    "Token \"%s\" has plain secret",
+                    furi_string_get_cstr(tokenInfo->name));
+                token_update_needed = true;
+            } else {
+                tokenInfo->token = NULL;
+                tokenInfo->token_length = 0;
+                FURI_LOG_W(
+                    LOGGING_TAG,
+                    "Token \"%s\" has invalid secret",
+                    furi_string_get_cstr(tokenInfo->name));
+            }
+        } else {
+            tokenInfo->token = NULL;
+            tokenInfo->token_length = 0;
+        }
+
+        furi_string_free(temp_str);
+    } else { // encrypted
+        tokenInfo->token_length = secret_bytes_count;
+        if(secret_bytes_count > 0) {
+            tokenInfo->token = malloc(tokenInfo->token_length);
+            furi_check(tokenInfo->token != NULL);
+            if(!flipper_format_read_hex(
+                   context->config_file,
+                   TOTP_CONFIG_KEY_TOKEN_SECRET,
+                   tokenInfo->token,
+                   tokenInfo->token_length)) {
+                free(tokenInfo->token);
+                tokenInfo->token = NULL;
+                tokenInfo->token_length = 0;
+            }
+        } else {
+            tokenInfo->token = NULL;
+        }
+    }
+
+    uint32_t temp_data32;
+    if(!flipper_format_read_uint32(
+           context->config_file, TOTP_CONFIG_KEY_TOKEN_ALGO, &temp_data32, 1) ||
+       !token_info_set_algo_from_int(tokenInfo, temp_data32)) {
+        tokenInfo->algo = TokenHashAlgoDefault;
+    }
+
+    if(!flipper_format_read_uint32(
+           context->config_file, TOTP_CONFIG_KEY_TOKEN_DIGITS, &temp_data32, 1) ||
+       !token_info_set_digits_from_int(tokenInfo, temp_data32)) {
+        tokenInfo->digits = TokenDigitsCountSix;
+    }
+
+    if(!flipper_format_read_uint32(
+           context->config_file, TOTP_CONFIG_KEY_TOKEN_DURATION, &temp_data32, 1) ||
+       !token_info_set_duration_from_int(tokenInfo, temp_data32)) {
+        tokenInfo->duration = TokenDurationDefault;
+    }
+
+    if(flipper_format_read_uint32(
+           context->config_file, TOTP_CONFIG_KEY_TOKEN_AUTOMATION_FEATURES, &temp_data32, 1)) {
+        tokenInfo->automation_features = temp_data32;
+    } else {
+        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)) {
+        return false;
+    }
+
+    return true;
+}
+
+const TokenInfo*
+    totp_token_info_iterator_get_current_token(const TokenInfoIteratorContext* context) {
+    return context->current_token;
+}
+
+size_t totp_token_info_iterator_get_current_token_index(const TokenInfoIteratorContext* context) {
+    return context->current_index;
+}
+
+size_t totp_token_info_iterator_get_total_count(const TokenInfoIteratorContext* context) {
+    return context->total_count;
+}
+
+void totp_token_info_iterator_attach_to_config_file(
+    TokenInfoIteratorContext* context,
+    FlipperFormat* 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);
+}

+ 139 - 0
totp/services/config/token_info_iterator.h

@@ -0,0 +1,139 @@
+#pragma once
+
+#include "../../types/token_info.h"
+#include <flipper_format/flipper_format.h>
+#include "constants.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef int TotpIteratorUpdateTokenResult;
+
+typedef TotpIteratorUpdateTokenResult (
+    *TOTP_ITERATOR_UPDATE_TOKEN_ACTION)(TokenInfo* const token_info, const void* context);
+
+typedef struct TokenInfoIteratorContext TokenInfoIteratorContext;
+
+enum TotpIteratorUpdateTokenResults {
+
+    /**
+     * @brief Token successfully updated
+     */
+    TotpIteratorUpdateTokenResultSuccess = 0,
+
+    /**
+     * @brief An error ocurred during updating config file
+     */
+    TotpIteratorUpdateTokenResultFileUpdateFailed = -1
+};
+
+/**
+ * @brief Initializes a new token info iterator
+ * @param storage storage reference
+ * @param config_file config file to use
+ * @param crypto_settings crypto settings
+ * @return Token info iterator context
+ */
+TokenInfoIteratorContext* totp_token_info_iterator_alloc(
+    Storage* storage,
+    FlipperFormat* config_file,
+    CryptoSettings* crypto_settings);
+
+/**
+ * @brief Navigates iterator to the token with given index
+ * @param context token info iterator context
+ * @param token_index token index to navigate to
+ * @return \c true if navigation succeeded; \c false otherwise
+ */
+bool totp_token_info_iterator_go_to(TokenInfoIteratorContext* context, size_t token_index);
+
+/**
+ * @brief Moves current token to a given new index
+ * @param context token info iterator context
+ * @param new_index new token index to move current token to
+ * @return \c true if operation succeeded; \c false otherwise
+ */
+bool totp_token_info_iterator_move_current_token_info(
+    TokenInfoIteratorContext* context,
+    size_t new_index);
+
+/**
+ * @brief Updates current token info using given update action
+ * @param context token info iterator context
+ * @param update action which is responsible to make all the necessary updates to token info
+ * @param update_context update action context
+ * @return \c true if operation succeeded; \c false otherwise
+ */
+TotpIteratorUpdateTokenResult totp_token_info_iterator_update_current_token(
+    TokenInfoIteratorContext* context,
+    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
+ * @param update action which is responsible to make all the necessary updates to token info
+ * @param update_context update action context
+ * @return \c true if operation succeeded; \c false otherwise
+ */
+TotpIteratorUpdateTokenResult totp_token_info_iterator_add_new_token(
+    TokenInfoIteratorContext* context,
+    TOTP_ITERATOR_UPDATE_TOKEN_ACTION update,
+    const void* update_context);
+
+/**
+ * @brief Remvoves current token info
+ * @param context token info iterator context
+ * @return \c true if operation succeeded; \c false otherwise
+ */
+bool totp_token_info_iterator_remove_current_token_info(TokenInfoIteratorContext* context);
+
+/**
+ * @brief Disposes token info iterator and releases all the resources
+ * @param context token info iterator context
+ */
+void totp_token_info_iterator_free(TokenInfoIteratorContext* context);
+
+/**
+ * @brief Gets current token info
+ * @param context token info iterator context
+ * @return current token info
+ */
+const TokenInfo*
+    totp_token_info_iterator_get_current_token(const TokenInfoIteratorContext* context);
+
+/**
+ * @brief Gets current token info index
+ * @param context token info iterator context
+ * @return current token info index
+ */
+size_t totp_token_info_iterator_get_current_token_index(const TokenInfoIteratorContext* context);
+
+/**
+ * @brief Gets total amount of token infos found
+ * @param context token info iterator context
+ * @return amount of token infos found
+ */
+size_t totp_token_info_iterator_get_total_count(const TokenInfoIteratorContext* context);
+
+/**
+ * @brief Attaches token info iterator to another config file
+ * @param context token info iterator context
+ * @param config_file config file reference to attach token info iterator to
+ */
+void totp_token_info_iterator_attach_to_config_file(
+    TokenInfoIteratorContext* context,
+    FlipperFormat* config_file);
+
+#ifdef __cplusplus
+}
+#endif

+ 4 - 0
totp/services/convert/convert.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#define CONVERT_DIGIT_TO_CHAR(digit) ((digit) + '0')
+#define CONVERT_CHAR_TO_DIGIT(ch) ((ch) - '0')

+ 23 - 0
totp/services/crypto/common_types.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <stdint.h>
+
+typedef uint8_t CryptoSeedIVResult;
+
+enum CryptoSeedIVResults {
+
+    /**
+     * @brief IV seeding operation failed
+     */
+    CryptoSeedIVResultFailed = 0b00,
+
+    /**
+     * @brief IV seeding operation succeeded
+     */
+    CryptoSeedIVResultFlagSuccess = 0b01,
+
+    /**
+     * @brief As a part of IV seeding operation new crypto verify data has been generated
+     */
+    CryptoSeedIVResultFlagNewCryptoVerifyData = 0b10
+};

+ 14 - 0
totp/services/crypto/constants.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "polyfills.h"
+
+#define CRYPTO_IV_LENGTH (16)
+#define CRYPTO_SALT_LENGTH (16)
+
+// According to this explanation: https://github.com/flipperdevices/flipperzero-firmware/issues/2885#issuecomment-1646664666
+// disabling usage of any key which is "the same across all devices"
+#define ACCEPTABLE_CRYPTO_KEY_SLOT_START FURI_HAL_CRYPTO_ENCLAVE_USER_KEY_SLOT_START
+#define ACCEPTABLE_CRYPTO_KEY_SLOT_END FURI_HAL_CRYPTO_ENCLAVE_USER_KEY_SLOT_END
+
+#define DEFAULT_CRYPTO_KEY_SLOT ACCEPTABLE_CRYPTO_KEY_SLOT_START
+#define CRYPTO_LATEST_VERSION (3)

+ 118 - 0
totp/services/crypto/crypto_facade.c

@@ -0,0 +1,118 @@
+#include "crypto_facade.h"
+#include "../../config/app/config.h"
+#include <furi_hal_crypto.h>
+#include <furi/core/check.h>
+#ifdef TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+#include "crypto_v1.h"
+#endif
+#ifdef TOTP_OBSOLETE_CRYPTO_V2_COMPATIBILITY_ENABLED
+#include "crypto_v2.h"
+#endif
+#include "crypto_v3.h"
+#include "constants.h"
+
+bool totp_crypto_check_key_slot(uint8_t key_slot) {
+    uint8_t empty_iv[CRYPTO_IV_LENGTH] = {0};
+    if(key_slot < ACCEPTABLE_CRYPTO_KEY_SLOT_START || key_slot > ACCEPTABLE_CRYPTO_KEY_SLOT_END) {
+        return false;
+    }
+
+    return furi_hal_crypto_enclave_ensure_key(key_slot) &&
+           furi_hal_crypto_enclave_load_key(key_slot, empty_iv) &&
+           furi_hal_crypto_enclave_unload_key(key_slot);
+}
+
+uint8_t* totp_crypto_encrypt(
+    const uint8_t* plain_data,
+    const size_t plain_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* encrypted_data_length) {
+#ifdef TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+    if(crypto_settings->crypto_version == 1) {
+        return totp_crypto_encrypt_v1(
+            plain_data, plain_data_length, crypto_settings, encrypted_data_length);
+    }
+#endif
+
+#ifdef TOTP_OBSOLETE_CRYPTO_V2_COMPATIBILITY_ENABLED
+    if(crypto_settings->crypto_version == 2) {
+        return totp_crypto_encrypt_v2(
+            plain_data, plain_data_length, crypto_settings, encrypted_data_length);
+    }
+#endif
+
+    if(crypto_settings->crypto_version == 3) {
+        return totp_crypto_encrypt_v3(
+            plain_data, plain_data_length, crypto_settings, encrypted_data_length);
+    }
+
+    furi_crash("Unsupported crypto version");
+}
+
+uint8_t* totp_crypto_decrypt(
+    const uint8_t* encrypted_data,
+    const size_t encrypted_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* decrypted_data_length) {
+#ifdef TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+    if(crypto_settings->crypto_version == 1) {
+        return totp_crypto_decrypt_v1(
+            encrypted_data, encrypted_data_length, crypto_settings, decrypted_data_length);
+    }
+#endif
+
+#ifdef TOTP_OBSOLETE_CRYPTO_V2_COMPATIBILITY_ENABLED
+    if(crypto_settings->crypto_version == 2) {
+        return totp_crypto_decrypt_v2(
+            encrypted_data, encrypted_data_length, crypto_settings, decrypted_data_length);
+    }
+#endif
+
+    if(crypto_settings->crypto_version == 3) {
+        return totp_crypto_decrypt_v3(
+            encrypted_data, encrypted_data_length, crypto_settings, decrypted_data_length);
+    }
+
+    furi_crash("Unsupported crypto version");
+}
+
+CryptoSeedIVResult
+    totp_crypto_seed_iv(CryptoSettings* crypto_settings, const uint8_t* pin, uint8_t pin_length) {
+#ifdef TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+    if(crypto_settings->crypto_version == 1) {
+        return totp_crypto_seed_iv_v1(crypto_settings, pin, pin_length);
+    }
+#endif
+
+#ifdef TOTP_OBSOLETE_CRYPTO_V2_COMPATIBILITY_ENABLED
+    if(crypto_settings->crypto_version == 2) {
+        return totp_crypto_seed_iv_v2(crypto_settings, pin, pin_length);
+    }
+#endif
+
+    if(crypto_settings->crypto_version == 3) {
+        return totp_crypto_seed_iv_v3(crypto_settings, pin, pin_length);
+    }
+
+    furi_crash("Unsupported crypto version");
+}
+
+bool totp_crypto_verify_key(const CryptoSettings* crypto_settings) {
+#ifdef TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+    if(crypto_settings->crypto_version == 1) {
+        return totp_crypto_verify_key_v1(crypto_settings);
+    }
+#endif
+
+#ifdef TOTP_OBSOLETE_CRYPTO_V2_COMPATIBILITY_ENABLED
+    if(crypto_settings->crypto_version == 2) {
+        return totp_crypto_verify_key_v2(crypto_settings);
+    }
+#endif
+
+    if(crypto_settings->crypto_version == 3) {
+        return totp_crypto_verify_key_v3(crypto_settings);
+    }
+
+    furi_crash("Unsupported crypto version");
+}

+ 67 - 0
totp/services/crypto/crypto_facade.h

@@ -0,0 +1,67 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stddef.h>
+#include "../../types/crypto_settings.h"
+#include "common_types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Checks whether key slot can be used for encryption purposes
+ * @param key_slot key slot index
+ * @return \c true if key slot can be used for encryption; \c false otherwise
+ */
+bool totp_crypto_check_key_slot(uint8_t key_slot);
+
+/**
+ * @brief Encrypts plain data using built-in certificate and given initialization vector (IV)
+ * @param plain_data plain data to be encrypted
+ * @param plain_data_length plain data length
+ * @param crypto_settings crypto settings
+ * @param[out] encrypted_data_length encrypted data length
+ * @return Encrypted data
+ */
+uint8_t* totp_crypto_encrypt(
+    const uint8_t* plain_data,
+    const size_t plain_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* encrypted_data_length);
+
+/**
+ * @brief Decrypts encrypted data using built-in certificate and given initialization vector (IV)
+ * @param encrypted_data encrypted data to be decrypted
+ * @param encrypted_data_length encrypted data length
+ * @param crypto_settings crypto settings
+ * @param[out] decrypted_data_length decrypted data length
+ * @return Decrypted data
+ */
+uint8_t* totp_crypto_decrypt(
+    const uint8_t* encrypted_data,
+    const size_t encrypted_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* decrypted_data_length);
+
+/**
+ * @brief Seed initialization vector (IV) using user's PIN
+ * @param crypto_settings crypto settings
+ * @param pin user's PIN
+ * @param pin_length user's PIN length
+ * @return Results of seeding IV
+ */
+CryptoSeedIVResult
+    totp_crypto_seed_iv(CryptoSettings* crypto_settings, const uint8_t* pin, uint8_t pin_length);
+
+/**
+ * @brief Verifies whether cryptographic information (certificate + IV) is valid and can be used for encryption and decryption
+ * @param crypto_settings crypto settings
+ * @return \c true if cryptographic information is valid; \c false otherwise
+ */
+bool totp_crypto_verify_key(const CryptoSettings* crypto_settings);
+
+#ifdef __cplusplus
+}
+#endif

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů