Kaynağa Gözat

* Refactoring

* Added `timezone` CLI command
* FAQ & README updated
alex.kopachov 3 yıl önce
ebeveyn
işleme
7b584be4ca

+ 1 - 1
.github/conf-file_description.md

@@ -1,6 +1,6 @@
 # Flipper Authenticator config file description
 
-By default Flipper Authenticator will store all its settings in [`/ext/apps/Misc/totp.conf`](https://github.com/akopachov/flipper-zero_authenticator/blob/master/totp/services/config/config.c#:~:text=%23define%20CONFIG_FILE_DIRECTORY_PATH,totp.conf%22) file.
+By default Flipper Authenticator stores all its settings in [`/ext/apps/Misc/totp.conf`](https://github.com/akopachov/flipper-zero_authenticator/blob/master/totp/services/config/config.c#:~:text=%23define%20CONFIG_FILE_DIRECTORY_PATH,totp.conf%22) file.
 
 File format is standard for Flipper Zero device. Each line has one setting identified by key, where key and value are separated by `:` symbol.
 

+ 26 - 1
FAQ.md

@@ -24,10 +24,35 @@ At first start app will create new config file (default location is [`/ext/apps/
 
 Detailed description of file format can be found [here](.github/conf-file_description.md)
 
+## Is there a CLI?
+
+**YES!**
+When Flipper Authenticator is running `totp` CLI command will be available for you to list, add or remove tokens.
+
 ## How to change\recover PIN?
 
-There is no way to change or recover PIN once it is set without loosing all the token secrets. If you would like to completely reset app settings including PIN and all the tokens - just delete `/ext/apps/Misc/totp.conf` file. Flipper Authenticator will create new empty file and you will be able to setup everything from scratch.
+For now there is no way to change or recover PIN once it is set without loosing all the token secrets. If you would like to completely reset app settings including PIN and all the tokens - just delete `/ext/apps/Misc/totp.conf` file. Flipper Authenticator will create new empty file and you will be able to setup everything from scratch.
 
 ## How to backup?
 
 All token secrets are stored in encrypted form and are tied to an original Flipper device and PIN. Given that, there is no sense to try to backup `/ext/apps/Misc/totp.conf` file as it will not help you in situation when you loose your Flipper device. Instead use your favorite password manager to store plain token secrets and\or any other information which will help you recover your accounts.
+
+## Flipper Authenticator generates invalid tokens, why so?
+
+There are multiple reasons why Flipper Authenticator generates invalid tokens:
+
+### Clock is not precise
+
+Flipper Zero clock has known clock drift problem. So there is a chance that clock on your flipper device is just not precise and you just need to sync them by using desktop\mobile [qFlipper application](https://flipperzero.one/update).
+
+### Timezone is not correct
+
+Because of Flipper Zero API doesn't provide an access to timezone offset it is necessary to set it manually for correct TOTP tokens generation. You may find you timezone offset (or another name is "UTC offset") [here](https://www.timeanddate.com/time/zone/timezone/utc) or on any other website found in google. Then set it either in [conf file](.github/conf-file_description.md) or via setting menu of Flipper Authenticator.
+
+### Token secret is not correct
+
+Sometimes it is possible that you just made a mistake while typing or copying token secret. Some providers (ex. Amazon) shows token secrets as a grouped string (ex. `XXXX ZZZZ YYYY NNNN MMMM`), it is fine to use such a string if you are entering token secret manually or using CLI, **however** it is not allowed to use such a string to copy&paste it straight to a config file. In such a scenario you need to drop all the whitespaces from your token secret before putting it into config file.
+
+### Token hashing algorithm is not correct
+
+In majority of situation using default hashing algorithm `SHA1` should work just fine. But for some rare providers it might be necessary to use `SHA256` or `SHA512` as a hashinig alogorithm.

+ 17 - 18
totp/services/cli/cli.c

@@ -5,8 +5,10 @@
 #include "commands/list/list.h"
 #include "commands/add/add.h"
 #include "commands/delete/delete.h"
+#include "commands/timezone/timezone.h"
 
 #define TOTP_CLI_COMMAND_NAME "totp"
+#define TOTP_CLI_COMMAND_HELP "help"
 
 static void totp_cli_print_unknown_command(FuriString* unknown_command) {
     printf("Command \"%s\" is unknown. Use \"help\" command to get list of available commands.", furi_string_get_cstr(unknown_command));
@@ -14,18 +16,13 @@ static void totp_cli_print_unknown_command(FuriString* unknown_command) {
 
 static void totp_cli_print_help() {
     printf("Usage:\r\n");
-    printf("totp <command> <arguments>\r\n");
+    printf(TOTP_CLI_COMMAND_NAME " <command> <arguments>\r\n");
     printf("Command list:\r\n");
-    printf("\thelp - print command usage help\r\n");
-    printf("\tlist - list all tokens\r\n");
-    printf("\tdelete <INDEX> [-f] - delete token\r\n");
-    printf("\t\t<INDEX> - token index in the list\r\n");
-    printf("\t\t-f - [OPTIONAL] force command to do not ask user for interactive confirmation\r\n");
-    printf("\tadd <NAME> <SECRET> [-a <ALGO>] [-d <DIGITS>] - add new token\r\n");
-    printf("\t\t<NAME>   - token name\r\n");
-    printf("\t\t<SECRET> - Base32 token secret\r\n");
-    printf("\t\t<ALGO>   - [OPTIONAL] token hashing algorithm, could be one of: sha1, sha256, sha512; default: sha1\r\n");
-    printf("\t\t<DIGITS> - [OPTIONAL] number of digits to generate, one of: 6, 8; default: 6\r\n\r\n");
+    printf("\t" TOTP_CLI_COMMAND_HELP " - print command usage help\r\n\r\n");
+    totp_cli_command_list_print_help();
+    totp_cli_command_delete_print_help();
+    totp_cli_command_add_print_help();
+    totp_cli_command_timezone_print_help();
 }
 
 static void totp_cli_print_unauthenticated() {
@@ -51,14 +48,16 @@ static void totp_cli_handler(Cli* cli, FuriString* args, void* context) {
 
     args_read_string_and_trim(args, cmd);
 
-    if(furi_string_cmp_str(cmd, "help") == 0 || furi_string_empty(cmd)) {
+    if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_HELP) == 0 || furi_string_empty(cmd)) {
         totp_cli_print_help();
-    } else if(furi_string_cmp_str(cmd, "add") == 0) {
-        totp_cli_handle_add_command(plugin_state, args);
-    } else if(furi_string_cmp_str(cmd, "list") == 0) {
-        totp_cli_handle_list_command(plugin_state);
-    } else if(furi_string_cmp_str(cmd, "delete") == 0) {
-        totp_cli_handle_delete_command(plugin_state, args, cli);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_ADD) == 0) {
+        totp_cli_command_add_handle(plugin_state, args);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_LIST) == 0) {
+        totp_cli_command_list_handle(plugin_state);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DELETE) == 0) {
+        totp_cli_command_delete_handle(plugin_state, args, cli);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_TIMEZONE) == 0) {
+        totp_cli_command_timezone_handle(plugin_state, args);
     } else {
         totp_cli_print_unknown_command(cmd);
     }

+ 10 - 0
totp/services/cli/cli_common_helpers.h

@@ -1,3 +1,13 @@
 #pragma once
 
+#define TOTP_CLI_ARG(arg) "<" arg ">"
+#define TOTP_CLI_OPTIONAL_PARAM(param) "[" param "]"
+#define TOTP_CLI_OPTIONAL_PARAM_MARK "[OPTIONAL]"
+
+#define TOTP_CLI_PRINTF(format, ...) \
+    _Pragma(STRINGIFY(GCC diagnostic push)); \
+    _Pragma(STRINGIFY(GCC diagnostic ignored "-Wdouble-promotion")); \
+    printf(format, ##__VA_ARGS__); \
+    _Pragma(STRINGIFY(GCC diagnostic pop));
+
 void totp_cli_print_invalid_arguments();

+ 24 - 9
totp/services/cli/commands/add/add.c

@@ -7,6 +7,13 @@
 #include "../../cli_common_helpers.h"
 #include "../../../../scenes/scene_director.h"
 
+#define TOTP_CLI_COMMAND_ADD_ARG_NAME "NAME"
+#define TOTP_CLI_COMMAND_ADD_ARG_SECRET "SECRET"
+#define TOTP_CLI_COMMAND_ADD_ARG_ALGO "ALGO"
+#define TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX "-a"
+#define TOTP_CLI_COMMAND_ADD_ARG_DIGITS "DIGITS"
+#define TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX "-d"
+
 static bool token_info_set_digits_from_str(TokenInfo* token_info, FuriString* str) {
     switch(furi_string_get_char(str, 0)) {
     case '6':
@@ -39,7 +46,15 @@ static bool token_info_set_algo_from_str(TokenInfo* token_info, FuriString* str)
     return false;
 }
 
-void totp_cli_handle_add_command(PluginState* plugin_state, FuriString* args) {
+void totp_cli_command_add_print_help() {
+    TOTP_CLI_PRINTF("\t" TOTP_CLI_COMMAND_ADD " " TOTP_CLI_ARG(TOTP_CLI_COMMAND_ADD_ARG_NAME) " " TOTP_CLI_ARG(TOTP_CLI_COMMAND_ADD_ARG_SECRET) " " TOTP_CLI_OPTIONAL_PARAM(TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX " " TOTP_CLI_ARG(TOTP_CLI_COMMAND_ADD_ARG_ALGO)) " " TOTP_CLI_OPTIONAL_PARAM(TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX " " TOTP_CLI_ARG(TOTP_CLI_COMMAND_ADD_ARG_DIGITS)) " - add new token\r\n");
+    TOTP_CLI_PRINTF("\t\t" TOTP_CLI_ARG(TOTP_CLI_COMMAND_ADD_ARG_NAME) "   - token name\r\n");
+    TOTP_CLI_PRINTF("\t\t" TOTP_CLI_ARG(TOTP_CLI_COMMAND_ADD_ARG_SECRET) " - Base32 token secret\r\n");
+    TOTP_CLI_PRINTF("\t\t" TOTP_CLI_ARG(TOTP_CLI_COMMAND_ADD_ARG_ALGO) "   - " TOTP_CLI_OPTIONAL_PARAM_MARK " token hashing algorithm, could be one of: sha1, sha256, sha512; default: sha1\r\n");
+    TOTP_CLI_PRINTF("\t\t" TOTP_CLI_ARG(TOTP_CLI_COMMAND_ADD_ARG_DIGITS) " - " TOTP_CLI_OPTIONAL_PARAM_MARK " number of digits to generate, one of: 6, 8; default: 6\r\n\r\n");
+}
+
+void totp_cli_command_add_handle(PluginState* plugin_state, FuriString* args) {
     FuriString* temp_str = furi_string_alloc();
     const char* temp_cstr;
 
@@ -67,7 +82,7 @@ void totp_cli_handle_add_command(PluginState* plugin_state, FuriString* args) {
 
     temp_cstr = furi_string_get_cstr(temp_str);
     if (!token_info_set_secret(token_info, temp_cstr, strlen(temp_cstr), plugin_state->iv)) {
-        printf("Token secret seems to be invalid and can not be parsed\r\n");
+        TOTP_CLI_PRINTF("Token secret seems to be invalid and can not be parsed\r\n");
         furi_string_free(temp_str);
         token_info_free(token_info);
         return;
@@ -76,19 +91,19 @@ void totp_cli_handle_add_command(PluginState* plugin_state, FuriString* args) {
     // Read optional arguments
     while (args_read_string_and_trim(args, temp_str)) {
         bool parsed = false;
-        if (furi_string_cmpi_str(temp_str, "-a") == 0) {
+        if (furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX) == 0) {
             if (!args_read_string_and_trim(args, temp_str)) {
-                printf("Missed value for argument \"-a\"\r\n");
+                TOTP_CLI_PRINTF("Missed value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX "\"\r\n");
             } else if (!token_info_set_algo_from_str(token_info, temp_str)) {
-                printf("\"%s\" is incorrect value for argument \"-a\"\r\n", furi_string_get_cstr(temp_str));
+                TOTP_CLI_PRINTF("\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX "\"\r\n", furi_string_get_cstr(temp_str));
             } else {
                 parsed = true;
             }
-        } else if (furi_string_cmpi_str(temp_str, "-d") == 0) {
+        } else if (furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX) == 0) {
             if (!args_read_string_and_trim(args, temp_str)) {
-                printf("Missed value for argument \"-d\"\r\n");
+                TOTP_CLI_PRINTF("Missed value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX "\"\r\n");
             } else if (!token_info_set_digits_from_str(token_info, temp_str)) {
-                printf("\"%s\" is incorrect value for argument \"-d\"\r\n", furi_string_get_cstr(temp_str));
+                TOTP_CLI_PRINTF("\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX "\"\r\n", furi_string_get_cstr(temp_str));
             } else {
                 parsed = true;
             }
@@ -121,5 +136,5 @@ void totp_cli_handle_add_command(PluginState* plugin_state, FuriString* args) {
 
     furi_string_free(temp_str);
 
-    printf("Token \"%s\" has been successfully added\r\n", token_info->name);
+    TOTP_CLI_PRINTF("Token \"%s\" has been successfully added\r\n", token_info->name);
 }

+ 4 - 1
totp/services/cli/commands/add/add.h

@@ -3,4 +3,7 @@
 #include <cli/cli.h>
 #include "../../../../types/plugin_state.h"
 
-void totp_cli_handle_add_command(PluginState* plugin_state, FuriString* args);
+#define TOTP_CLI_COMMAND_ADD "add"
+
+void totp_cli_command_add_handle(PluginState* plugin_state, FuriString* args);
+void totp_cli_command_add_print_help();

+ 20 - 11
totp/services/cli/commands/delete/delete.c

@@ -8,7 +8,16 @@
 #include "../../cli_common_helpers.h"
 #include "../../../../scenes/scene_director.h"
 
-void totp_cli_handle_delete_command(PluginState* plugin_state, FuriString* args, Cli* cli) {
+#define TOTP_CLI_COMMAND_DELETE_ARG_INDEX "INDEX"
+#define TOTP_CLI_COMMAND_DELETE_ARG_FORCE_SUFFIX "-f"
+
+void totp_cli_command_delete_print_help() {
+    TOTP_CLI_PRINTF("\t" TOTP_CLI_COMMAND_DELETE " " TOTP_CLI_ARG(TOTP_CLI_COMMAND_DELETE_ARG_INDEX) " " TOTP_CLI_OPTIONAL_PARAM(TOTP_CLI_COMMAND_DELETE_ARG_FORCE_SUFFIX) " - delete token\r\n");
+    TOTP_CLI_PRINTF("\t\t" TOTP_CLI_ARG(TOTP_CLI_COMMAND_DELETE_ARG_INDEX) " - token index in the list\r\n");
+    TOTP_CLI_PRINTF("\t\t" TOTP_CLI_COMMAND_DELETE_ARG_FORCE_SUFFIX " - " TOTP_CLI_OPTIONAL_PARAM_MARK " force command to do not ask user for interactive confirmation\r\n\r\n");
+}
+
+void totp_cli_command_delete_handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
     int token_number;
     if (!args_read_int_and_trim(args, &token_number) || token_number <= 0 || token_number > plugin_state->tokens_count) {
         totp_cli_print_invalid_arguments();
@@ -18,10 +27,10 @@ void totp_cli_handle_delete_command(PluginState* plugin_state, FuriString* args,
     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, "-f") == 0) {
+        if (furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_DELETE_ARG_FORCE_SUFFIX) == 0) {
             confirm_needed = false;
         } else {
-            printf("Unknown argument \"%s\"\r\n", furi_string_get_cstr(temp_str));
+            TOTP_CLI_PRINTF("Unknown argument \"%s\"\r\n", furi_string_get_cstr(temp_str));
             totp_cli_print_invalid_arguments();
             furi_string_free(temp_str);
             return;
@@ -35,21 +44,21 @@ void totp_cli_handle_delete_command(PluginState* plugin_state, FuriString* args,
 
     bool confirmed = !confirm_needed;
     if (confirm_needed) {
-        printf("WARNING!\r\n");
-        printf("Token \"%s\" will be permanently deleted without ability to recover it.\r\n", token_info->name);
-        printf("Confirm? [y/n]\r\n");
+        TOTP_CLI_PRINTF("WARNING!\r\n");
+        TOTP_CLI_PRINTF("TOKEN \"%s\" WILL BE PERMANENTLY DELETED WITHOUT ABILITY TO RECOVER IT.\r\n", token_info->name);
+        TOTP_CLI_PRINTF("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 != 0x0d);
+        } while (user_pick != 'y' && user_pick != 'n' && user_pick != CliSymbolAsciiCR);
 
-        confirmed = user_pick == 'y' || user_pick == 0x0d;
+        confirmed = user_pick == 'y' || user_pick == CliSymbolAsciiCR;
     }
 
     if (confirmed) {
         bool activate_generate_token_scene = false;
-        if (plugin_state->current_scene == TotpSceneGenerateToken) {
+        if (plugin_state->current_scene != TotpSceneAuthentication) {
             totp_scene_director_activate_scene(plugin_state, TotpSceneNone, NULL);
             activate_generate_token_scene = true;
         }
@@ -63,9 +72,9 @@ void totp_cli_handle_delete_command(PluginState* plugin_state, FuriString* args,
             totp_scene_director_activate_scene(plugin_state, TotpSceneGenerateToken, NULL);
         }
 
-        printf("Token \"%s\" has been successfully deleted\r\n", token_info->name);
+        TOTP_CLI_PRINTF("Token \"%s\" has been successfully deleted\r\n", token_info->name);
         token_info_free(token_info);
     } else {
-        printf("User not confirmed\r\n");
+        TOTP_CLI_PRINTF("User not confirmed\r\n");
     }
 }

+ 4 - 1
totp/services/cli/commands/delete/delete.h

@@ -3,4 +3,7 @@
 #include <cli/cli.h>
 #include "../../../../types/plugin_state.h"
 
-void totp_cli_handle_delete_command(PluginState* plugin_state, FuriString* args, Cli* cli);
+#define TOTP_CLI_COMMAND_DELETE "delete"
+
+void totp_cli_command_delete_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+void totp_cli_command_delete_print_help();

+ 12 - 7
totp/services/cli/commands/list/list.c

@@ -4,6 +4,7 @@
 #include "../../../list/list.h"
 #include "../../../../types/token_info.h"
 #include "../../../config/constants.h"
+#include "../../cli_common_helpers.h"
 
 static char* get_algo_as_cstr(TokenHashAlgo algo) {
     switch(algo) {
@@ -29,24 +30,28 @@ static uint8_t get_digits_as_int(TokenDigitsCount digits) {
     return 6;
 }
 
-void totp_cli_handle_list_command(PluginState* plugin_state) {
+void totp_cli_command_list_print_help() {
+    TOTP_CLI_PRINTF("\t" TOTP_CLI_COMMAND_LIST " - list all tokens\r\n\r\n");
+}
+
+void totp_cli_command_list_handle(PluginState* plugin_state) {
     if (plugin_state->tokens_list == NULL) {
-        printf("There are no tokens");
+        TOTP_CLI_PRINTF("There are no tokens");
         return;
     }
 
     ListNode* node = plugin_state->tokens_list;
 
-    printf("+-----+-----------------------------+--------+--------+\r\n");
-    printf("| %-*s | %-*s | %-*s | %-s |\r\n", 3, "#", 27, "Name", 6, "Algo", "Digits");
-    printf("+-----+-----------------------------+--------+--------+\r\n");
+    TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n");
+    TOTP_CLI_PRINTF("| %-*s | %-*s | %-*s | %-s |\r\n", 3, "#", 27, "Name", 6, "Algo", "Digits");
+    TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n");
     uint16_t index = 1;
     while(node != NULL) {
         TokenInfo* token_info = (TokenInfo* )node->data;
         token_info_get_digits_count(token_info);
-        printf("| %-3" PRIu16 " | %-27.27s | %-6s | %-6" PRIu8 " |\r\n", index, token_info->name, get_algo_as_cstr(token_info->algo), get_digits_as_int(token_info->digits));
+        TOTP_CLI_PRINTF("| %-3" PRIu16 " | %-27.27s | %-6s | %-6" PRIu8 " |\r\n", index, token_info->name, get_algo_as_cstr(token_info->algo), get_digits_as_int(token_info->digits));
         node = node->next;
         index++;
     }
-    printf("+-----+-----------------------------+--------+--------+\r\n");
+    TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n");
 }

+ 4 - 1
totp/services/cli/commands/list/list.h

@@ -2,4 +2,7 @@
 
 #include "../../../../types/plugin_state.h"
 
-void totp_cli_handle_list_command(PluginState* plugin_state);
+#define TOTP_CLI_COMMAND_LIST "list"
+
+void totp_cli_command_list_handle(PluginState* plugin_state);
+void totp_cli_command_list_print_help();

+ 36 - 0
totp/services/cli/commands/timezone/timezone.c

@@ -0,0 +1,36 @@
+#include "timezone.h"
+#include <lib/toolbox/args.h>
+#include "../../../config/config.h"
+#include "../../../../scenes/scene_director.h"
+#include "../../cli_common_helpers.h"
+
+#define TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE "TIMEZONE"
+
+void totp_cli_command_timezone_print_help() {
+    TOTP_CLI_PRINTF("\t" TOTP_CLI_COMMAND_TIMEZONE " " TOTP_CLI_OPTIONAL_PARAM(TOTP_CLI_ARG(TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE)) "\r\n");
+    TOTP_CLI_PRINTF("\t\t" TOTP_CLI_ARG(TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE) " - " TOTP_CLI_OPTIONAL_PARAM_MARK " timezone offset in hours to be set, if not provided then current timezone offset will be printed\r\n\r\n");
+}
+
+void totp_cli_command_timezone_handle(PluginState* plugin_state, FuriString* args) {
+    FuriString* temp_str = furi_string_alloc();
+    if (args_read_string_and_trim(args, temp_str)) {
+        float tz = strtof(furi_string_get_cstr(temp_str), NULL);
+        if (tz >= -12.75f && tz <= 12.75f) {
+            plugin_state->timezone_offset = tz;
+            totp_config_file_update_timezone_offset(tz);
+            TOTP_CLI_PRINTF("Timezone is set to %f\r\n", tz);
+            if (plugin_state->current_scene == TotpSceneGenerateToken) {
+                totp_scene_director_activate_scene(plugin_state, TotpSceneNone, NULL);
+                totp_scene_director_activate_scene(plugin_state, TotpSceneGenerateToken, NULL);
+            } else if (plugin_state->current_scene == TotpSceneAppSettings) {
+                totp_scene_director_activate_scene(plugin_state, TotpSceneNone, NULL);
+                totp_scene_director_activate_scene(plugin_state, TotpSceneAppSettings, NULL);
+            }
+        } else {
+            TOTP_CLI_PRINTF("Invalid timezone offset\r\n");
+        }
+    } else {
+        TOTP_CLI_PRINTF("Current timezone offset is %f\r\n", plugin_state->timezone_offset);
+    }
+    furi_string_free(temp_str);
+}

+ 9 - 0
totp/services/cli/commands/timezone/timezone.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../../types/plugin_state.h"
+
+#define TOTP_CLI_COMMAND_TIMEZONE "timezone"
+
+void totp_cli_command_timezone_handle(PluginState* plugin_state, FuriString* args);
+void totp_cli_command_timezone_print_help();