Przeglądaj źródła

move base pack here

MX 2 lat temu
commit
020928cbfb
100 zmienionych plików z 16340 dodań i 0 usunięć
  1. 21 0
      LICENSE
  2. 59 0
      application.fam
  3. 100 0
      cli/cli.c
  4. 19 0
      cli/cli.h
  5. 123 0
      cli/cli_helpers.c
  6. 116 0
      cli/cli_helpers.h
  7. 192 0
      cli/commands/add/add.c
  8. 17 0
      cli/commands/add/add.h
  9. 186 0
      cli/commands/automation/automation.c
  10. 15 0
      cli/commands/automation/automation.h
  11. 108 0
      cli/commands/delete/delete.c
  12. 16 0
      cli/commands/delete/delete.h
  13. 94 0
      cli/commands/details/details.c
  14. 14 0
      cli/commands/details/details.h
  15. 76 0
      cli/commands/help/help.c
  16. 14 0
      cli/commands/help/help.h
  17. 58 0
      cli/commands/list/list.c
  18. 14 0
      cli/commands/list/list.h
  19. 85 0
      cli/commands/move/move.c
  20. 15 0
      cli/commands/move/move.h
  21. 105 0
      cli/commands/notification/notification.c
  22. 14 0
      cli/commands/notification/notification.h
  23. 193 0
      cli/commands/pin/pin.c
  24. 14 0
      cli/commands/pin/pin.h
  25. 42 0
      cli/commands/reset/reset.c
  26. 13 0
      cli/commands/reset/reset.h
  27. 54 0
      cli/commands/timezone/timezone.c
  28. 15 0
      cli/commands/timezone/timezone.h
  29. 177 0
      cli/commands/update/update.c
  30. 14 0
      cli/commands/update/update.h
  31. 141 0
      cli/common_command_arguments.c
  32. 106 0
      cli/common_command_arguments.h
  33. 42 0
      config/app/config.h
  34. 34 0
      config/wolfssl/config.h
  35. BIN
      images/DolphinCommon_56x48.png
  36. BIN
      images/hid_ble_31x9.png
  37. BIN
      images/hid_usb_31x9.png
  38. BIN
      images/totp_arrow_left_8x9.png
  39. BIN
      images/totp_arrow_right_8x9.png
  40. 60 0
      lib/base32/base32.c
  41. 40 0
      lib/base32/base32.h
  42. 73 0
      lib/base64/base64.c
  43. 14 0
      lib/base64/base64.h
  44. 941 0
      lib/fonts/712serif/712serif.c
  45. 8 0
      lib/fonts/712serif/712serif.h
  46. 23 0
      lib/fonts/available_fonts.c
  47. 7 0
      lib/fonts/available_fonts.h
  48. 1057 0
      lib/fonts/bedstead/bedstead.c
  49. 8 0
      lib/fonts/bedstead/bedstead.h
  50. 1115 0
      lib/fonts/dpcomic/dpcomic.c
  51. 7 0
      lib/fonts/dpcomic/dpcomic.h
  52. 24 0
      lib/fonts/font_info.h
  53. 1173 0
      lib/fonts/funclimbing/funclimbing.c
  54. 7 0
      lib/fonts/funclimbing/funclimbing.h
  55. 941 0
      lib/fonts/graph35pix/graph35pix.c
  56. 8 0
      lib/fonts/graph35pix/graph35pix.h
  57. 1173 0
      lib/fonts/karma_future/karma_future.c
  58. 8 0
      lib/fonts/karma_future/karma_future.h
  59. 942 0
      lib/fonts/mode_nine/mode_nine.c
  60. 8 0
      lib/fonts/mode_nine/mode_nine.h
  61. 1115 0
      lib/fonts/pixelflag/pixelflag.c
  62. 7 0
      lib/fonts/pixelflag/pixelflag.h
  63. 1058 0
      lib/fonts/redhat_mono/redhat_mono.c
  64. 8 0
      lib/fonts/redhat_mono/redhat_mono.h
  65. 1057 0
      lib/fonts/zector/zector.c
  66. 8 0
      lib/fonts/zector/zector.h
  67. 22 0
      lib/polyfills/memset_s.c
  68. 23 0
      lib/polyfills/memset_s.h
  69. 11 0
      lib/polyfills/strnlen.c
  70. 6 0
      lib/polyfills/strnlen.h
  71. 28 0
      lib/roll_value/roll_value.c
  72. 59 0
      lib/roll_value/roll_value.h
  73. 16 0
      lib/timezone_utils/timezone_utils.c
  74. 18 0
      lib/timezone_utils/timezone_utils.h
  75. 1 0
      lib/wolfssl
  76. 728 0
      services/config/config.c
  77. 104 0
      services/config/config.h
  78. 3 0
      services/config/config_file_context.h
  79. 24 0
      services/config/constants.h
  80. 199 0
      services/config/migrations/common_migration.c
  81. 13 0
      services/config/migrations/common_migration.h
  82. 552 0
      services/config/token_info_iterator.c
  83. 123 0
      services/config/token_info_iterator.h
  84. 4 0
      services/convert/convert.h
  85. 23 0
      services/crypto/common_types.h
  86. 14 0
      services/crypto/constants.h
  87. 118 0
      services/crypto/crypto_facade.c
  88. 59 0
      services/crypto/crypto_facade.h
  89. 145 0
      services/crypto/crypto_v1.c
  90. 55 0
      services/crypto/crypto_v1.h
  91. 190 0
      services/crypto/crypto_v2.c
  92. 55 0
      services/crypto/crypto_v2.h
  93. 195 0
      services/crypto/crypto_v3.c
  94. 52 0
      services/crypto/crypto_v3.h
  95. 14 0
      services/crypto/polyfills.h
  96. 66 0
      services/idle_timeout/idle_timeout.c
  97. 44 0
      services/idle_timeout/idle_timeout.h
  98. 125 0
      services/totp/totp.c
  99. 55 0
      services/totp/totp.h
  100. BIN
      totp_10px.png

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Alexander Kopachov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 59 - 0
application.fam

@@ -0,0 +1,59 @@
+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="4.01",
+    fap_author="Alexander Kopachov (@akopachov)",
+    fap_description="Software-based TOTP 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_private_libs=[
+        Lib(
+            name="base32",
+        ),
+        Lib(
+            name="base64",
+        ),
+        Lib(
+            name="timezone_utils",
+        ),
+        Lib(
+            name="polyfills",
+        ),
+        Lib(
+            name="roll_value",
+        ),
+        Lib(
+            name="fonts",
+        ),
+        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"]
+        ),
+    ],
+)

+ 100 - 0
cli/cli.c

@@ -0,0 +1,100 @@
+// Original idea: https://github.com/br0ziliy
+
+#include "cli.h"
+#include <lib/toolbox/args.h>
+#include "cli_helpers.h"
+#include "commands/list/list.h"
+#include "commands/add/add.h"
+#include "commands/update/update.h"
+#include "commands/delete/delete.h"
+#include "commands/timezone/timezone.h"
+#include "commands/help/help.h"
+#include "commands/move/move.h"
+#include "commands/pin/pin.h"
+#include "commands/notification/notification.h"
+#include "commands/reset/reset.h"
+#include "commands/automation/automation.h"
+#include "commands/details/details.h"
+
+struct TotpCliContext {
+    PluginState* plugin_state;
+};
+
+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 totp_cli_handler(Cli* cli, FuriString* args, void* context) {
+    TotpCliContext* cli_context = context;
+    PluginState* plugin_state = cli_context->plugin_state;
+
+    FuriString* cmd = furi_string_alloc();
+
+    args_read_string_and_trim(args, cmd);
+
+    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)) {
+        totp_cli_command_help_handle();
+    } 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) {
+        totp_cli_command_add_handle(plugin_state, args, cli);
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_LIST) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_LIST_ALT) == 0) {
+        totp_cli_command_list_handle(plugin_state, cli);
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DELETE) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DELETE_ALT) == 0) {
+        totp_cli_command_delete_handle(plugin_state, args, cli);
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_TIMEZONE) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_TIMEZONE_ALT) == 0) {
+        totp_cli_command_timezone_handle(plugin_state, args, cli);
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_MOVE) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_MOVE_ALT) == 0) {
+        totp_cli_command_move_handle(plugin_state, args, cli);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_PIN) == 0) {
+        totp_cli_command_pin_handle(plugin_state, args, cli);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_NOTIFICATION) == 0) {
+        totp_cli_command_notification_handle(plugin_state, args, cli);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_AUTOMATION) == 0) {
+        totp_cli_command_automation_handle(plugin_state, args, cli);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_RESET) == 0) {
+        totp_cli_command_reset_handle(plugin_state, cli);
+    } else if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_UPDATE) == 0) {
+        totp_cli_command_update_handle(plugin_state, args, cli);
+    } else if(
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DETAILS) == 0 ||
+        furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DETAILS_ALT) == 0) {
+        totp_cli_command_details_handle(plugin_state, args, cli);
+    } else {
+        totp_cli_print_unknown_command(cmd);
+    }
+
+    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;
+    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);
+    furi_record_close(RECORD_CLI);
+    free(context);
+}

+ 19 - 0
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);

+ 123 - 0
cli/cli_helpers.c

@@ -0,0 +1,123 @@
+#include "cli_helpers.h"
+#include <cli/cli.h>
+#include <lib/toolbox/args.h>
+#include "../types/plugin_event.h"
+
+const char* TOTP_CLI_COLOR_ERROR = "91m";
+const char* TOTP_CLI_COLOR_WARNING = "93m";
+const char* TOTP_CLI_COLOR_SUCCESS = "92m";
+const char* TOTP_CLI_COLOR_INFO = "96m";
+
+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_print_invalid_arguments() {
+    TOTP_CLI_PRINTF_ERROR(
+        "Invalid command arguments. use \"help\" command to get list of available commands");
+}
+
+void totp_cli_print_error_updating_config_file() {
+    TOTP_CLI_PRINTF_ERROR("An error has occurred during updating config file\r\n");
+}
+
+void totp_cli_print_error_loading_token_info() {
+    TOTP_CLI_PRINTF_ERROR("An error has occurred during loading token information\r\n");
+}
+
+void totp_cli_print_processing() {
+    TOTP_CLI_PRINTF("Processing, please wait...\r\n");
+}
+
+void totp_cli_delete_last_char() {
+    TOTP_CLI_PRINTF("\b \b");
+    fflush(stdout);
+}
+
+void totp_cli_delete_current_line() {
+    TOTP_CLI_PRINTF("\33[2K\r");
+    fflush(stdout);
+}
+
+void totp_cli_delete_last_line() {
+    TOTP_CLI_PRINTF("\033[A\33[2K\r");
+    fflush(stdout);
+}

+ 116 - 0
cli/cli_helpers.h

@@ -0,0 +1,116 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../types/plugin_state.h"
+
+#define TOTP_CLI_COMMAND_NAME "totp"
+
+#define DOCOPT_ARGUMENT(arg) "<" arg ">"
+#define DOCOPT_MULTIPLE(arg) arg "..."
+#define DOCOPT_OPTIONAL(param) "[" param "]"
+#define DOCOPT_REQUIRED(param) "(" param ")"
+#define DOCOPT_OPTION(option, value) option " " value
+#define DOCOPT_SWITCH(option) option
+#define DOCOPT_OPTIONS "[options]"
+#define DOCOPT_DEFAULT(val) "[default: " val "]"
+
+extern const char* TOTP_CLI_COLOR_ERROR;
+extern const char* TOTP_CLI_COLOR_WARNING;
+extern const char* TOTP_CLI_COLOR_SUCCESS;
+extern const char* TOTP_CLI_COLOR_INFO;
+
+#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)
+
+/**
+ * @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 Deletes last printed line in console
+ */
+void totp_cli_delete_last_line();
+
+/**
+ * @brief Deletes current printed line in console
+ */
+void totp_cli_delete_current_line();
+
+/**
+ * @brief Deletes last printed char in console
+ */
+void totp_cli_delete_last_char();
+
+/**
+ * @brief Prints error message about invalid command arguments
+ */
+void totp_cli_print_invalid_arguments();
+
+/**
+ * @brief Prints error message about config file update error
+ */
+void totp_cli_print_error_updating_config_file();
+
+/**
+ * @brief Prints error message about config file loading error
+ */
+void totp_cli_print_error_loading_token_info();
+
+/**
+ * @brief Prints message to let user know that command is processing now
+ */
+void totp_cli_print_processing();

+ 192 - 0
cli/commands/add/add.c

@@ -0,0 +1,192 @@
+#include "add.h"
+#include <stdlib.h>
+#include <lib/toolbox/args.h>
+#include "../../../types/token_info.h"
+#include "../../../services/config/config.h"
+#include "../../../services/convert/convert.h"
+#include "../../cli_helpers.h"
+#include "../../../ui/scene_director.h"
+#include "../../common_command_arguments.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_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;
+}
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_add_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_ADD ", " TOTP_CLI_COMMAND_ADD_ALT
+                    ", " TOTP_CLI_COMMAND_ADD_ALT2 "     Add new token\r\n");
+}
+
+void totp_cli_command_add_docopt_usage() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_NAME
+        " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_ADD " | " TOTP_CLI_COMMAND_ADD_ALT " | " TOTP_CLI_COMMAND_ADD_ALT2) " " DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_NAME) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_ALGO_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_ALGO))) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_SECRET_ENCODING))) " " DOCOPT_OPTIONAL(
+            DOCOPT_OPTION(
+                TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX,
+                DOCOPT_ARGUMENT(
+                    TOTP_CLI_COMMAND_ARG_DIGITS))) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_DURATION_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_DURATION))) " " DOCOPT_OPTIONAL(DOCOPT_SWITCH(TOTP_CLI_COMMAND_ARG_UNSECURE_PREFIX)) " " DOCOPT_MULTIPLE(DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE)))) "\r\n");
+}
+
+void totp_cli_command_add_docopt_arguments() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_ARG_NAME "          Token name\r\n");
+}
+
+void totp_cli_command_add_docopt_options() {
+    TOTP_CLI_PRINTF("  " DOCOPT_OPTION(
+        TOTP_CLI_COMMAND_ARG_ALGO_PREFIX,
+        DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_ARG_ALGO)) "      Token hashing algorithm. Must be one of: " TOKEN_HASH_ALGO_SHA1_NAME
+                                        ", " TOKEN_HASH_ALGO_SHA256_NAME
+                                        ", " TOKEN_HASH_ALGO_SHA512_NAME
+                                        ", " TOKEN_HASH_ALGO_STEAM_NAME
+                                        " " DOCOPT_DEFAULT(TOKEN_HASH_ALGO_SHA1_NAME) "\r\n");
+    TOTP_CLI_PRINTF(
+        "  " DOCOPT_OPTION(
+            TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX,
+            DOCOPT_ARGUMENT(
+                TOTP_CLI_COMMAND_ARG_DIGITS)) "    Number of digits to generate, one of: %" PRIu8
+                                              ", %" PRIu8 ", %" PRIu8
+                                              " " DOCOPT_DEFAULT("%" PRIu8) "\r\n",
+        TokenDigitsCountFive,
+        TokenDigitsCountSix,
+        TokenDigitsCountEight,
+        TokenDigitsCountSix);
+
+    TOTP_CLI_PRINTF("  " DOCOPT_OPTION(
+        TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX,
+        DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_ARG_SECRET_ENCODING)) "  Token secret encoding, one of " PLAIN_TOKEN_ENCODING_BASE32_NAME
+                                                   ", " PLAIN_TOKEN_ENCODING_BASE64_NAME
+                                                   " " DOCOPT_DEFAULT(
+                                                       PLAIN_TOKEN_ENCODING_BASE32_NAME) "\r\n");
+
+    TOTP_CLI_PRINTF(
+        "  " DOCOPT_OPTION(
+            TOTP_CLI_COMMAND_ARG_DURATION_PREFIX,
+            DOCOPT_ARGUMENT(
+                TOTP_CLI_COMMAND_ARG_DURATION)) "  Token lifetime duration in seconds, between: %" PRIu8
+                                                " and %" PRIu8
+                                                " " DOCOPT_DEFAULT("%" PRIu8) "\r\n",
+        TokenDurationMin,
+        TokenDurationMax,
+        TokenDurationDefault);
+    TOTP_CLI_PRINTF("  " DOCOPT_SWITCH(
+        TOTP_CLI_COMMAND_ARG_UNSECURE_PREFIX) "             Show console user input as-is without masking\r\n");
+    TOTP_CLI_PRINTF("  " DOCOPT_OPTION(
+        TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX,
+        DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE)) "   Token automation features to be enabled. Must be one of: " TOKEN_AUTOMATION_FEATURE_NONE_NAME
+                                                      ", " TOKEN_AUTOMATION_FEATURE_ENTER_AT_THE_END_NAME
+                                                      ", " TOKEN_AUTOMATION_FEATURE_TAB_AT_THE_END_NAME
+                                                      " " DOCOPT_DEFAULT(
+                                                          TOKEN_AUTOMATION_FEATURE_NONE_NAME) "\r\n");
+    TOTP_CLI_PRINTF("                 # " TOKEN_AUTOMATION_FEATURE_NONE_NAME " - No features\r\n");
+    TOTP_CLI_PRINTF("                 # " TOKEN_AUTOMATION_FEATURE_ENTER_AT_THE_END_NAME
+                    " - Type <Enter> key at the end of token input automation\r\n");
+    TOTP_CLI_PRINTF("                 # " TOKEN_AUTOMATION_FEATURE_TAB_AT_THE_END_NAME
+                    " - Type <Tab> key at the end of token input automation\r\n");
+    TOTP_CLI_PRINTF("                 # " TOKEN_AUTOMATION_FEATURE_TYPE_SLOWER_NAME
+                    " - Type slower\r\n");
+}
+#endif
+
+void totp_cli_command_add_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);
+}

+ 17 - 0
cli/commands/add/add.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../config/app/config.h"
+#include "../../../types/plugin_state.h"
+
+#define TOTP_CLI_COMMAND_ADD "add"
+#define TOTP_CLI_COMMAND_ADD_ALT "mk"
+#define TOTP_CLI_COMMAND_ADD_ALT2 "new"
+
+void totp_cli_command_add_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_add_docopt_commands();
+void totp_cli_command_add_docopt_usage();
+void totp_cli_command_add_docopt_arguments();
+void totp_cli_command_add_docopt_options();
+#endif

+ 186 - 0
cli/commands/automation/automation.c

@@ -0,0 +1,186 @@
+#include "automation.h"
+#include <lib/toolbox/args.h>
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+#include "../../cli_helpers.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_ARG_KB_LAYOUT_PREFIX "-k"
+#define TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT "layout"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_automation_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_AUTOMATION "       Get or set automation settings\r\n");
+}
+
+void totp_cli_command_automation_docopt_usage() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_NAME " " TOTP_CLI_COMMAND_AUTOMATION " " DOCOPT_OPTIONAL(DOCOPT_OPTION(
+        TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT_PREFIX,
+        DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT))) " " DOCOPT_OPTIONAL(DOCOPT_MULTIPLE(DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_AUTOMATION_ARG_METHOD))) "\r\n");
+}
+
+void totp_cli_command_automation_docopt_arguments() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_AUTOMATION_ARG_METHOD
+        "    Automation method to be set. Must be one of: " TOTP_CLI_COMMAND_AUTOMATION_METHOD_NONE
+        ", " TOTP_CLI_COMMAND_AUTOMATION_METHOD_USB
+#ifdef TOTP_BADBT_AUTOMATION_ENABLED
+        ", " TOTP_CLI_COMMAND_AUTOMATION_METHOD_BT
+#endif
+        "\r\n");
+}
+
+void totp_cli_command_automation_docopt_options() {
+    TOTP_CLI_PRINTF("  " DOCOPT_OPTION(
+        TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT_PREFIX,
+        DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT)) "    Automation keyboard layout. Must be one of: " TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTY
+                                                        ", " TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_AZERTY
+                                                        "\r\n");
+}
+#endif
+
+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;
+    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 {
+        result = false;
+    }
+
+    return result;
+}
+
+void totp_cli_command_automation_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);
+}

+ 15 - 0
cli/commands/automation/automation.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_AUTOMATION "automation"
+
+void totp_cli_command_automation_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_automation_docopt_commands();
+void totp_cli_command_automation_docopt_usage();
+void totp_cli_command_automation_docopt_arguments();
+void totp_cli_command_automation_docopt_options();
+#endif

+ 108 - 0
cli/commands/delete/delete.c

@@ -0,0 +1,108 @@
+#include "delete.h"
+
+#include <stdlib.h>
+#include <ctype.h>
+#include <lib/toolbox/args.h>
+#include "../../../services/config/config.h"
+#include "../../cli_helpers.h"
+#include "../../../ui/scene_director.h"
+#include "../../common_command_arguments.h"
+
+#define TOTP_CLI_COMMAND_DELETE_ARG_FORCE_PREFIX "-f"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_delete_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_DELETE ", " TOTP_CLI_COMMAND_DELETE_ALT
+                    "       Delete existing token\r\n");
+}
+
+void totp_cli_command_delete_docopt_usage() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_NAME
+        " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_DELETE " | " TOTP_CLI_COMMAND_DELETE_ALT) " " DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_ARG_INDEX) " " DOCOPT_OPTIONAL(DOCOPT_SWITCH(TOTP_CLI_COMMAND_DELETE_ARG_FORCE_PREFIX)) "\r\n");
+}
+
+void totp_cli_command_delete_docopt_arguments() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_ARG_INDEX "         Token index in the list\r\n");
+}
+
+void totp_cli_command_delete_docopt_options() {
+    TOTP_CLI_PRINTF("  " DOCOPT_SWITCH(
+        TOTP_CLI_COMMAND_DELETE_ARG_FORCE_PREFIX) "             Force command to do not ask user for interactive confirmation\r\n");
+}
+#endif
+
+void totp_cli_command_delete_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);
+}

+ 16 - 0
cli/commands/delete/delete.h

@@ -0,0 +1,16 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_DELETE "delete"
+#define TOTP_CLI_COMMAND_DELETE_ALT "rm"
+
+void totp_cli_command_delete_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_delete_docopt_commands();
+void totp_cli_command_delete_docopt_usage();
+void totp_cli_command_delete_docopt_arguments();
+void totp_cli_command_delete_docopt_options();
+#endif

+ 94 - 0
cli/commands/details/details.c

@@ -0,0 +1,94 @@
+#include "details.h"
+#include <stdlib.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 "../../cli_helpers.h"
+#include "../../common_command_arguments.h"
+
+#define TOTP_CLI_PRINTF_AUTOMATION_FEATURE(description, header_printed) \
+    do {                                                                \
+        TOTP_CLI_PRINTF(                                                \
+            "| %-20s | %-28.28s |\r\n",                                 \
+            header_printed ? "" : "Automation features",                \
+            description);                                               \
+        header_printed = true;                                          \
+    } while(false)
+
+static void print_automation_features(const TokenInfo* token_info) {
+    if(token_info->automation_features == TokenAutomationFeatureNone) {
+        TOTP_CLI_PRINTF("| %-20s | %-28.28s |\r\n", "Automation features", "None");
+        return;
+    }
+
+    bool header_printed = false;
+    if(token_info->automation_features & TokenAutomationFeatureEnterAtTheEnd) {
+        TOTP_CLI_PRINTF_AUTOMATION_FEATURE("Type <Enter> key at the end", header_printed);
+    }
+
+    if(token_info->automation_features & TokenAutomationFeatureTabAtTheEnd) {
+        TOTP_CLI_PRINTF_AUTOMATION_FEATURE("Type <Tab> key at the end", header_printed);
+    }
+
+    if(token_info->automation_features & TokenAutomationFeatureTypeSlower) {
+        TOTP_CLI_PRINTF_AUTOMATION_FEATURE("Type slower", header_printed);
+    }
+}
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_details_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_DETAILS ", " TOTP_CLI_COMMAND_DETAILS_ALT
+                    "      Displays token details\r\n");
+}
+
+void totp_cli_command_details_docopt_usage() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_NAME " " DOCOPT_REQUIRED(
+        TOTP_CLI_COMMAND_DETAILS
+        " | " TOTP_CLI_COMMAND_DETAILS_ALT) " " DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_INDEX) "\r\n");
+}
+#endif
+
+void totp_cli_command_details_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;
+    }
+
+    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);
+
+        TOTP_CLI_PRINTF("+----------------------+------------------------------+\r\n");
+        TOTP_CLI_PRINTF("| %-20s | %-28s |\r\n", "Property", "Value");
+        TOTP_CLI_PRINTF("+----------------------+------------------------------+\r\n");
+        TOTP_CLI_PRINTF("| %-20s | %-28d |\r\n", "Index", token_number);
+        TOTP_CLI_PRINTF(
+            "| %-20s | %-28.28s |\r\n", "Name", furi_string_get_cstr(token_info->name));
+        TOTP_CLI_PRINTF(
+            "| %-20s | %-28s |\r\n", "Hashing algorithm", token_info_get_algo_as_cstr(token_info));
+        TOTP_CLI_PRINTF("| %-20s | %-28" PRIu8 " |\r\n", "Number of digits", token_info->digits);
+        TOTP_CLI_PRINTF(
+            "| %-20s | %" PRIu8 " sec.%-21s |\r\n", "Token lifetime", token_info->duration, " ");
+        print_automation_features(token_info);
+        TOTP_CLI_PRINTF("+----------------------+------------------------------+\r\n");
+    } 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);
+}

+ 14 - 0
cli/commands/details/details.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_DETAILS "lsattr"
+#define TOTP_CLI_COMMAND_DETAILS_ALT "cat"
+
+void totp_cli_command_details_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_details_docopt_commands();
+void totp_cli_command_details_docopt_usage();
+#endif

+ 76 - 0
cli/commands/help/help.c

@@ -0,0 +1,76 @@
+#include "help.h"
+#include "../../cli_helpers.h"
+#include "../add/add.h"
+#include "../update/update.h"
+#include "../delete/delete.h"
+#include "../list/list.h"
+#include "../timezone/timezone.h"
+#include "../move/move.h"
+#include "../pin/pin.h"
+#include "../notification/notification.h"
+#include "../reset/reset.h"
+#include "../automation/automation.h"
+#include "../details/details.h"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_help_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_HELP ", " TOTP_CLI_COMMAND_HELP_ALT
+                    ", " TOTP_CLI_COMMAND_HELP_ALT2 "       Show command usage help\r\n");
+}
+
+void totp_cli_command_help_docopt_usage() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_NAME " " DOCOPT_REQUIRED(
+        TOTP_CLI_COMMAND_HELP " | " TOTP_CLI_COMMAND_HELP_ALT
+                              " | " TOTP_CLI_COMMAND_HELP_ALT2) "\r\n");
+}
+#endif
+
+void totp_cli_command_help_handle() {
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+    TOTP_CLI_PRINTF("Usage:\r\n");
+    totp_cli_command_help_docopt_usage();
+    totp_cli_command_list_docopt_usage();
+    totp_cli_command_details_docopt_usage();
+    totp_cli_command_add_docopt_usage();
+    totp_cli_command_update_docopt_usage();
+    totp_cli_command_delete_docopt_usage();
+    totp_cli_command_timezone_docopt_usage();
+    totp_cli_command_move_docopt_usage();
+    totp_cli_command_pin_docopt_usage();
+    totp_cli_command_notification_docopt_usage();
+    totp_cli_command_reset_docopt_usage();
+    totp_cli_command_automation_docopt_usage();
+    cli_nl();
+    TOTP_CLI_PRINTF("Commands:\r\n");
+    totp_cli_command_help_docopt_commands();
+    totp_cli_command_list_docopt_commands();
+    totp_cli_command_details_docopt_commands();
+    totp_cli_command_add_docopt_commands();
+    totp_cli_command_update_docopt_commands();
+    totp_cli_command_delete_docopt_commands();
+    totp_cli_command_timezone_docopt_commands();
+    totp_cli_command_move_docopt_commands();
+    totp_cli_command_pin_docopt_commands();
+    totp_cli_command_notification_docopt_commands();
+    totp_cli_command_reset_docopt_commands();
+    totp_cli_command_automation_docopt_commands();
+    cli_nl();
+    TOTP_CLI_PRINTF("Arguments:\r\n");
+    totp_cli_command_add_docopt_arguments();
+    totp_cli_command_delete_docopt_arguments();
+    totp_cli_command_move_docopt_arguments();
+    totp_cli_command_timezone_docopt_arguments();
+    totp_cli_command_notification_docopt_arguments();
+    totp_cli_command_automation_docopt_arguments();
+    cli_nl();
+    TOTP_CLI_PRINTF("Options:\r\n");
+    totp_cli_command_add_docopt_options();
+    totp_cli_command_update_docopt_options();
+    totp_cli_command_delete_docopt_options();
+    totp_cli_command_pin_docopt_options();
+    totp_cli_command_automation_docopt_options();
+#else
+    TOTP_CLI_PRINTF(
+        "All the TOTP CLI commands, their arguments, options and usage can be found here https://t.ly/_6pJG");
+#endif
+}

+ 14 - 0
cli/commands/help/help.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "../../../config/app/config.h"
+#include <cli/cli.h>
+
+#define TOTP_CLI_COMMAND_HELP "help"
+#define TOTP_CLI_COMMAND_HELP_ALT "h"
+#define TOTP_CLI_COMMAND_HELP_ALT2 "?"
+
+void totp_cli_command_help_handle();
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_help_docopt_commands();
+void totp_cli_command_help_docopt_usage();
+#endif

+ 58 - 0
cli/commands/list/list.c

@@ -0,0 +1,58 @@
+#include "list.h"
+#include <stdlib.h>
+#include "../../../types/token_info.h"
+#include "../../../services/config/constants.h"
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+#include "../../cli_helpers.h"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_list_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_LIST ", " TOTP_CLI_COMMAND_LIST_ALT
+                    "         List all available tokens\r\n");
+}
+
+void totp_cli_command_list_docopt_usage() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_NAME " " DOCOPT_REQUIRED(
+        TOTP_CLI_COMMAND_LIST " | " TOTP_CLI_COMMAND_LIST_ALT) "\r\n");
+}
+#endif
+
+void totp_cli_command_list_handle(PluginState* plugin_state, 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;
+    }
+
+    TOTP_CLI_LOCK_UI(plugin_state);
+
+    size_t original_index = totp_token_info_iterator_get_current_token_index(iterator_context);
+
+    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
+    TOTP_CLI_PRINTF("| %-3s | %-25s | %-6s | %-s | %-s |\r\n", "#", "Name", "Algo", "Ln", "Dur");
+    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
+    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);
+        TOTP_CLI_PRINTF(
+            "| %-3" PRIu16 " | %-25.25s | %-6s | %-2" PRIu8 " | %-3" PRIu8 " |\r\n",
+            i + 1,
+            furi_string_get_cstr(token_info->name),
+            token_info_get_algo_as_cstr(token_info),
+            token_info->digits,
+            token_info->duration);
+    }
+
+    TOTP_CLI_PRINTF("+-----+---------------------------+--------+----+-----+\r\n");
+
+    totp_token_info_iterator_go_to(iterator_context, original_index);
+
+    TOTP_CLI_UNLOCK_UI(plugin_state);
+}

+ 14 - 0
cli/commands/list/list.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_LIST "list"
+#define TOTP_CLI_COMMAND_LIST_ALT "ls"
+
+void totp_cli_command_list_handle(PluginState* plugin_state, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_list_docopt_commands();
+void totp_cli_command_list_docopt_usage();
+#endif

+ 85 - 0
cli/commands/move/move.c

@@ -0,0 +1,85 @@
+#include "move.h"
+
+#include <stdlib.h>
+#include <lib/toolbox/args.h>
+#include "../../../types/token_info.h"
+#include "../../../services/config/config.h"
+#include "../../cli_helpers.h"
+#include "../../../ui/scene_director.h"
+#include "../../common_command_arguments.h"
+
+#define TOTP_CLI_COMMAND_MOVE_ARG_NEW_INDEX "new_index"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_move_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_MOVE ", " TOTP_CLI_COMMAND_MOVE_ALT
+                    "         Move token\r\n");
+}
+
+void totp_cli_command_move_docopt_usage() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_NAME
+        " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_MOVE " | " TOTP_CLI_COMMAND_MOVE_ALT) " " DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_ARG_INDEX) " " DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_MOVE_ARG_NEW_INDEX) "\r\n");
+}
+
+void totp_cli_command_move_docopt_arguments() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_MOVE_ARG_NEW_INDEX
+                    "     New token index in the list\r\n");
+}
+#endif
+
+void totp_cli_command_move_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);
+}

+ 15 - 0
cli/commands/move/move.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_MOVE "move"
+#define TOTP_CLI_COMMAND_MOVE_ALT "mv"
+
+void totp_cli_command_move_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_move_docopt_commands();
+void totp_cli_command_move_docopt_usage();
+void totp_cli_command_move_docopt_arguments();
+#endif

+ 105 - 0
cli/commands/notification/notification.c

@@ -0,0 +1,105 @@
+#include "notification.h"
+#include <lib/toolbox/args.h>
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+#include "../../cli_helpers.h"
+
+#define TOTP_CLI_COMMAND_NOTIFICATION_ARG_METHOD "notification"
+#define TOTP_CLI_COMMAND_NOTIFICATION_METHOD_NONE "none"
+#define TOTP_CLI_COMMAND_NOTIFICATION_METHOD_SOUND "sound"
+#define TOTP_CLI_COMMAND_NOTIFICATION_METHOD_VIBRO "vibro"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_notification_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_NOTIFICATION
+                    "           Get or set notification method\r\n");
+}
+
+void totp_cli_command_notification_docopt_usage() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_NAME " " TOTP_CLI_COMMAND_NOTIFICATION " " DOCOPT_OPTIONAL(
+            DOCOPT_MULTIPLE(DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_NOTIFICATION_ARG_METHOD))) "\r\n");
+}
+
+void totp_cli_command_notification_docopt_arguments() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_NOTIFICATION_ARG_METHOD
+        "  Notification method to be set. Must be one of: " TOTP_CLI_COMMAND_NOTIFICATION_METHOD_NONE
+        ", " TOTP_CLI_COMMAND_NOTIFICATION_METHOD_SOUND
+        ", " TOTP_CLI_COMMAND_NOTIFICATION_METHOD_VIBRO "\r\n");
+}
+#endif
+
+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 "\"");
+    }
+}
+
+void totp_cli_command_notification_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);
+}

+ 14 - 0
cli/commands/notification/notification.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_NOTIFICATION "notify"
+
+void totp_cli_command_notification_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_notification_docopt_commands();
+void totp_cli_command_notification_docopt_usage();
+void totp_cli_command_notification_docopt_arguments();
+#endif

+ 193 - 0
cli/commands/pin/pin.c

@@ -0,0 +1,193 @@
+#include "pin.h"
+
+#include <stdlib.h>
+#include <lib/toolbox/args.h>
+#include "../../../types/token_info.h"
+#include "../../../types/user_pin_codes.h"
+#include "../../../services/config/config.h"
+#include "../../cli_helpers.h"
+#include <memset_s.h>
+#include "../../../services/crypto/crypto_facade.h"
+#include "../../../ui/scene_director.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"
+#define TOTP_CLI_COMMAND_PIN_ARG_NEW_CRYPTO_KEY_SLOT "slot"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_pin_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_PIN "              Set\\change\\remove PIN\r\n");
+}
+
+void totp_cli_command_pin_docopt_usage() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_NAME " " TOTP_CLI_COMMAND_PIN
+        " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_PIN_COMMAND_SET " | " TOTP_CLI_COMMAND_PIN_COMMAND_REMOVE) " " DOCOPT_OPTIONAL(
+            DOCOPT_OPTION(
+                TOTP_CLI_COMMAND_PIN_ARG_NEW_CRYPTO_KEY_SLOT_PREFIX,
+                DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_PIN_ARG_NEW_CRYPTO_KEY_SLOT))) "\r\n");
+}
+
+void totp_cli_command_pin_docopt_options() {
+    TOTP_CLI_PRINTF(
+        "  " DOCOPT_OPTION(
+            TOTP_CLI_COMMAND_PIN_ARG_NEW_CRYPTO_KEY_SLOT_PREFIX,
+            DOCOPT_ARGUMENT(
+                TOTP_CLI_COMMAND_PIN_ARG_NEW_CRYPTO_KEY_SLOT)) "      New crypto key slot. Must be between %d and %d\r\n",
+        ACCEPTABLE_CRYPTO_KEY_SLOT_START,
+        ACCEPTABLE_CRYPTO_KEY_SLOT_END);
+}
+#endif
+
+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;
+}
+
+void totp_cli_command_pin_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);
+}

+ 14 - 0
cli/commands/pin/pin.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_PIN "pin"
+
+void totp_cli_command_pin_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_pin_docopt_commands();
+void totp_cli_command_pin_docopt_usage();
+void totp_cli_command_pin_docopt_options();
+#endif

+ 42 - 0
cli/commands/reset/reset.c

@@ -0,0 +1,42 @@
+#include "reset.h"
+
+#include <stdlib.h>
+#include <furi/core/string.h>
+#include "../../cli_helpers.h"
+#include "../../../ui/scene_director.h"
+#include "../../../services/config/config.h"
+
+#define TOTP_CLI_RESET_CONFIRMATION_KEYWORD "YES"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_reset_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_RESET
+                    "            Reset application to default settings\r\n");
+}
+
+void totp_cli_command_reset_docopt_usage() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_NAME " " TOTP_CLI_COMMAND_RESET "\r\n");
+}
+#endif
+
+void totp_cli_command_reset_handle(PluginState* plugin_state, Cli* cli) {
+    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);
+    }
+}

+ 13 - 0
cli/commands/reset/reset.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_RESET "reset"
+
+void totp_cli_command_reset_handle(PluginState* plugin_state, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_reset_docopt_commands();
+void totp_cli_command_reset_docopt_usage();
+#endif

+ 54 - 0
cli/commands/timezone/timezone.c

@@ -0,0 +1,54 @@
+#include "timezone.h"
+#include <lib/toolbox/args.h>
+#include "../../../services/config/config.h"
+#include "../../../ui/scene_director.h"
+#include "../../cli_helpers.h"
+
+#define TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE "timezone"
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_timezone_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_TIMEZONE ", " TOTP_CLI_COMMAND_TIMEZONE_ALT
+                    "     Get or set current timezone\r\n");
+}
+
+void totp_cli_command_timezone_docopt_usage() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_NAME
+        " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_TIMEZONE " | " TOTP_CLI_COMMAND_TIMEZONE_ALT) " " DOCOPT_OPTIONAL(
+            DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE)) "\r\n");
+}
+
+void totp_cli_command_timezone_docopt_arguments() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE
+                    "      Timezone offset in hours to be set\r\n");
+}
+#endif
+
+void totp_cli_command_timezone_handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
+    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);
+}

+ 15 - 0
cli/commands/timezone/timezone.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_TIMEZONE "timezone"
+#define TOTP_CLI_COMMAND_TIMEZONE_ALT "tz"
+
+void totp_cli_command_timezone_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_timezone_docopt_commands();
+void totp_cli_command_timezone_docopt_usage();
+void totp_cli_command_timezone_docopt_arguments();
+#endif

+ 177 - 0
cli/commands/update/update.c

@@ -0,0 +1,177 @@
+#include "update.h"
+#include <stdlib.h>
+#include <lib/toolbox/args.h>
+#include "../../../types/token_info.h"
+#include "../../../services/config/config.h"
+#include "../../../services/convert/convert.h"
+#include "../../cli_helpers.h"
+#include "../../../ui/scene_director.h"
+#include "../../common_command_arguments.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_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;
+}
+
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_update_docopt_commands() {
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_UPDATE "           Update existing token\r\n");
+}
+
+void totp_cli_command_update_docopt_usage() {
+    TOTP_CLI_PRINTF(
+        "  " TOTP_CLI_COMMAND_NAME
+        " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_UPDATE) " " DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_INDEX) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_ALGO_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_ALGO))) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_SECRET_ENCODING))) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_NAME_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_NAME))) " " DOCOPT_OPTIONAL(
+            DOCOPT_OPTION(
+                TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX,
+                DOCOPT_ARGUMENT(
+                    TOTP_CLI_COMMAND_ARG_DIGITS))) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_DURATION_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_DURATION))) " " DOCOPT_OPTIONAL(DOCOPT_SWITCH(TOTP_CLI_COMMAND_ARG_UNSECURE_PREFIX)) " " DOCOPT_OPTIONAL(DOCOPT_SWITCH(TOTP_CLI_COMMAND_UPDATE_ARG_SECRET_PREFIX)) " " DOCOPT_MULTIPLE(DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE)))) "\r\n");
+}
+
+void totp_cli_command_update_docopt_options() {
+    TOTP_CLI_PRINTF("  " DOCOPT_OPTION(
+        TOTP_CLI_COMMAND_ARG_NAME_PREFIX,
+        DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ARG_NAME)) "      Token name\r\n");
+
+    TOTP_CLI_PRINTF("  " DOCOPT_SWITCH(
+        TOTP_CLI_COMMAND_UPDATE_ARG_SECRET_PREFIX) "             Update token secret\r\n");
+}
+#endif
+
+void totp_cli_command_update_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);
+}

+ 14 - 0
cli/commands/update/update.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <cli/cli.h>
+#include "../../../types/plugin_state.h"
+#include "../../../config/app/config.h"
+
+#define TOTP_CLI_COMMAND_UPDATE "update"
+
+void totp_cli_command_update_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
+#ifdef TOTP_CLI_RICH_HELP_ENABLED
+void totp_cli_command_update_docopt_commands();
+void totp_cli_command_update_docopt_usage();
+void totp_cli_command_update_docopt_options();
+#endif

+ 141 - 0
cli/common_command_arguments.c

@@ -0,0 +1,141 @@
+#include "common_command_arguments.h"
+#include <lib/toolbox/args.h>
+
+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));
+}
+
+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;
+}

+ 106 - 0
cli/common_command_arguments.h

@@ -0,0 +1,106 @@
+#pragma once
+#include <stdlib.h>
+#include "../types/token_info.h"
+#include "cli_helpers.h"
+
+#define TOTP_CLI_COMMAND_ARG_NAME "name"
+#define TOTP_CLI_COMMAND_ARG_NAME_PREFIX "-n"
+#define TOTP_CLI_COMMAND_ARG_ALGO "algo"
+#define TOTP_CLI_COMMAND_ARG_ALGO_PREFIX "-a"
+#define TOTP_CLI_COMMAND_ARG_DIGITS "digits"
+#define TOTP_CLI_COMMAND_ARG_DIGITS_PREFIX "-d"
+#define TOTP_CLI_COMMAND_ARG_UNSECURE_PREFIX "-u"
+#define TOTP_CLI_COMMAND_ARG_DURATION "duration"
+#define TOTP_CLI_COMMAND_ARG_DURATION_PREFIX "-l"
+#define TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE_PREFIX "-b"
+#define TOTP_CLI_COMMAND_ARG_AUTOMATION_FEATURE "feature"
+#define TOTP_CLI_COMMAND_ARG_INDEX "index"
+#define TOTP_CLI_COMMAND_ARG_SECRET_ENCODING_PREFIX "-e"
+#define TOTP_CLI_COMMAND_ARG_SECRET_ENCODING "encoding"
+
+/**
+ * @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);
+
+/**
+ * @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);

+ 42 - 0
config/app/config.h

@@ -0,0 +1,42 @@
+// 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
+#ifndef TOTP_NO_OBSOLETE_CRYPTO_V1_COMPATIBILITY
+#define TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+#endif
+
+// 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 userfriendly TOTP CLI help text
+// If disabled, it will print a link to a wiki page
+#ifndef TOTP_CLI_NO_RICH_HELP
+#define TOTP_CLI_RICH_HELP_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
+
+// 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
+
+// Target firmware
+#ifndef TOTP_TARGET_FIRMWARE
+#define TOTP_TARGET_FIRMWARE TOTP_FIRMWARE_XTREME_UL
+#endif

+ 34 - 0
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
images/DolphinCommon_56x48.png


BIN
images/hid_ble_31x9.png


BIN
images/hid_usb_31x9.png


BIN
images/totp_arrow_left_8x9.png


BIN
images/totp_arrow_right_8x9.png


+ 60 - 0
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
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
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
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);

+ 941 - 0
lib/fonts/712serif/712serif.c

@@ -0,0 +1,941 @@
+#include "712serif.h"
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for 7:12 Serif 24pt
+*/
+
+/* Character bitmaps for 7:12 Serif 24pt */
+const uint8_t _712Serif_24ptBitmaps[] = {
+    /* @0 '-' (14 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @28 '0' (14 pixels wide) */
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @56 '1' (14 pixels wide) */
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+
+    /* @84 '2' (14 pixels wide) */
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0xC0,
+    0x03,
+    0xC0,
+    0x03,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+
+    /* @112 '3' (14 pixels wide) */
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0xC0,
+    0x03,
+    0xC0,
+    0x03,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @140 '4' (14 pixels wide) */
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0xC0,
+    0x0F,
+    0xC0,
+    0x0F,
+
+    /* @168 '5' (14 pixels wide) */
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0xFC,
+    0x03,
+    0xFC,
+    0x03,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @196 '6' (14 pixels wide) */
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0xFC,
+    0x03,
+    0xFC,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @224 '7' (14 pixels wide) */
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+
+    /* @252 '8' (14 pixels wide) */
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @280 '9' (14 pixels wide) */
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x0F,
+    0xF0,
+    0x0F,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @308 'B' (14 pixels wide) */
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0xFF,
+    0x0F,
+    0xFF,
+    0x0F,
+
+    /* @336 'C' (14 pixels wide) */
+    0xF0,
+    0x33,
+    0xF0,
+    0x33,
+    0x0C,
+    0x3C,
+    0x0C,
+    0x3C,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @364 'D' (14 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+
+    /* @392 'F' (14 pixels wide) */
+    0xFF,
+    0x3F,
+    0xFF,
+    0x3F,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0xFC,
+    0x03,
+    0xFC,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x3F,
+    0x00,
+    0x3F,
+    0x00,
+
+    /* @420 'G' (14 pixels wide) */
+    0xF0,
+    0x33,
+    0xF0,
+    0x33,
+    0x0C,
+    0x3C,
+    0x0C,
+    0x3C,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x3F,
+    0x03,
+    0x3F,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0x0C,
+    0x3C,
+    0x0C,
+    0x3C,
+    0xF0,
+    0x33,
+    0xF0,
+    0x33,
+
+    /* @448 'H' (14 pixels wide) */
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+
+    /* @476 'J' (14 pixels wide) */
+    0x00,
+    0x3F,
+    0x00,
+    0x3F,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @504 'K' (14 pixels wide) */
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+
+    /* @532 'M' (14 pixels wide) */
+    0x0F,
+    0x3C,
+    0x0F,
+    0x3C,
+    0x3C,
+    0x0F,
+    0x3C,
+    0x0F,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+
+    /* @560 'N' (14 pixels wide) */
+    0x0F,
+    0x3F,
+    0x0F,
+    0x3F,
+    0x3C,
+    0x0C,
+    0x3C,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0x0C,
+    0x0F,
+    0x0C,
+    0x0F,
+    0x3F,
+    0x0C,
+    0x3F,
+    0x0C,
+
+    /* @588 'P' (14 pixels wide) */
+    0xFF,
+    0x0F,
+    0xFF,
+    0x0F,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x3C,
+    0x30,
+    0x3C,
+    0x30,
+    0xCC,
+    0x0F,
+    0xCC,
+    0x0F,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x3F,
+    0x00,
+    0x3F,
+    0x00,
+
+    /* @616 'Q' (14 pixels wide) */
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0xF3,
+    0x30,
+    0xF3,
+    0x30,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xF0,
+    0x33,
+    0xF0,
+    0x33,
+
+    /* @644 'R' (14 pixels wide) */
+    0xFF,
+    0x0F,
+    0xFF,
+    0x0F,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0x0C,
+    0x30,
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+    0x0C,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+
+    /* @672 'T' (14 pixels wide) */
+    0xFF,
+    0x3F,
+    0xFF,
+    0x3F,
+    0xC3,
+    0x30,
+    0xC3,
+    0x30,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+
+    /* @700 'V' (14 pixels wide) */
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+
+    /* @728 'W' (14 pixels wide) */
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0xCC,
+    0x0C,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+
+    /* @756 'X' (14 pixels wide) */
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+
+    /* @784 'Y' (14 pixels wide) */
+    0x3F,
+    0x3F,
+    0x3F,
+    0x3F,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x0C,
+    0x30,
+    0x03,
+    0x30,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+};
+
+/* Character descriptors for 7:12 Serif 24pt */
+/* { [Char width in bits], [Offset into _712Serif_24ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO _712Serif_24ptDescriptors[] = {
+    {14, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {14, 28}, /* 0 */
+    {14, 56}, /* 1 */
+    {14, 84}, /* 2 */
+    {14, 112}, /* 3 */
+    {14, 140}, /* 4 */
+    {14, 168}, /* 5 */
+    {14, 196}, /* 6 */
+    {14, 224}, /* 7 */
+    {14, 252}, /* 8 */
+    {14, 280}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {14, 308}, /* B */
+    {14, 336}, /* C */
+    {14, 364}, /* D */
+    {0, 0}, /* E */
+    {14, 392}, /* F */
+    {14, 420}, /* G */
+    {14, 448}, /* H */
+    {0, 0}, /* I */
+    {14, 476}, /* J */
+    {14, 504}, /* K */
+    {0, 0}, /* L */
+    {14, 532}, /* M */
+    {14, 560}, /* N */
+    {0, 0}, /* O */
+    {14, 588}, /* P */
+    {14, 616}, /* Q */
+    {14, 644}, /* R */
+    {0, 0}, /* S */
+    {14, 672}, /* T */
+    {0, 0}, /* U */
+    {14, 700}, /* V */
+    {14, 728}, /* W */
+    {14, 756}, /* X */
+    {14, 784}, /* Y */
+};
+
+/* Font information for 7:12 Serif 24pt */
+const FONT_INFO _712Serif_24ptFontInfo = {
+    "712 Serif",
+    14, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    _712Serif_24ptDescriptors, /*  Character descriptor array */
+    _712Serif_24ptBitmaps, /*  Character bitmap array */
+};

+ 8 - 0
lib/fonts/712serif/712serif.h

@@ -0,0 +1,8 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+/* Font data for 7:12 Serif 24pt */
+extern const FONT_INFO _712Serif_24ptFontInfo;

+ 23 - 0
lib/fonts/available_fonts.c

@@ -0,0 +1,23 @@
+#include "available_fonts.h"
+#include "712serif/712serif.h"
+#include "bedstead/bedstead.h"
+#include "dpcomic/dpcomic.h"
+#include "funclimbing/funclimbing.h"
+#include "graph35pix/graph35pix.h"
+#include "karma_future/karma_future.h"
+#include "mode_nine/mode_nine.h"
+#include "pixelflag/pixelflag.h"
+#include "redhat_mono/redhat_mono.h"
+#include "zector/zector.h"
+
+const FONT_INFO* const available_fonts[AVAILABLE_FONTS_COUNT] = {
+    &modeNine_15ptFontInfo,
+    &_712Serif_24ptFontInfo,
+    &bedstead_17ptFontInfo,
+    &dPComic_18ptFontInfo,
+    &funclimbingDemo_18ptFontInfo,
+    &graph35pix_12ptFontInfo,
+    &karmaFuture_14ptFontInfo,
+    &pixelFlag_18ptFontInfo,
+    &redHatMono_16ptFontInfo,
+    &zector_18ptFontInfo};

+ 7 - 0
lib/fonts/available_fonts.h

@@ -0,0 +1,7 @@
+#pragma once
+
+#include "font_info.h"
+
+#define AVAILABLE_FONTS_COUNT (10)
+
+extern const FONT_INFO* const available_fonts[AVAILABLE_FONTS_COUNT];

+ 1057 - 0
lib/fonts/bedstead/bedstead.c

@@ -0,0 +1,1057 @@
+#include "bedstead.h"
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for Bedstead 17pt
+*/
+
+/* Character bitmaps for Bedstead 17pt */
+const uint8_t bedstead_17ptBitmaps[] = {
+    /* @0 '-' (13 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @32 '0' (13 pixels wide) */
+    0xE0,
+    0x00,
+    0xF0,
+    0x01,
+    0xF8,
+    0x03,
+    0xBC,
+    0x07,
+    0x1E,
+    0x0F,
+    0x0F,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1E,
+    0x1E,
+    0x0F,
+    0xBC,
+    0x07,
+    0xF8,
+    0x03,
+    0xF0,
+    0x01,
+    0xE0,
+    0x00,
+
+    /* @64 '1' (13 pixels wide) */
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xF8,
+    0x00,
+    0xF8,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xF8,
+    0x03,
+    0xF8,
+    0x03,
+
+    /* @96 '2' (13 pixels wide) */
+    0xF0,
+    0x07,
+    0xFC,
+    0x0F,
+    0x1E,
+    0x1E,
+    0x1E,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1E,
+    0xC0,
+    0x0F,
+    0xE0,
+    0x07,
+    0xF0,
+    0x00,
+    0x7C,
+    0x00,
+    0x3E,
+    0x00,
+    0x1E,
+    0x00,
+    0x0E,
+    0x00,
+    0xFE,
+    0x1F,
+    0xFE,
+    0x1F,
+
+    /* @128 '3' (13 pixels wide) */
+    0xFE,
+    0x1F,
+    0xFE,
+    0x1F,
+    0x00,
+    0x1C,
+    0x00,
+    0x1E,
+    0x00,
+    0x0F,
+    0x00,
+    0x0F,
+    0x00,
+    0x07,
+    0xE0,
+    0x07,
+    0xE0,
+    0x0F,
+    0x00,
+    0x1E,
+    0x00,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x1E,
+    0x1C,
+    0x3E,
+    0x0E,
+    0xFC,
+    0x0F,
+    0xF8,
+    0x07,
+
+    /* @160 '4' (13 pixels wide) */
+    0x80,
+    0x03,
+    0x80,
+    0x03,
+    0xE0,
+    0x03,
+    0xF0,
+    0x03,
+    0xF8,
+    0x03,
+    0xBC,
+    0x03,
+    0x9E,
+    0x03,
+    0x8F,
+    0x03,
+    0x87,
+    0x03,
+    0xFF,
+    0x0F,
+    0xFF,
+    0x0F,
+    0x80,
+    0x03,
+    0x80,
+    0x03,
+    0x80,
+    0x03,
+    0x80,
+    0x03,
+    0x80,
+    0x03,
+
+    /* @192 '5' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0xFF,
+    0x07,
+    0xFF,
+    0x0F,
+    0x00,
+    0x1E,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1E,
+    0xFE,
+    0x0F,
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+
+    /* @224 '6' (13 pixels wide) */
+    0xE0,
+    0x07,
+    0xF0,
+    0x07,
+    0x78,
+    0x00,
+    0x3C,
+    0x00,
+    0x1E,
+    0x00,
+    0x0F,
+    0x00,
+    0x07,
+    0x00,
+    0xFF,
+    0x07,
+    0xFF,
+    0x0F,
+    0x07,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1E,
+    0x1E,
+    0x0F,
+    0xFE,
+    0x0F,
+    0xFC,
+    0x07,
+
+    /* @256 '7' (13 pixels wide) */
+    0xFE,
+    0x1F,
+    0xFE,
+    0x1F,
+    0x00,
+    0x1C,
+    0x00,
+    0x1E,
+    0x00,
+    0x0F,
+    0x80,
+    0x07,
+    0xC0,
+    0x03,
+    0xE0,
+    0x01,
+    0xF0,
+    0x00,
+    0x78,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+
+    /* @288 '8' (13 pixels wide) */
+    0xFC,
+    0x07,
+    0xFE,
+    0x0F,
+    0x0F,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1E,
+    0xFE,
+    0x0F,
+    0xFE,
+    0x0F,
+    0x0F,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1E,
+    0x1E,
+    0x0F,
+    0xFE,
+    0x0F,
+    0xFC,
+    0x07,
+
+    /* @320 '9' (13 pixels wide) */
+    0xFC,
+    0x07,
+    0xFE,
+    0x0F,
+    0x0F,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1C,
+    0xFE,
+    0x1F,
+    0xFC,
+    0x1F,
+    0x00,
+    0x1C,
+    0x00,
+    0x1E,
+    0x00,
+    0x0F,
+    0x80,
+    0x07,
+    0xC0,
+    0x03,
+    0xFC,
+    0x01,
+    0xFC,
+    0x00,
+
+    /* @352 'B' (13 pixels wide) */
+    0xFF,
+    0x07,
+    0xFF,
+    0x0F,
+    0x07,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1E,
+    0xFF,
+    0x0F,
+    0xFF,
+    0x0F,
+    0x07,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1E,
+    0x07,
+    0x0F,
+    0xFF,
+    0x0F,
+    0xFF,
+    0x07,
+
+    /* @384 'C' (13 pixels wide) */
+    0xFC,
+    0x01,
+    0xFE,
+    0x07,
+    0x0F,
+    0x0F,
+    0x07,
+    0x0F,
+    0x07,
+    0x0E,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x0F,
+    0x8F,
+    0x0F,
+    0xFE,
+    0x07,
+    0xFC,
+    0x03,
+
+    /* @416 'D' (13 pixels wide) */
+    0xFF,
+    0x07,
+    0xFF,
+    0x0F,
+    0x07,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1E,
+    0x07,
+    0x0F,
+    0xFF,
+    0x0F,
+    0xFF,
+    0x07,
+
+    /* @448 'F' (13 pixels wide) */
+    0xFF,
+    0x0F,
+    0xFF,
+    0x0F,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+
+    /* @480 'G' (13 pixels wide) */
+    0xFC,
+    0x07,
+    0xFE,
+    0x0F,
+    0x0F,
+    0x1F,
+    0x0F,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x87,
+    0x1F,
+    0x87,
+    0x1F,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1C,
+    0xFE,
+    0x1F,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x1F,
+
+    /* @512 'H' (13 pixels wide) */
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+
+    /* @544 'J' (13 pixels wide) */
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x00,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x1E,
+    0x1C,
+    0x3E,
+    0x1E,
+    0xFC,
+    0x0F,
+    0xF8,
+    0x07,
+
+    /* @576 'K' (13 pixels wide) */
+    0x07,
+    0x0E,
+    0x07,
+    0x0F,
+    0x87,
+    0x07,
+    0xC7,
+    0x03,
+    0xE7,
+    0x01,
+    0xF7,
+    0x00,
+    0x7F,
+    0x00,
+    0x3F,
+    0x00,
+    0x3F,
+    0x00,
+    0x7F,
+    0x00,
+    0xF7,
+    0x00,
+    0xE7,
+    0x01,
+    0xC7,
+    0x03,
+    0x87,
+    0x07,
+    0x07,
+    0x0F,
+    0x07,
+    0x0E,
+
+    /* @608 'M' (13 pixels wide) */
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0xBF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0xE7,
+    0x1C,
+    0xE7,
+    0x1C,
+    0xE7,
+    0x1C,
+    0xE7,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+
+    /* @640 'N' (13 pixels wide) */
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x3F,
+    0x1C,
+    0x7F,
+    0x1C,
+    0xFF,
+    0x1C,
+    0xE7,
+    0x1F,
+    0xC7,
+    0x1F,
+    0x87,
+    0x1F,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+
+    /* @672 'P' (13 pixels wide) */
+    0xFF,
+    0x07,
+    0xFF,
+    0x0F,
+    0x07,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1E,
+    0xFF,
+    0x0F,
+    0xFF,
+    0x07,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+
+    /* @704 'Q' (13 pixels wide) */
+    0xFC,
+    0x07,
+    0xFE,
+    0x0F,
+    0x0F,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0xE7,
+    0x1C,
+    0xE7,
+    0x1F,
+    0xC7,
+    0x0F,
+    0x8F,
+    0x07,
+    0xDE,
+    0x0F,
+    0xFE,
+    0x1F,
+    0xFC,
+    0x1C,
+
+    /* @736 'R' (13 pixels wide) */
+    0xFF,
+    0x07,
+    0xFF,
+    0x0F,
+    0x07,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1E,
+    0xFF,
+    0x0F,
+    0xFF,
+    0x07,
+    0xE7,
+    0x00,
+    0xE7,
+    0x01,
+    0xC7,
+    0x03,
+    0x87,
+    0x07,
+    0x07,
+    0x0F,
+    0x07,
+    0x1E,
+    0x07,
+    0x1C,
+
+    /* @768 'T' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+
+    /* @800 'V' (13 pixels wide) */
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x1F,
+    0x1F,
+    0xBC,
+    0x07,
+    0xB8,
+    0x03,
+    0xB8,
+    0x03,
+    0xF8,
+    0x07,
+    0xF0,
+    0x03,
+    0xE0,
+    0x01,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+
+    /* @832 'W' (13 pixels wide) */
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0xE7,
+    0x1C,
+    0xE7,
+    0x1C,
+    0xE7,
+    0x1C,
+    0xE7,
+    0x1C,
+    0xE7,
+    0x1C,
+    0xFF,
+    0x1F,
+    0xFE,
+    0x0F,
+    0xFE,
+    0x0F,
+    0xBC,
+    0x07,
+
+    /* @864 'X' (13 pixels wide) */
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1E,
+    0x1E,
+    0x0F,
+    0xBC,
+    0x07,
+    0xF8,
+    0x03,
+    0xF0,
+    0x01,
+    0xF0,
+    0x01,
+    0xF8,
+    0x03,
+    0xBC,
+    0x07,
+    0x1E,
+    0x0F,
+    0x0F,
+    0x1E,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+
+    /* @896 'Y' (13 pixels wide) */
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x0F,
+    0x1E,
+    0x1E,
+    0x0F,
+    0xBC,
+    0x07,
+    0xF8,
+    0x03,
+    0xF0,
+    0x01,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+};
+
+/* Character descriptors for Bedstead 17pt */
+/* { [Char width in bits], [Offset into bedstead_17ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO bedstead_17ptDescriptors[] = {
+    {13, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {13, 32}, /* 0 */
+    {13, 64}, /* 1 */
+    {13, 96}, /* 2 */
+    {13, 128}, /* 3 */
+    {13, 160}, /* 4 */
+    {13, 192}, /* 5 */
+    {13, 224}, /* 6 */
+    {13, 256}, /* 7 */
+    {13, 288}, /* 8 */
+    {13, 320}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {13, 352}, /* B */
+    {13, 384}, /* C */
+    {13, 416}, /* D */
+    {0, 0}, /* E */
+    {13, 448}, /* F */
+    {13, 480}, /* G */
+    {13, 512}, /* H */
+    {0, 0}, /* I */
+    {13, 544}, /* J */
+    {13, 576}, /* K */
+    {0, 0}, /* L */
+    {13, 608}, /* M */
+    {13, 640}, /* N */
+    {0, 0}, /* O */
+    {13, 672}, /* P */
+    {13, 704}, /* Q */
+    {13, 736}, /* R */
+    {0, 0}, /* S */
+    {13, 768}, /* T */
+    {0, 0}, /* U */
+    {13, 800}, /* V */
+    {13, 832}, /* W */
+    {13, 864}, /* X */
+    {13, 896}, /* Y */
+};
+
+/* Font information for Bedstead 17pt */
+const FONT_INFO bedstead_17ptFontInfo = {
+    "Bedstead",
+    16, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    bedstead_17ptDescriptors, /*  Character descriptor array */
+    bedstead_17ptBitmaps, /*  Character bitmap array */
+};

+ 8 - 0
lib/fonts/bedstead/bedstead.h

@@ -0,0 +1,8 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+/* Font data for Bedstead 17pt */
+extern const FONT_INFO bedstead_17ptFontInfo;

+ 1115 - 0
lib/fonts/dpcomic/dpcomic.c

@@ -0,0 +1,1115 @@
+#include "dpcomic.h"
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for DPComic 18pt
+*/
+
+/* Character bitmaps for DPComic 18pt */
+const uint8_t dPComic_18ptBitmaps[] = {
+    /* @0 '-' (15 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xE0,
+    0x3F,
+    0xF8,
+    0x0F,
+    0xF8,
+    0x0F,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @34 '0' (15 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xE0,
+    0x07,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0xE0,
+    0x07,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @68 '1' (15 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xE0,
+    0x01,
+    0xF8,
+    0x01,
+    0xF8,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xE0,
+    0x0F,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @102 '2' (15 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x07,
+    0xC0,
+    0x07,
+    0xE0,
+    0x0F,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x07,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xE0,
+    0x3F,
+    0xF8,
+    0x0F,
+    0xF8,
+    0x0F,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @136 '3' (15 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x3F,
+    0xC0,
+    0x3F,
+    0xE0,
+    0x3F,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x07,
+    0xC0,
+    0x07,
+    0xC0,
+    0x07,
+    0x00,
+    0x0F,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0xE0,
+    0x0F,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @170 '4' (15 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0F,
+    0xC0,
+    0x0F,
+    0xC0,
+    0x0F,
+    0xE0,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0xF8,
+    0x0F,
+    0xF8,
+    0x0F,
+    0xF8,
+    0x0F,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @204 '5' (15 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x3F,
+    0xC0,
+    0x3F,
+    0xC0,
+    0x0F,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x07,
+    0xE0,
+    0x0F,
+    0xE0,
+    0x0F,
+    0x00,
+    0x0E,
+    0x00,
+    0x0F,
+    0x00,
+    0x0F,
+    0xE0,
+    0x07,
+    0xF8,
+    0x01,
+    0xF8,
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @238 '6' (15 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x0F,
+    0x00,
+    0x0F,
+    0xC0,
+    0x07,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x07,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0F,
+    0x38,
+    0x0F,
+    0xF8,
+    0x07,
+    0xE0,
+    0x01,
+    0xE0,
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @272 '7' (15 pixels wide) */
+    0x00,
+    0x00,
+    0xE0,
+    0x0F,
+    0xE0,
+    0x0F,
+    0xF8,
+    0x0F,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x07,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xE0,
+    0x00,
+    0xF8,
+    0x00,
+    0xF8,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @306 '8' (15 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x0F,
+    0xC0,
+    0x0F,
+    0xE0,
+    0x38,
+    0xE0,
+    0x3E,
+    0xE0,
+    0x3E,
+    0xC0,
+    0x0F,
+    0xE0,
+    0x07,
+    0xE0,
+    0x07,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0F,
+    0xE0,
+    0x07,
+    0xE0,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @340 '9' (15 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x07,
+    0xC0,
+    0x07,
+    0xE0,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0xF8,
+    0x0F,
+    0xF8,
+    0x0F,
+    0xE0,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0xE0,
+    0x01,
+    0xF8,
+    0x00,
+    0xF8,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @374 'B' (15 pixels wide) */
+    0xC0,
+    0x0F,
+    0xE0,
+    0x3F,
+    0xE0,
+    0x3F,
+    0xF8,
+    0x38,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0xF8,
+    0x0F,
+    0xF8,
+    0x3F,
+    0xF8,
+    0x3F,
+    0x38,
+    0x38,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0xF8,
+    0x0F,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @408 'C' (15 pixels wide) */
+    0x00,
+    0x0F,
+    0xC0,
+    0x3F,
+    0xC0,
+    0x3F,
+    0xE0,
+    0x39,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xF8,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x38,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0xF8,
+    0x0F,
+    0xE0,
+    0x07,
+    0xE0,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @442 'D' (15 pixels wide) */
+    0xC0,
+    0x07,
+    0xE0,
+    0x0F,
+    0xE0,
+    0x0F,
+    0xF8,
+    0x3E,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x3E,
+    0x38,
+    0x0F,
+    0x38,
+    0x0F,
+    0xF8,
+    0x07,
+    0xF8,
+    0x01,
+    0xF8,
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @476 'F' (15 pixels wide) */
+    0x80,
+    0x1F,
+    0xC0,
+    0x0F,
+    0xC0,
+    0x0F,
+    0xF0,
+    0x01,
+    0x70,
+    0x00,
+    0x70,
+    0x00,
+    0xF0,
+    0x0F,
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0x70,
+    0x00,
+    0x70,
+    0x00,
+    0x70,
+    0x00,
+    0x70,
+    0x00,
+    0x70,
+    0x00,
+    0x70,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @510 'G' (15 pixels wide) */
+    0x00,
+    0x0F,
+    0xC0,
+    0x3F,
+    0xC0,
+    0x3F,
+    0xE0,
+    0x39,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xF8,
+    0x00,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0x38,
+    0x38,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0xF8,
+    0x0F,
+    0xE0,
+    0x07,
+    0xE0,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @544 'H' (15 pixels wide) */
+    0x00,
+    0x30,
+    0x20,
+    0x38,
+    0x20,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0xF8,
+    0x3F,
+    0xF8,
+    0x3F,
+    0xF8,
+    0x3F,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @578 'J' (15 pixels wide) */
+    0x00,
+    0x08,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x00,
+    0x0E,
+    0x18,
+    0x0F,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0xE0,
+    0x01,
+    0xE0,
+    0x01,
+
+    /* @612 'K' (15 pixels wide) */
+    0x20,
+    0x30,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x3E,
+    0x38,
+    0x0F,
+    0x38,
+    0x0F,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x38,
+    0x0F,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @646 'M' (15 pixels wide) */
+    0x08,
+    0x10,
+    0x3E,
+    0x1C,
+    0x3E,
+    0x1C,
+    0x7E,
+    0x1E,
+    0xFE,
+    0x1F,
+    0xFE,
+    0x1F,
+    0xCE,
+    0x1D,
+    0x0E,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x0E,
+    0x1C,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @680 'N' (15 pixels wide) */
+    0x20,
+    0x30,
+    0xF8,
+    0x38,
+    0xF8,
+    0x38,
+    0xF8,
+    0x38,
+    0xF8,
+    0x39,
+    0xF8,
+    0x39,
+    0xF8,
+    0x39,
+    0x38,
+    0x3F,
+    0x38,
+    0x3F,
+    0x38,
+    0x3F,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @714 'P' (15 pixels wide) */
+    0xC0,
+    0x07,
+    0xE0,
+    0x0F,
+    0xE0,
+    0x0F,
+    0xF8,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0F,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0xF8,
+    0x01,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @748 'Q' (15 pixels wide) */
+    0x00,
+    0x0F,
+    0xC0,
+    0x3F,
+    0xC0,
+    0x3F,
+    0xE0,
+    0x39,
+    0xE0,
+    0x38,
+    0xE0,
+    0x38,
+    0xF8,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x3E,
+    0x38,
+    0x3E,
+    0xF8,
+    0x0F,
+    0xE0,
+    0x3F,
+    0xE0,
+    0x3F,
+    0x00,
+    0x38,
+    0x00,
+    0x38,
+
+    /* @782 'R' (15 pixels wide) */
+    0xC0,
+    0x07,
+    0xE0,
+    0x0F,
+    0xE0,
+    0x0F,
+    0xF8,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0F,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0xF8,
+    0x01,
+    0x38,
+    0x07,
+    0x38,
+    0x07,
+    0x38,
+    0x0F,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @816 'T' (15 pixels wide) */
+    0x00,
+    0x7E,
+    0xE0,
+    0x3F,
+    0xE0,
+    0x3F,
+    0xF8,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @850 'V' (15 pixels wide) */
+    0x20,
+    0x30,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x07,
+    0x38,
+    0x07,
+    0xE0,
+    0x07,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @884 'W' (15 pixels wide) */
+    0x04,
+    0x40,
+    0x07,
+    0x70,
+    0x07,
+    0x70,
+    0x07,
+    0x71,
+    0xC7,
+    0x71,
+    0xC7,
+    0x71,
+    0xC7,
+    0x71,
+    0xC7,
+    0x71,
+    0xC7,
+    0x71,
+    0xC7,
+    0x71,
+    0xE7,
+    0x39,
+    0xE7,
+    0x39,
+    0x3C,
+    0x0F,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @918 'X' (15 pixels wide) */
+    0x18,
+    0x30,
+    0x38,
+    0x38,
+    0x38,
+    0x38,
+    0xF8,
+    0x3E,
+    0xE0,
+    0x0F,
+    0xE0,
+    0x0F,
+    0xC0,
+    0x07,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xE0,
+    0x07,
+    0x38,
+    0x0F,
+    0x38,
+    0x0F,
+    0x38,
+    0x3E,
+    0x18,
+    0x38,
+    0x18,
+    0x38,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @952 'Y' (15 pixels wide) */
+    0x18,
+    0x08,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0x38,
+    0x0E,
+    0xF8,
+    0x0F,
+    0xF8,
+    0x0F,
+    0xE0,
+    0x07,
+    0xE0,
+    0x01,
+    0xE0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+};
+
+/* Character descriptors for DPComic 18pt */
+/* { [Char width in bits], [Offset into dPComic_18ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO dPComic_18ptDescriptors[] = {
+    {15, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {15, 34}, /* 0 */
+    {15, 68}, /* 1 */
+    {15, 102}, /* 2 */
+    {15, 136}, /* 3 */
+    {15, 170}, /* 4 */
+    {15, 204}, /* 5 */
+    {15, 238}, /* 6 */
+    {15, 272}, /* 7 */
+    {15, 306}, /* 8 */
+    {15, 340}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {15, 374}, /* B */
+    {15, 408}, /* C */
+    {15, 442}, /* D */
+    {0, 0}, /* E */
+    {15, 476}, /* F */
+    {15, 510}, /* G */
+    {15, 544}, /* H */
+    {0, 0}, /* I */
+    {15, 578}, /* J */
+    {15, 612}, /* K */
+    {0, 0}, /* L */
+    {15, 646}, /* M */
+    {15, 680}, /* N */
+    {0, 0}, /* O */
+    {15, 714}, /* P */
+    {15, 748}, /* Q */
+    {15, 782}, /* R */
+    {0, 0}, /* S */
+    {15, 816}, /* T */
+    {0, 0}, /* U */
+    {15, 850}, /* V */
+    {15, 884}, /* W */
+    {15, 918}, /* X */
+    {15, 952}, /* Y */
+};
+
+/* Font information for DPComic 18pt */
+const FONT_INFO dPComic_18ptFontInfo = {
+    "DP Comic",
+    17, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    dPComic_18ptDescriptors, /*  Character descriptor array */
+    dPComic_18ptBitmaps, /*  Character bitmap array */
+};

+ 7 - 0
lib/fonts/dpcomic/dpcomic.h

@@ -0,0 +1,7 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+extern const FONT_INFO dPComic_18ptFontInfo;

+ 24 - 0
lib/fonts/font_info.h

@@ -0,0 +1,24 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include <stdint.h>
+
+// This structure describes a single character's display information
+typedef struct {
+    const uint8_t width; // width, in bits (or pixels), of the character
+    const uint16_t
+        offset; // offset of the character's bitmap, in bytes, into the the FONT_INFO's data array
+
+} FONT_CHAR_INFO;
+
+// Describes a single font
+typedef struct {
+    const char* name; // Font name
+    const uint8_t height; // height, in pages (8 pixels), of the font's characters
+    const uint8_t startChar; // the first character in the font (e.g. in charInfo and data)
+    const uint8_t endChar; // the last character in the font
+    const uint8_t spacePixels; // number of pixels that a space character takes up
+    const FONT_CHAR_INFO* charInfo; // pointer to array of char information
+    const uint8_t* data; // pointer to generated array of character visual representation
+} FONT_INFO;

+ 1173 - 0
lib/fonts/funclimbing/funclimbing.c

@@ -0,0 +1,1173 @@
+#include "funclimbing.h"
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for fun climbing (Demo) 18pt
+*/
+
+/* Character bitmaps for fun climbing (Demo) 18pt */
+const uint8_t funclimbingDemo_18ptBitmaps[] = {
+    /* @0 '-' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xE0,
+    0x03,
+    0xE0,
+    0x03,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @36 '0' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x80,
+    0x00,
+    0xE0,
+    0x03,
+    0x20,
+    0x02,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x08,
+    0x04,
+    0x08,
+    0x08,
+    0x08,
+    0x08,
+    0x08,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x20,
+    0x02,
+    0xE0,
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @72 '1' (16 pixels wide) */
+    0x00,
+    0x02,
+    0x00,
+    0x03,
+    0x80,
+    0x03,
+    0xC0,
+    0x02,
+    0x60,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x00,
+
+    /* @108 '2' (16 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x07,
+    0x60,
+    0x04,
+    0x20,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x08,
+    0x02,
+    0x08,
+    0x02,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x40,
+    0x00,
+    0x60,
+    0x00,
+    0xC0,
+    0x03,
+    0x00,
+    0x1C,
+    0x00,
+    0x00,
+
+    /* @144 '3' (16 pixels wide) */
+    0x00,
+    0x02,
+    0x80,
+    0x05,
+    0x40,
+    0x08,
+    0x40,
+    0x08,
+    0x40,
+    0x10,
+    0x40,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0x0C,
+    0x00,
+    0x06,
+    0x00,
+    0x03,
+    0x80,
+    0x0F,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x30,
+    0x08,
+    0xC0,
+    0x07,
+    0x00,
+    0x00,
+
+    /* @180 '4' (16 pixels wide) */
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x40,
+    0x10,
+    0x40,
+    0x10,
+    0x40,
+    0x10,
+    0x40,
+    0x10,
+    0x40,
+    0x10,
+    0x20,
+    0x10,
+    0x20,
+    0x10,
+    0x20,
+    0x10,
+    0x20,
+    0x10,
+    0x30,
+    0x10,
+    0xF0,
+    0x1F,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+
+    /* @216 '5' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x60,
+    0x00,
+    0xA0,
+    0x03,
+    0x20,
+    0x04,
+    0x20,
+    0x00,
+    0x30,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0xF0,
+    0x00,
+    0x80,
+    0x03,
+    0x00,
+    0x04,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0x04,
+    0x80,
+    0x03,
+    0xF0,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @252 '6' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x40,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x18,
+    0x03,
+    0xC8,
+    0x06,
+    0x28,
+    0x0C,
+    0x18,
+    0x08,
+    0x18,
+    0x08,
+    0x08,
+    0x08,
+    0x08,
+    0x08,
+    0x10,
+    0x08,
+    0x30,
+    0x0C,
+    0xC0,
+    0x07,
+    0x00,
+    0x00,
+
+    /* @288 '7' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x0F,
+    0xF0,
+    0x09,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x06,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x03,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @324 '8' (16 pixels wide) */
+    0x80,
+    0x01,
+    0x60,
+    0x02,
+    0x30,
+    0x04,
+    0x10,
+    0x08,
+    0x10,
+    0x08,
+    0x08,
+    0x08,
+    0x08,
+    0x08,
+    0x10,
+    0x0C,
+    0x10,
+    0x04,
+    0x60,
+    0x03,
+    0xC0,
+    0x01,
+    0x40,
+    0x03,
+    0x20,
+    0x04,
+    0x20,
+    0x04,
+    0x20,
+    0x04,
+    0x20,
+    0x04,
+    0xC0,
+    0x03,
+    0x00,
+    0x00,
+
+    /* @360 '9' (16 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x01,
+    0x20,
+    0x02,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0xE0,
+    0x03,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @396 'B' (16 pixels wide) */
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0xE8,
+    0x0F,
+    0x38,
+    0x10,
+    0x10,
+    0x10,
+    0x10,
+    0x30,
+    0x10,
+    0x10,
+    0x10,
+    0x18,
+    0xF0,
+    0x07,
+    0x20,
+    0x00,
+
+    /* @432 'C' (16 pixels wide) */
+    0x00,
+    0x03,
+    0x80,
+    0x06,
+    0x40,
+    0x04,
+    0x40,
+    0x04,
+    0x40,
+    0x08,
+    0x40,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x40,
+    0x00,
+    0x40,
+    0x08,
+    0x40,
+    0x08,
+    0x40,
+    0x08,
+    0xC0,
+    0x08,
+    0x80,
+    0x04,
+    0x80,
+    0x05,
+    0x00,
+    0x02,
+
+    /* @468 'D' (16 pixels wide) */
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x80,
+    0x07,
+    0x40,
+    0x04,
+    0x20,
+    0x04,
+    0x20,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x30,
+    0x04,
+    0x20,
+    0x04,
+    0x20,
+    0x04,
+    0x40,
+    0x04,
+    0x80,
+    0x05,
+    0x00,
+    0x07,
+
+    /* @504 'F' (16 pixels wide) */
+    0x00,
+    0x02,
+    0x80,
+    0x0D,
+    0xC0,
+    0x08,
+    0x40,
+    0x10,
+    0x60,
+    0x10,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0xF8,
+    0x01,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x40,
+    0x00,
+
+    /* @540 'G' (16 pixels wide) */
+    0x80,
+    0x00,
+    0x60,
+    0x01,
+    0x20,
+    0x03,
+    0x30,
+    0x02,
+    0x10,
+    0x02,
+    0x10,
+    0x02,
+    0x10,
+    0x02,
+    0x10,
+    0x02,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0xD0,
+    0x0F,
+    0x10,
+    0x02,
+    0x10,
+    0x01,
+    0x20,
+    0x01,
+    0xE0,
+    0x00,
+    0x40,
+    0x00,
+
+    /* @576 'H' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0x18,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x0F,
+    0x90,
+    0x09,
+    0xD0,
+    0x18,
+    0x50,
+    0x10,
+    0x30,
+    0x10,
+    0x30,
+    0x10,
+    0x10,
+    0x10,
+    0x00,
+    0x00,
+
+    /* @612 'J' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x20,
+    0x04,
+    0x20,
+    0x02,
+    0x20,
+    0x02,
+    0x60,
+    0x03,
+    0xC0,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @648 'K' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x08,
+    0x10,
+    0x0C,
+    0x10,
+    0x02,
+    0xB0,
+    0x01,
+    0xE0,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0xA0,
+    0x00,
+    0x20,
+    0x01,
+    0x20,
+    0x02,
+    0x20,
+    0x0C,
+    0x20,
+    0x08,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @684 'M' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x10,
+    0x10,
+    0x30,
+    0x18,
+    0x30,
+    0x18,
+    0x50,
+    0x34,
+    0x50,
+    0x24,
+    0xC8,
+    0x22,
+    0x88,
+    0x22,
+    0x88,
+    0x21,
+    0x08,
+    0x21,
+    0x04,
+    0x60,
+    0x04,
+    0x40,
+    0x04,
+    0x40,
+    0x04,
+    0x40,
+    0x02,
+    0x40,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @720 'N' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x02,
+    0x01,
+    0x86,
+    0x07,
+    0x44,
+    0x04,
+    0x24,
+    0x08,
+    0x24,
+    0x08,
+    0x14,
+    0x18,
+    0x14,
+    0x10,
+    0x1C,
+    0x10,
+    0x0C,
+    0x10,
+    0x0C,
+    0x10,
+    0x08,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @756 'P' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xE0,
+    0x01,
+    0x20,
+    0x02,
+    0x20,
+    0x04,
+    0x60,
+    0x04,
+    0x40,
+    0x04,
+    0x40,
+    0x04,
+    0x40,
+    0x04,
+    0x40,
+    0x06,
+    0x40,
+    0x02,
+    0xC0,
+    0x01,
+    0xC0,
+    0x00,
+    0x40,
+    0x00,
+    0x40,
+    0x00,
+    0x40,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @792 'Q' (16 pixels wide) */
+    0x00,
+    0x00,
+    0xC0,
+    0x03,
+    0x30,
+    0x06,
+    0x18,
+    0x08,
+    0x08,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x20,
+    0x04,
+    0x20,
+    0x04,
+    0x20,
+    0x08,
+    0x21,
+    0x08,
+    0x33,
+    0x10,
+    0x12,
+    0x30,
+    0x0E,
+    0xC0,
+    0x07,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0x10,
+    0x00,
+    0x00,
+
+    /* @828 'R' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x07,
+    0x88,
+    0x0D,
+    0x48,
+    0x08,
+    0x68,
+    0x00,
+    0x28,
+    0x00,
+    0x28,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @864 'T' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0xE0,
+    0x03,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x04,
+    0x80,
+    0x04,
+    0x80,
+    0x04,
+    0x80,
+    0x04,
+    0x80,
+    0x05,
+    0x00,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @900 'V' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x08,
+    0x10,
+    0x10,
+    0x10,
+    0x10,
+    0x18,
+    0x10,
+    0x08,
+    0x10,
+    0x08,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x20,
+    0x04,
+    0x20,
+    0x02,
+    0x20,
+    0x02,
+    0x20,
+    0x01,
+    0x40,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @936 'W' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x03,
+    0x80,
+    0x02,
+    0x80,
+    0x02,
+    0x40,
+    0x02,
+    0x40,
+    0x02,
+    0x41,
+    0x86,
+    0x42,
+    0x84,
+    0x42,
+    0x44,
+    0x44,
+    0x44,
+    0x64,
+    0x24,
+    0x28,
+    0x28,
+    0x28,
+    0x18,
+    0x30,
+    0x18,
+    0x30,
+    0x18,
+    0x20,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @972 'X' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x08,
+    0x08,
+    0x10,
+    0x04,
+    0x20,
+    0x04,
+    0x40,
+    0x02,
+    0x80,
+    0x03,
+    0x80,
+    0x01,
+    0x80,
+    0x03,
+    0x40,
+    0x02,
+    0x30,
+    0x04,
+    0x18,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @1008 'Y' (16 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x08,
+    0x08,
+    0x08,
+    0x08,
+    0x08,
+    0x0C,
+    0x08,
+    0x0C,
+    0x08,
+    0x0A,
+    0x10,
+    0x0A,
+    0xF0,
+    0x09,
+    0x00,
+    0x0C,
+    0x10,
+    0x04,
+    0x10,
+    0x04,
+    0x20,
+    0x02,
+    0xE0,
+    0x03,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+};
+
+/* Character descriptors for fun climbing (Demo) 18pt */
+/* { [Char width in bits], [Offset into funclimbingDemo_18ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO funclimbingDemo_18ptDescriptors[] = {
+    {16, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {16, 36}, /* 0 */
+    {16, 72}, /* 1 */
+    {16, 108}, /* 2 */
+    {16, 144}, /* 3 */
+    {16, 180}, /* 4 */
+    {16, 216}, /* 5 */
+    {16, 252}, /* 6 */
+    {16, 288}, /* 7 */
+    {16, 324}, /* 8 */
+    {16, 360}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {16, 396}, /* B */
+    {16, 432}, /* C */
+    {16, 468}, /* D */
+    {0, 0}, /* E */
+    {16, 504}, /* F */
+    {16, 540}, /* G */
+    {16, 576}, /* H */
+    {0, 0}, /* I */
+    {16, 612}, /* J */
+    {16, 648}, /* K */
+    {0, 0}, /* L */
+    {16, 684}, /* M */
+    {16, 720}, /* N */
+    {0, 0}, /* O */
+    {16, 756}, /* P */
+    {16, 792}, /* Q */
+    {16, 828}, /* R */
+    {0, 0}, /* S */
+    {16, 864}, /* T */
+    {0, 0}, /* U */
+    {16, 900}, /* V */
+    {16, 936}, /* W */
+    {16, 972}, /* X */
+    {16, 1008}, /* Y */
+};
+
+/* Font information for fun climbing (Demo) 18pt */
+const FONT_INFO funclimbingDemo_18ptFontInfo = {
+    "Fun Climbing",
+    18, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    funclimbingDemo_18ptDescriptors, /*  Character descriptor array */
+    funclimbingDemo_18ptBitmaps, /*  Character bitmap array */
+};

+ 7 - 0
lib/fonts/funclimbing/funclimbing.h

@@ -0,0 +1,7 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+extern const FONT_INFO funclimbingDemo_18ptFontInfo;

+ 941 - 0
lib/fonts/graph35pix/graph35pix.c

@@ -0,0 +1,941 @@
+#include "graph35pix.h"
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for Graph 35+ pix 12pt
+*/
+
+/* Character bitmaps for Graph 35+ pix 12pt */
+const uint8_t graph35pix_12ptBitmaps[] = {
+    /* @0 '-' (10 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @28 '0' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xC3,
+    0x03,
+    0xC3,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x0F,
+    0x03,
+    0x0F,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+
+    /* @56 '1' (10 pixels wide) */
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x3C,
+    0x00,
+    0x3C,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+
+    /* @84 '2' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+
+    /* @112 '3' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+
+    /* @140 '4' (10 pixels wide) */
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xF0,
+    0x00,
+    0xF0,
+    0x00,
+    0xCC,
+    0x00,
+    0xCC,
+    0x00,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+
+    /* @168 '5' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+
+    /* @196 '6' (10 pixels wide) */
+    0xF0,
+    0x00,
+    0xF0,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+
+    /* @224 '7' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+
+    /* @252 '8' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+
+    /* @280 '9' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x03,
+    0xFC,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0x3C,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @308 'B' (10 pixels wide) */
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+
+    /* @336 'C' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+
+    /* @364 'D' (10 pixels wide) */
+    0x3F,
+    0x00,
+    0x3F,
+    0x00,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0x3F,
+    0x00,
+    0x3F,
+    0x00,
+
+    /* @392 'F' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+
+    /* @420 'G' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xF3,
+    0x03,
+    0xF3,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFC,
+    0x03,
+    0xFC,
+    0x03,
+
+    /* @448 'H' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @476 'J' (10 pixels wide) */
+    0xF0,
+    0x03,
+    0xF0,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0x3C,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @504 'K' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0x33,
+    0x00,
+    0x33,
+    0x00,
+    0x0F,
+    0x00,
+    0x0F,
+    0x00,
+    0x33,
+    0x00,
+    0x33,
+    0x00,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @532 'M' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xCF,
+    0x03,
+    0xCF,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @560 'N' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x0F,
+    0x03,
+    0x0F,
+    0x03,
+    0x0F,
+    0x03,
+    0x0F,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0xC3,
+    0x03,
+    0xC3,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @588 'P' (10 pixels wide) */
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+
+    /* @616 'Q' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0x3C,
+    0x03,
+    0x3C,
+    0x03,
+
+    /* @644 'R' (10 pixels wide) */
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x33,
+    0x00,
+    0x33,
+    0x00,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @672 'T' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+
+    /* @700 'V' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xCC,
+    0x00,
+    0xCC,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+
+    /* @728 'W' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0xCC,
+    0x00,
+    0xCC,
+    0x00,
+
+    /* @756 'X' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xCC,
+    0x00,
+    0xCC,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0xCC,
+    0x00,
+    0xCC,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @784 'Y' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xCC,
+    0x00,
+    0xCC,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+};
+
+/* Character descriptors for Graph 35+ pix 12pt */
+/* { [Char width in bits], [Offset into graph35pix_12ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO graph35pix_12ptDescriptors[] = {
+    {10, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {10, 28}, /* 0 */
+    {10, 56}, /* 1 */
+    {10, 84}, /* 2 */
+    {10, 112}, /* 3 */
+    {10, 140}, /* 4 */
+    {10, 168}, /* 5 */
+    {10, 196}, /* 6 */
+    {10, 224}, /* 7 */
+    {10, 252}, /* 8 */
+    {10, 280}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {10, 308}, /* B */
+    {10, 336}, /* C */
+    {10, 364}, /* D */
+    {0, 0}, /* E */
+    {10, 392}, /* F */
+    {10, 420}, /* G */
+    {10, 448}, /* H */
+    {0, 0}, /* I */
+    {10, 476}, /* J */
+    {10, 504}, /* K */
+    {0, 0}, /* L */
+    {10, 532}, /* M */
+    {10, 560}, /* N */
+    {0, 0}, /* O */
+    {10, 588}, /* P */
+    {10, 616}, /* Q */
+    {10, 644}, /* R */
+    {0, 0}, /* S */
+    {10, 672}, /* T */
+    {0, 0}, /* U */
+    {10, 700}, /* V */
+    {10, 728}, /* W */
+    {10, 756}, /* X */
+    {10, 784}, /* Y */
+};
+
+/* Font information for Graph 35+ pix 12pt */
+const FONT_INFO graph35pix_12ptFontInfo = {
+    "Graph 35pix",
+    14, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    graph35pix_12ptDescriptors, /*  Character descriptor array */
+    graph35pix_12ptBitmaps, /*  Character bitmap array */
+};

+ 8 - 0
lib/fonts/graph35pix/graph35pix.h

@@ -0,0 +1,8 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+/* Font data for Graph 35+ pix 12pt */
+extern const FONT_INFO graph35pix_12ptFontInfo;

+ 1173 - 0
lib/fonts/karma_future/karma_future.c

@@ -0,0 +1,1173 @@
+#include "karma_future.h"
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for Karma Future 14pt
+*/
+
+/* Character bitmaps for Karma Future 14pt */
+const uint8_t karmaFuture_14ptBitmaps[] = {
+    /* @0 '-' (12 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xFE,
+    0x03,
+    0x02,
+    0x02,
+    0x02,
+    0x02,
+    0xFE,
+    0x03,
+    0xFC,
+    0x03,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @36 '0' (12 pixels wide) */
+    0xF8,
+    0x01,
+    0x0E,
+    0x03,
+    0x02,
+    0x06,
+    0xF3,
+    0x06,
+    0xF9,
+    0x0C,
+    0x79,
+    0x0C,
+    0x39,
+    0x0D,
+    0x99,
+    0x0C,
+    0xC9,
+    0x0C,
+    0xE9,
+    0x0C,
+    0xF1,
+    0x0C,
+    0xF1,
+    0x0C,
+    0xF1,
+    0x0C,
+    0xF3,
+    0x0E,
+    0x02,
+    0x0E,
+    0x0E,
+    0x07,
+    0xFC,
+    0x07,
+    0xF8,
+    0x01,
+
+    /* @72 '1' (12 pixels wide) */
+    0x70,
+    0x00,
+    0xD8,
+    0x00,
+    0xCE,
+    0x00,
+    0xC2,
+    0x00,
+    0xC2,
+    0x00,
+    0xDE,
+    0x00,
+    0xDC,
+    0x00,
+    0xDC,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xDE,
+    0x03,
+    0x02,
+    0x02,
+    0x02,
+    0x02,
+    0xFE,
+    0x03,
+    0xFC,
+    0x03,
+
+    /* @108 '2' (12 pixels wide) */
+    0xFC,
+    0x03,
+    0x06,
+    0x03,
+    0x03,
+    0x06,
+    0xF1,
+    0x04,
+    0xF9,
+    0x0C,
+    0xF9,
+    0x0C,
+    0x7F,
+    0x0E,
+    0x3E,
+    0x0F,
+    0x1E,
+    0x0F,
+    0x90,
+    0x07,
+    0xC8,
+    0x03,
+    0xE4,
+    0x01,
+    0xE6,
+    0x00,
+    0xF3,
+    0x07,
+    0x01,
+    0x0C,
+    0x01,
+    0x0C,
+    0xFF,
+    0x0F,
+    0xFE,
+    0x0F,
+
+    /* @144 '3' (12 pixels wide) */
+    0xFC,
+    0x03,
+    0x06,
+    0x03,
+    0x03,
+    0x06,
+    0xF1,
+    0x04,
+    0xF9,
+    0x0C,
+    0xFF,
+    0x0C,
+    0xFE,
+    0x0C,
+    0x0E,
+    0x0E,
+    0x08,
+    0x0F,
+    0xF8,
+    0x0E,
+    0xF8,
+    0x04,
+    0xFF,
+    0x0C,
+    0xF9,
+    0x0C,
+    0xF1,
+    0x0C,
+    0x03,
+    0x0E,
+    0x06,
+    0x0F,
+    0xFC,
+    0x07,
+    0xFC,
+    0x03,
+
+    /* @180 '4' (12 pixels wide) */
+    0xE0,
+    0x03,
+    0x30,
+    0x03,
+    0x10,
+    0x03,
+    0x08,
+    0x03,
+    0x4C,
+    0x03,
+    0x64,
+    0x03,
+    0x72,
+    0x03,
+    0x73,
+    0x03,
+    0x79,
+    0x03,
+    0x01,
+    0x04,
+    0x01,
+    0x0C,
+    0x7F,
+    0x0F,
+    0x7E,
+    0x0F,
+    0x7E,
+    0x0F,
+    0x40,
+    0x03,
+    0x40,
+    0x03,
+    0xC0,
+    0x03,
+    0x80,
+    0x03,
+
+    /* @216 '5' (12 pixels wide) */
+    0xFF,
+    0x07,
+    0x01,
+    0x0C,
+    0x01,
+    0x0C,
+    0xF9,
+    0x0F,
+    0xF9,
+    0x0F,
+    0xF9,
+    0x0F,
+    0x01,
+    0x03,
+    0x03,
+    0x06,
+    0xFE,
+    0x04,
+    0xFE,
+    0x0C,
+    0xFC,
+    0x0C,
+    0xFF,
+    0x0C,
+    0xF9,
+    0x0C,
+    0xF1,
+    0x0C,
+    0x03,
+    0x0E,
+    0x06,
+    0x0F,
+    0xFC,
+    0x07,
+    0xF8,
+    0x03,
+
+    /* @252 '6' (12 pixels wide) */
+    0xF0,
+    0x03,
+    0x10,
+    0x03,
+    0x08,
+    0x03,
+    0xCC,
+    0x03,
+    0xE6,
+    0x03,
+    0xF2,
+    0x03,
+    0xF2,
+    0x00,
+    0x01,
+    0x03,
+    0x01,
+    0x06,
+    0xF1,
+    0x04,
+    0xF9,
+    0x0C,
+    0xF9,
+    0x0C,
+    0xF9,
+    0x0C,
+    0xF1,
+    0x0C,
+    0x03,
+    0x0E,
+    0x06,
+    0x0F,
+    0xFC,
+    0x07,
+    0xF8,
+    0x03,
+
+    /* @288 '7' (12 pixels wide) */
+    0xFF,
+    0x07,
+    0x01,
+    0x0C,
+    0x01,
+    0x0C,
+    0xFF,
+    0x0E,
+    0xFE,
+    0x0E,
+    0x7E,
+    0x0F,
+    0x40,
+    0x07,
+    0x60,
+    0x07,
+    0x20,
+    0x03,
+    0x30,
+    0x03,
+    0x90,
+    0x01,
+    0x90,
+    0x01,
+    0xD0,
+    0x00,
+    0xC8,
+    0x00,
+    0xE8,
+    0x00,
+    0xE8,
+    0x00,
+    0x78,
+    0x00,
+    0x78,
+    0x00,
+
+    /* @324 '8' (12 pixels wide) */
+    0xF8,
+    0x01,
+    0x0C,
+    0x01,
+    0x06,
+    0x03,
+    0x72,
+    0x06,
+    0xF2,
+    0x06,
+    0xF2,
+    0x06,
+    0x72,
+    0x06,
+    0x06,
+    0x07,
+    0x02,
+    0x06,
+    0xF1,
+    0x04,
+    0xF9,
+    0x0C,
+    0xF9,
+    0x0C,
+    0xF9,
+    0x0C,
+    0xF1,
+    0x0C,
+    0x03,
+    0x0E,
+    0x06,
+    0x0F,
+    0xFC,
+    0x07,
+    0xF8,
+    0x03,
+
+    /* @360 '9' (12 pixels wide) */
+    0xFC,
+    0x03,
+    0x06,
+    0x03,
+    0x03,
+    0x06,
+    0xF1,
+    0x04,
+    0xF9,
+    0x0C,
+    0xF9,
+    0x0C,
+    0xF1,
+    0x0C,
+    0x03,
+    0x0C,
+    0x06,
+    0x0C,
+    0xFE,
+    0x0E,
+    0x7C,
+    0x0E,
+    0x38,
+    0x0F,
+    0x30,
+    0x07,
+    0x1C,
+    0x07,
+    0x84,
+    0x03,
+    0xC4,
+    0x01,
+    0xFC,
+    0x00,
+    0xF8,
+    0x00,
+
+    /* @396 'B' (12 pixels wide) */
+    0xFE,
+    0x01,
+    0x02,
+    0x03,
+    0x02,
+    0x02,
+    0x72,
+    0x06,
+    0xF2,
+    0x06,
+    0xF2,
+    0x06,
+    0x72,
+    0x06,
+    0x02,
+    0x06,
+    0x02,
+    0x06,
+    0xF2,
+    0x0C,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0C,
+    0x02,
+    0x0E,
+    0x02,
+    0x0E,
+    0xFE,
+    0x07,
+    0xFC,
+    0x03,
+
+    /* @432 'C' (12 pixels wide) */
+    0xF0,
+    0x03,
+    0x18,
+    0x02,
+    0x0C,
+    0x06,
+    0xE4,
+    0x0C,
+    0xE6,
+    0x0D,
+    0xF2,
+    0x0F,
+    0x72,
+    0x0F,
+    0x32,
+    0x0F,
+    0x12,
+    0x00,
+    0x12,
+    0x00,
+    0x12,
+    0x00,
+    0x32,
+    0x07,
+    0xA6,
+    0x0D,
+    0xE4,
+    0x0C,
+    0x08,
+    0x0E,
+    0x18,
+    0x0E,
+    0xF0,
+    0x07,
+    0xF0,
+    0x03,
+
+    /* @468 'D' (12 pixels wide) */
+    0xFE,
+    0x01,
+    0x02,
+    0x03,
+    0x02,
+    0x02,
+    0x72,
+    0x06,
+    0xF2,
+    0x06,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0E,
+    0xF2,
+    0x0E,
+    0x72,
+    0x0E,
+    0x02,
+    0x06,
+    0x02,
+    0x07,
+    0xFE,
+    0x03,
+    0xFC,
+    0x01,
+
+    /* @504 'F' (12 pixels wide) */
+    0xFE,
+    0x07,
+    0x02,
+    0x0C,
+    0x02,
+    0x0C,
+    0xF2,
+    0x0F,
+    0xF2,
+    0x0F,
+    0xF2,
+    0x0F,
+    0x12,
+    0x00,
+    0xF2,
+    0x03,
+    0x02,
+    0x02,
+    0xF2,
+    0x03,
+    0xF2,
+    0x03,
+    0xF2,
+    0x03,
+    0x12,
+    0x00,
+    0x12,
+    0x00,
+    0x12,
+    0x00,
+    0x12,
+    0x00,
+    0x1E,
+    0x00,
+    0x1C,
+    0x00,
+
+    /* @540 'G' (12 pixels wide) */
+    0xF0,
+    0x07,
+    0x18,
+    0x06,
+    0x0C,
+    0x0C,
+    0xE4,
+    0x0D,
+    0xE6,
+    0x0F,
+    0xF2,
+    0x0F,
+    0xF2,
+    0x0F,
+    0x32,
+    0x04,
+    0x32,
+    0x0C,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xE6,
+    0x0D,
+    0xE6,
+    0x0D,
+    0xE4,
+    0x0D,
+    0x08,
+    0x0C,
+    0x18,
+    0x0C,
+    0xF0,
+    0x0F,
+    0xF0,
+    0x0F,
+
+    /* @576 'H' (12 pixels wide) */
+    0x1E,
+    0x07,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0xF2,
+    0x0D,
+    0x02,
+    0x0C,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x1E,
+    0x0F,
+    0x1C,
+    0x0F,
+
+    /* @612 'J' (12 pixels wide) */
+    0x80,
+    0x03,
+    0x80,
+    0x06,
+    0x80,
+    0x06,
+    0x80,
+    0x06,
+    0x80,
+    0x06,
+    0x80,
+    0x06,
+    0x80,
+    0x06,
+    0x80,
+    0x06,
+    0x80,
+    0x06,
+    0x80,
+    0x06,
+    0x8E,
+    0x06,
+    0x8A,
+    0x06,
+    0xDA,
+    0x06,
+    0x72,
+    0x06,
+    0x02,
+    0x06,
+    0x06,
+    0x03,
+    0xFC,
+    0x03,
+    0xF8,
+    0x01,
+
+    /* @648 'K' (12 pixels wide) */
+    0x1E,
+    0x07,
+    0x92,
+    0x0D,
+    0xD2,
+    0x0C,
+    0x72,
+    0x0E,
+    0x32,
+    0x0E,
+    0x12,
+    0x0F,
+    0x92,
+    0x07,
+    0xC2,
+    0x03,
+    0xC2,
+    0x01,
+    0x92,
+    0x01,
+    0x12,
+    0x01,
+    0x32,
+    0x02,
+    0x32,
+    0x02,
+    0x72,
+    0x06,
+    0xD2,
+    0x0C,
+    0x92,
+    0x0D,
+    0x9E,
+    0x0F,
+    0x1C,
+    0x0F,
+
+    /* @684 'M' (12 pixels wide) */
+    0x1E,
+    0x07,
+    0x92,
+    0x0D,
+    0xA2,
+    0x0C,
+    0xE2,
+    0x0C,
+    0x62,
+    0x0C,
+    0x62,
+    0x0C,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x92,
+    0x0D,
+    0x92,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x1E,
+    0x0F,
+    0x1C,
+    0x0F,
+
+    /* @720 'N' (12 pixels wide) */
+    0x1E,
+    0x07,
+    0x12,
+    0x0D,
+    0x22,
+    0x0D,
+    0x62,
+    0x0D,
+    0x62,
+    0x0D,
+    0xD2,
+    0x0D,
+    0xD2,
+    0x0D,
+    0x92,
+    0x0D,
+    0x92,
+    0x0D,
+    0x32,
+    0x0D,
+    0x32,
+    0x0D,
+    0x72,
+    0x0C,
+    0x52,
+    0x0C,
+    0xD2,
+    0x0C,
+    0x92,
+    0x0C,
+    0x92,
+    0x0D,
+    0x1E,
+    0x0F,
+    0x1C,
+    0x0F,
+
+    /* @756 'P' (12 pixels wide) */
+    0xFE,
+    0x03,
+    0x02,
+    0x02,
+    0x02,
+    0x06,
+    0xF2,
+    0x0C,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0C,
+    0x02,
+    0x0E,
+    0x02,
+    0x0E,
+    0xF2,
+    0x0F,
+    0xF2,
+    0x07,
+    0xF2,
+    0x03,
+    0x12,
+    0x00,
+    0x12,
+    0x00,
+    0x12,
+    0x00,
+    0x1E,
+    0x00,
+    0x1C,
+    0x00,
+
+    /* @792 'Q' (12 pixels wide) */
+    0xF0,
+    0x01,
+    0x18,
+    0x03,
+    0x0C,
+    0x02,
+    0x64,
+    0x06,
+    0xE6,
+    0x06,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0x32,
+    0x0D,
+    0x26,
+    0x0E,
+    0x26,
+    0x0E,
+    0x64,
+    0x0E,
+    0x08,
+    0x0C,
+    0x18,
+    0x0D,
+    0xF0,
+    0x0F,
+    0xF0,
+    0x0F,
+
+    /* @828 'R' (12 pixels wide) */
+    0xFE,
+    0x03,
+    0x02,
+    0x02,
+    0x02,
+    0x06,
+    0xF2,
+    0x0C,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0D,
+    0xF2,
+    0x0C,
+    0x02,
+    0x0E,
+    0x02,
+    0x0E,
+    0x12,
+    0x0F,
+    0x32,
+    0x06,
+    0x32,
+    0x06,
+    0x72,
+    0x06,
+    0xD2,
+    0x0C,
+    0x92,
+    0x0D,
+    0x9E,
+    0x0F,
+    0x1C,
+    0x0F,
+
+    /* @864 'T' (12 pixels wide) */
+    0xFE,
+    0x03,
+    0x02,
+    0x02,
+    0x02,
+    0x02,
+    0xDE,
+    0x03,
+    0xDC,
+    0x03,
+    0xDC,
+    0x03,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xD0,
+    0x00,
+    0xF0,
+    0x00,
+    0xF0,
+    0x00,
+
+    /* @900 'V' (12 pixels wide) */
+    0x1E,
+    0x07,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x92,
+    0x0D,
+    0xA6,
+    0x0E,
+    0xA4,
+    0x0E,
+    0xA4,
+    0x0E,
+    0xE4,
+    0x06,
+    0x6C,
+    0x06,
+    0x68,
+    0x06,
+    0x68,
+    0x02,
+    0x18,
+    0x03,
+    0x10,
+    0x03,
+    0x10,
+    0x01,
+    0x90,
+    0x01,
+    0x90,
+    0x01,
+    0xE0,
+    0x01,
+    0xE0,
+    0x01,
+
+    /* @936 'W' (12 pixels wide) */
+    0x1E,
+    0x07,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0x12,
+    0x0D,
+    0xF2,
+    0x0D,
+    0x92,
+    0x0D,
+    0x12,
+    0x0D,
+    0x62,
+    0x0C,
+    0x62,
+    0x0C,
+    0xE2,
+    0x0C,
+    0xE2,
+    0x0C,
+    0xB2,
+    0x0D,
+    0x3E,
+    0x0F,
+    0x1C,
+    0x0F,
+
+    /* @972 'X' (12 pixels wide) */
+    0x1E,
+    0x07,
+    0x12,
+    0x0D,
+    0xB2,
+    0x0D,
+    0xA6,
+    0x0E,
+    0xE4,
+    0x0E,
+    0x6C,
+    0x0E,
+    0x18,
+    0x07,
+    0x98,
+    0x07,
+    0x90,
+    0x03,
+    0x10,
+    0x01,
+    0x68,
+    0x02,
+    0xE4,
+    0x02,
+    0xE4,
+    0x06,
+    0xE6,
+    0x06,
+    0xB2,
+    0x0D,
+    0x32,
+    0x0D,
+    0x1E,
+    0x0F,
+    0x1C,
+    0x0E,
+
+    /* @1008 'Y' (12 pixels wide) */
+    0x1E,
+    0x07,
+    0x12,
+    0x0D,
+    0xB2,
+    0x0D,
+    0xA6,
+    0x0E,
+    0xE4,
+    0x0E,
+    0x6C,
+    0x0E,
+    0x68,
+    0x06,
+    0x18,
+    0x07,
+    0x18,
+    0x03,
+    0x90,
+    0x03,
+    0x90,
+    0x01,
+    0x90,
+    0x01,
+    0xA0,
+    0x01,
+    0xA0,
+    0x01,
+    0xA0,
+    0x01,
+    0xE0,
+    0x01,
+    0xE0,
+    0x01,
+    0xE0,
+    0x01,
+};
+
+/* Character descriptors for Karma Future 14pt */
+/* { [Char width in bits], [Offset into karmaFuture_14ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO karmaFuture_14ptDescriptors[] = {
+    {12, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {12, 36}, /* 0 */
+    {12, 72}, /* 1 */
+    {12, 108}, /* 2 */
+    {12, 144}, /* 3 */
+    {12, 180}, /* 4 */
+    {12, 216}, /* 5 */
+    {12, 252}, /* 6 */
+    {12, 288}, /* 7 */
+    {12, 324}, /* 8 */
+    {12, 360}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {12, 396}, /* B */
+    {12, 432}, /* C */
+    {12, 468}, /* D */
+    {0, 0}, /* E */
+    {12, 504}, /* F */
+    {12, 540}, /* G */
+    {12, 576}, /* H */
+    {0, 0}, /* I */
+    {12, 612}, /* J */
+    {12, 648}, /* K */
+    {0, 0}, /* L */
+    {12, 684}, /* M */
+    {12, 720}, /* N */
+    {0, 0}, /* O */
+    {12, 756}, /* P */
+    {12, 792}, /* Q */
+    {12, 828}, /* R */
+    {0, 0}, /* S */
+    {12, 864}, /* T */
+    {0, 0}, /* U */
+    {12, 900}, /* V */
+    {12, 936}, /* W */
+    {12, 972}, /* X */
+    {12, 1008}, /* Y */
+};
+
+/* Font information for Karma Future 14pt */
+const FONT_INFO karmaFuture_14ptFontInfo = {
+    "Karma Future",
+    18, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    karmaFuture_14ptDescriptors, /*  Character descriptor array */
+    karmaFuture_14ptBitmaps, /*  Character bitmap array */
+};

+ 8 - 0
lib/fonts/karma_future/karma_future.h

@@ -0,0 +1,8 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+/* Font data for Karma Future 14pt */
+extern const FONT_INFO karmaFuture_14ptFontInfo;

+ 942 - 0
lib/fonts/mode_nine/mode_nine.c

@@ -0,0 +1,942 @@
+#include "mode_nine.h"
+#include <stdint.h>
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for ModeNine 15pt
+*/
+
+/* Character bitmaps for ModeNine 15pt */
+const uint8_t modeNine_15ptBitmaps[] = {
+    /* @0 '-' (10 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @28 '0' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xFE,
+    0x01,
+    0xFC,
+    0x00,
+
+    /* @56 '1' (10 pixels wide) */
+    0x30,
+    0x00,
+    0x38,
+    0x00,
+    0x3C,
+    0x00,
+    0x3C,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+
+    /* @84 '2' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x80,
+    0x03,
+    0xFC,
+    0x01,
+    0xFE,
+    0x00,
+    0x07,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+
+    /* @112 '3' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x80,
+    0x03,
+    0xC0,
+    0x01,
+    0xE0,
+    0x00,
+    0x70,
+    0x00,
+    0xF8,
+    0x00,
+    0xFC,
+    0x01,
+    0x80,
+    0x03,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xFE,
+    0x01,
+    0xFC,
+    0x00,
+
+    /* @140 '4' (10 pixels wide) */
+    0xE0,
+    0x00,
+    0xF0,
+    0x00,
+    0xF8,
+    0x00,
+    0xDC,
+    0x00,
+    0xCE,
+    0x00,
+    0xC7,
+    0x00,
+    0xC3,
+    0x00,
+    0xC3,
+    0x00,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+
+    /* @168 '5' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xFF,
+    0x00,
+    0xFF,
+    0x01,
+    0x80,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xFE,
+    0x01,
+    0xFC,
+    0x00,
+
+    /* @196 '6' (10 pixels wide) */
+    0xF0,
+    0x00,
+    0xFC,
+    0x00,
+    0x0E,
+    0x00,
+    0x06,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xFF,
+    0x00,
+    0xFF,
+    0x01,
+    0x83,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xFE,
+    0x01,
+    0xFC,
+    0x00,
+
+    /* @224 '7' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x00,
+    0x03,
+    0x80,
+    0x01,
+    0xC0,
+    0x01,
+    0xE0,
+    0x00,
+    0x30,
+    0x00,
+    0x18,
+    0x00,
+    0x1C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+
+    /* @252 '8' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xFE,
+    0x01,
+    0xFE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xFE,
+    0x01,
+    0xFC,
+    0x00,
+
+    /* @280 '9' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x07,
+    0x03,
+    0xFE,
+    0x03,
+    0xFC,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x80,
+    0x01,
+    0xC0,
+    0x01,
+    0xFC,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @308 'B' (10 pixels wide) */
+    0xFF,
+    0x00,
+    0xFF,
+    0x01,
+    0x83,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x83,
+    0x03,
+    0xFF,
+    0x01,
+    0xFF,
+    0x01,
+    0x83,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x83,
+    0x03,
+    0xFF,
+    0x01,
+    0xFF,
+    0x00,
+
+    /* @336 'C' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xFE,
+    0x01,
+    0xFC,
+    0x00,
+
+    /* @364 'D' (10 pixels wide) */
+    0xFF,
+    0x00,
+    0xFF,
+    0x01,
+    0x83,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x83,
+    0x03,
+    0xFF,
+    0x01,
+    0xFF,
+    0x00,
+
+    /* @392 'F' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xFF,
+    0x00,
+    0xFF,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+
+    /* @420 'G' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0xC3,
+    0x03,
+    0xC3,
+    0x03,
+    0x03,
+    0x03,
+    0x07,
+    0x03,
+    0xFE,
+    0x03,
+    0xFC,
+    0x03,
+
+    /* @448 'H' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @476 'J' (10 pixels wide) */
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xFE,
+    0x01,
+    0xFC,
+    0x00,
+
+    /* @504 'K' (10 pixels wide) */
+    0x83,
+    0x03,
+    0xC3,
+    0x01,
+    0xE3,
+    0x00,
+    0x73,
+    0x00,
+    0x3B,
+    0x00,
+    0x1F,
+    0x00,
+    0x0F,
+    0x00,
+    0x0F,
+    0x00,
+    0x1F,
+    0x00,
+    0x3B,
+    0x00,
+    0x73,
+    0x00,
+    0xE3,
+    0x00,
+    0xC3,
+    0x01,
+    0x83,
+    0x03,
+
+    /* @532 'M' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xCF,
+    0x03,
+    0xFF,
+    0x03,
+    0x7B,
+    0x03,
+    0x33,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @560 'N' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x07,
+    0x03,
+    0x0F,
+    0x03,
+    0x1F,
+    0x03,
+    0x3B,
+    0x03,
+    0x73,
+    0x03,
+    0xE3,
+    0x03,
+    0xC3,
+    0x03,
+    0x83,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @588 'P' (10 pixels wide) */
+    0xFF,
+    0x00,
+    0xFF,
+    0x01,
+    0x83,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x83,
+    0x03,
+    0xFF,
+    0x01,
+    0xFF,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+
+    /* @616 'Q' (10 pixels wide) */
+    0xFC,
+    0x00,
+    0xFE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x33,
+    0x03,
+    0x73,
+    0x03,
+    0xE7,
+    0x03,
+    0xFE,
+    0x01,
+    0xFC,
+    0x03,
+
+    /* @644 'R' (10 pixels wide) */
+    0xFF,
+    0x00,
+    0xFF,
+    0x01,
+    0x83,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x83,
+    0x03,
+    0xFF,
+    0x01,
+    0xFF,
+    0x00,
+    0x1F,
+    0x00,
+    0x3B,
+    0x00,
+    0x73,
+    0x00,
+    0xE3,
+    0x00,
+    0xC3,
+    0x01,
+    0x83,
+    0x03,
+
+    /* @672 'T' (10 pixels wide) */
+    0xFF,
+    0x03,
+    0xFF,
+    0x03,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+
+    /* @700 'V' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x86,
+    0x01,
+    0x86,
+    0x01,
+    0xCC,
+    0x00,
+    0xCC,
+    0x00,
+    0x78,
+    0x00,
+    0x78,
+    0x00,
+    0x30,
+    0x00,
+
+    /* @728 'W' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0x33,
+    0x03,
+    0xFF,
+    0x03,
+    0xFE,
+    0x01,
+
+    /* @756 'X' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xCE,
+    0x01,
+    0xFC,
+    0x00,
+    0xFC,
+    0x00,
+    0xCE,
+    0x01,
+    0x87,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+
+    /* @784 'Y' (10 pixels wide) */
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x03,
+    0x87,
+    0x03,
+    0xCE,
+    0x01,
+    0xFC,
+    0x00,
+    0x78,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+    0x30,
+    0x00,
+};
+
+/* Character descriptors for ModeNine 15pt */
+/* { [Char width in bits], [Offset into modeNine_15ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO modeNine_15ptDescriptors[] = {
+    {10, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {10, 28}, /* 0 */
+    {10, 56}, /* 1 */
+    {10, 84}, /* 2 */
+    {10, 112}, /* 3 */
+    {10, 140}, /* 4 */
+    {10, 168}, /* 5 */
+    {10, 196}, /* 6 */
+    {10, 224}, /* 7 */
+    {10, 252}, /* 8 */
+    {10, 280}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {10, 308}, /* B */
+    {10, 336}, /* C */
+    {10, 364}, /* D */
+    {0, 0}, /* E */
+    {10, 392}, /* F */
+    {10, 420}, /* G */
+    {10, 448}, /* H */
+    {0, 0}, /* I */
+    {10, 476}, /* J */
+    {10, 504}, /* K */
+    {0, 0}, /* L */
+    {10, 532}, /* M */
+    {10, 560}, /* N */
+    {0, 0}, /* O */
+    {10, 588}, /* P */
+    {10, 616}, /* Q */
+    {10, 644}, /* R */
+    {0, 0}, /* S */
+    {10, 672}, /* T */
+    {0, 0}, /* U */
+    {10, 700}, /* V */
+    {10, 728}, /* W */
+    {10, 756}, /* X */
+    {10, 784}, /* Y */
+};
+
+/* Font information for ModeNine 15pt */
+const FONT_INFO modeNine_15ptFontInfo = {
+    "Mode Nine",
+    14, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    modeNine_15ptDescriptors, /*  Character descriptor array */
+    modeNine_15ptBitmaps, /*  Character bitmap array */
+};

+ 8 - 0
lib/fonts/mode_nine/mode_nine.h

@@ -0,0 +1,8 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+/* Font data for ModeNine 15pt */
+extern const FONT_INFO modeNine_15ptFontInfo;

+ 1115 - 0
lib/fonts/pixelflag/pixelflag.c

@@ -0,0 +1,1115 @@
+#include "pixelflag.h"
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for {PixelFlag} 18pt
+*/
+
+/* Character bitmaps for {PixelFlag} 18pt */
+const uint8_t pixelFlag_18ptBitmaps[] = {
+    /* @0 '-' (13 pixels wide) */
+    0xFE,
+    0x07,
+    0xFE,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xFE,
+    0x07,
+    0xFE,
+    0x07,
+
+    /* @34 '0' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @68 '1' (13 pixels wide) */
+    0xff,
+    0xff,
+    0xff,
+    0xff,
+    0x00,
+    0xe0,
+    0xc0,
+    0xe0,
+    0xc0,
+    0xe0,
+    0xe0,
+    0xe0,
+    0xe0,
+    0xe0,
+    0xc0,
+    0xe0,
+    0xc0,
+    0xe0,
+    0xc0,
+    0xe0,
+    0xc0,
+    0xe0,
+    0xc0,
+    0xe0,
+    0xe0,
+    0xe3,
+    0xe0,
+    0xe3,
+    0x00,
+    0xe0,
+    0xff,
+    0xff,
+    0xff,
+    0xff,
+
+    /* @102 '2' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0xF8,
+    0x07,
+    0xFC,
+    0x07,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x1F,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @136 '3' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0xE0,
+    0x07,
+    0xE0,
+    0x1F,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @170 '4' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x07,
+    0xC0,
+    0x07,
+    0xC0,
+    0x06,
+    0x60,
+    0x06,
+    0x78,
+    0x06,
+    0x18,
+    0x06,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x1F,
+    0x00,
+    0x06,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @204 '5' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x1F,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0xFC,
+    0x07,
+    0xFC,
+    0x1F,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @238 '6' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x00,
+    0xFC,
+    0x07,
+    0xFC,
+    0x1F,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @272 '7' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x1F,
+    0x00,
+    0x06,
+    0x00,
+    0x07,
+    0x00,
+    0x03,
+    0xC0,
+    0x00,
+    0xE0,
+    0x00,
+    0x60,
+    0x00,
+    0x18,
+    0x00,
+    0x1C,
+    0x00,
+    0x0C,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @306 '8' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xF8,
+    0x07,
+    0xFC,
+    0x1F,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @340 '9' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xF8,
+    0x1F,
+    0xF8,
+    0x1F,
+    0x00,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @374 'B' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x07,
+    0xFC,
+    0x1F,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @408 'C' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @442 'D' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @476 'F' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x1F,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0xFC,
+    0x03,
+    0xFC,
+    0x03,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @510 'G' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x00,
+    0xCC,
+    0x1F,
+    0xCC,
+    0x1F,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @544 'H' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x1F,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @578 'J' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x1F,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @612 'K' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x06,
+    0x0C,
+    0x07,
+    0x0C,
+    0x03,
+    0xFC,
+    0x00,
+    0xFC,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x06,
+    0x0C,
+    0x1E,
+    0x0C,
+    0x18,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @646 'M' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x1C,
+    0x1E,
+    0x7C,
+    0x1F,
+    0x6C,
+    0x1B,
+    0xCC,
+    0x18,
+    0xCC,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @680 'N' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x1C,
+    0x18,
+    0x7C,
+    0x18,
+    0x6C,
+    0x18,
+    0xCC,
+    0x18,
+    0xCC,
+    0x1B,
+    0x0C,
+    0x1B,
+    0x0C,
+    0x1E,
+    0x0C,
+    0x1E,
+    0x0C,
+    0x18,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @714 'P' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @748 'Q' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xF8,
+    0x07,
+    0xF8,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xCC,
+    0x18,
+    0xCC,
+    0x1B,
+    0x0C,
+    0x1B,
+    0x0C,
+    0x1E,
+    0xFC,
+    0x1F,
+    0xF8,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @782 'R' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xFC,
+    0x07,
+    0xFC,
+    0x1F,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @816 'T' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0xFC,
+    0x1F,
+    0xFC,
+    0x1F,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @850 'V' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x1C,
+    0x1E,
+    0x18,
+    0x06,
+    0x60,
+    0x03,
+    0xE0,
+    0x03,
+    0xC0,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @884 'W' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0xCC,
+    0x18,
+    0xCC,
+    0x18,
+    0xCC,
+    0x18,
+    0xCC,
+    0x18,
+    0xCC,
+    0x18,
+    0xCC,
+    0x18,
+    0xFC,
+    0x1F,
+    0x78,
+    0x07,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @918 'X' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x18,
+    0x06,
+    0x78,
+    0x07,
+    0x60,
+    0x03,
+    0xC0,
+    0x00,
+    0xE0,
+    0x03,
+    0x60,
+    0x03,
+    0x18,
+    0x06,
+    0x1C,
+    0x1E,
+    0x0C,
+    0x18,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+
+    /* @952 'Y' (13 pixels wide) */
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+    0x00,
+    0x00,
+    0x0C,
+    0x18,
+    0x0C,
+    0x18,
+    0x18,
+    0x06,
+    0x78,
+    0x07,
+    0x60,
+    0x03,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0xC0,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x1F,
+    0xFF,
+    0x1F,
+};
+
+/* Character descriptors for {PixelFlag} 18pt */
+/* { [Char width in bits], [Offset into pixelFlag_18ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO pixelFlag_18ptDescriptors[] = {
+    {13, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {13, 34}, /* 0 */
+    {13, 68}, /* 1 */
+    {13, 102}, /* 2 */
+    {13, 136}, /* 3 */
+    {13, 170}, /* 4 */
+    {13, 204}, /* 5 */
+    {13, 238}, /* 6 */
+    {13, 272}, /* 7 */
+    {13, 306}, /* 8 */
+    {13, 340}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {13, 374}, /* B */
+    {13, 408}, /* C */
+    {13, 442}, /* D */
+    {0, 0}, /* E */
+    {13, 476}, /* F */
+    {13, 510}, /* G */
+    {13, 544}, /* H */
+    {0, 0}, /* I */
+    {13, 578}, /* J */
+    {13, 612}, /* K */
+    {0, 0}, /* L */
+    {13, 646}, /* M */
+    {13, 680}, /* N */
+    {0, 0}, /* O */
+    {13, 714}, /* P */
+    {13, 748}, /* Q */
+    {13, 782}, /* R */
+    {0, 0}, /* S */
+    {13, 816}, /* T */
+    {0, 0}, /* U */
+    {13, 850}, /* V */
+    {13, 884}, /* W */
+    {13, 918}, /* X */
+    {13, 952}, /* Y */
+};
+
+/* Font information for {PixelFlag} 18pt */
+const FONT_INFO pixelFlag_18ptFontInfo = {
+    "Pixel Flag",
+    17, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    0, /*  Width, in pixels, of space character */
+    pixelFlag_18ptDescriptors, /*  Character descriptor array */
+    pixelFlag_18ptBitmaps, /*  Character bitmap array */
+};

+ 7 - 0
lib/fonts/pixelflag/pixelflag.h

@@ -0,0 +1,7 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+extern const FONT_INFO pixelFlag_18ptFontInfo;

+ 1058 - 0
lib/fonts/redhat_mono/redhat_mono.c

@@ -0,0 +1,1058 @@
+#include "redhat_mono.h"
+#include <stdint.h>
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for Red Hat Mono 16pt
+*/
+
+/* Character bitmaps for Red Hat Mono 16pt */
+const uint8_t redHatMono_16ptBitmaps[] = {
+    /* @0 '-' (12 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xF8,
+    0x03,
+    0xF8,
+    0x03,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @32 '0' (12 pixels wide) */
+    0xF0,
+    0x00,
+    0xF8,
+    0x03,
+    0x1C,
+    0x07,
+    0x0E,
+    0x07,
+    0x8E,
+    0x07,
+    0x86,
+    0x0E,
+    0xC6,
+    0x0C,
+    0x66,
+    0x0C,
+    0x66,
+    0x0C,
+    0x36,
+    0x0E,
+    0x36,
+    0x0E,
+    0x1E,
+    0x06,
+    0x1C,
+    0x07,
+    0xF8,
+    0x03,
+    0xF0,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @64 '1' (12 pixels wide) */
+    0x40,
+    0x00,
+    0x78,
+    0x00,
+    0x7E,
+    0x00,
+    0x66,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0x60,
+    0x00,
+    0xFE,
+    0x07,
+    0xFE,
+    0x07,
+    0x00,
+    0x00,
+
+    /* @96 '2' (12 pixels wide) */
+    0xF0,
+    0x01,
+    0xFC,
+    0x03,
+    0x0E,
+    0x07,
+    0x04,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x07,
+    0x00,
+    0x03,
+    0x80,
+    0x03,
+    0xC0,
+    0x01,
+    0xE0,
+    0x00,
+    0x70,
+    0x00,
+    0x38,
+    0x00,
+    0x1C,
+    0x00,
+    0xFE,
+    0x07,
+    0xFE,
+    0x07,
+    0x00,
+    0x00,
+
+    /* @128 '3' (12 pixels wide) */
+    0xF0,
+    0x00,
+    0xFC,
+    0x03,
+    0x0E,
+    0x07,
+    0x04,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x80,
+    0x03,
+    0xE0,
+    0x00,
+    0xE0,
+    0x03,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x0E,
+    0x06,
+    0x07,
+    0xFE,
+    0x07,
+    0xF8,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @160 '4' (12 pixels wide) */
+    0x00,
+    0x03,
+    0x80,
+    0x03,
+    0xC0,
+    0x03,
+    0xE0,
+    0x03,
+    0x70,
+    0x03,
+    0x30,
+    0x03,
+    0x18,
+    0x03,
+    0x0C,
+    0x03,
+    0x0E,
+    0x03,
+    0xFF,
+    0x0F,
+    0xFF,
+    0x0F,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x03,
+    0x00,
+    0x00,
+
+    /* @192 '5' (12 pixels wide) */
+    0xFC,
+    0x07,
+    0xFC,
+    0x07,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0xFC,
+    0x01,
+    0xFE,
+    0x07,
+    0x00,
+    0x07,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x06,
+    0x07,
+    0xFE,
+    0x03,
+    0xF8,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @224 '6' (12 pixels wide) */
+    0xE0,
+    0x03,
+    0xF8,
+    0x07,
+    0x3C,
+    0x06,
+    0x0C,
+    0x00,
+    0x0E,
+    0x00,
+    0x06,
+    0x00,
+    0xE6,
+    0x03,
+    0xFE,
+    0x07,
+    0x0E,
+    0x0E,
+    0x06,
+    0x0C,
+    0x06,
+    0x0C,
+    0x0E,
+    0x0E,
+    0x1C,
+    0x06,
+    0xF8,
+    0x07,
+    0xF0,
+    0x03,
+    0x00,
+    0x00,
+
+    /* @256 '7' (12 pixels wide) */
+    0xFE,
+    0x0F,
+    0xFE,
+    0x0F,
+    0x00,
+    0x0E,
+    0x00,
+    0x06,
+    0x00,
+    0x07,
+    0x00,
+    0x03,
+    0x80,
+    0x01,
+    0xC0,
+    0x01,
+    0xC0,
+    0x00,
+    0xE0,
+    0x00,
+    0x60,
+    0x00,
+    0x70,
+    0x00,
+    0x30,
+    0x00,
+    0x38,
+    0x00,
+    0x1C,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @288 '8' (12 pixels wide) */
+    0xF0,
+    0x01,
+    0xFC,
+    0x03,
+    0x1C,
+    0x07,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0C,
+    0x06,
+    0xBC,
+    0x03,
+    0xF0,
+    0x01,
+    0x1C,
+    0x07,
+    0x0E,
+    0x06,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x0E,
+    0x06,
+    0xFC,
+    0x07,
+    0xF8,
+    0x03,
+    0x00,
+    0x00,
+
+    /* @320 '9' (12 pixels wide) */
+    0xF8,
+    0x01,
+    0xFC,
+    0x03,
+    0x0C,
+    0x07,
+    0x0E,
+    0x0E,
+    0x06,
+    0x0C,
+    0x06,
+    0x0C,
+    0x0E,
+    0x0E,
+    0xFC,
+    0x0F,
+    0xF8,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0E,
+    0x00,
+    0x06,
+    0x8C,
+    0x07,
+    0xFC,
+    0x03,
+    0xF8,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @352 'B' (12 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x03,
+    0x8C,
+    0x07,
+    0x0C,
+    0x06,
+    0x0C,
+    0x06,
+    0x0C,
+    0x07,
+    0xFC,
+    0x03,
+    0xFC,
+    0x01,
+    0x8C,
+    0x07,
+    0x0C,
+    0x06,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x07,
+    0xFC,
+    0x07,
+    0xFC,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @384 'C' (12 pixels wide) */
+    0xF0,
+    0x01,
+    0xF8,
+    0x03,
+    0xBC,
+    0x07,
+    0x0C,
+    0x06,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x1C,
+    0x07,
+    0xF8,
+    0x03,
+    0xF0,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @416 'D' (12 pixels wide) */
+    0x7E,
+    0x00,
+    0xFE,
+    0x01,
+    0xCE,
+    0x03,
+    0x0E,
+    0x07,
+    0x0E,
+    0x06,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x06,
+    0x0C,
+    0x07,
+    0x8C,
+    0x07,
+    0xFC,
+    0x03,
+    0xFC,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @448 'F' (12 pixels wide) */
+    0xFC,
+    0x0F,
+    0xFC,
+    0x0F,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0xFC,
+    0x01,
+    0xFC,
+    0x01,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @480 'G' (12 pixels wide) */
+    0xF0,
+    0x01,
+    0xF8,
+    0x03,
+    0xBC,
+    0x07,
+    0x0C,
+    0x06,
+    0x0E,
+    0x0E,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x86,
+    0x0F,
+    0x86,
+    0x0F,
+    0x06,
+    0x0C,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x06,
+    0x1C,
+    0x07,
+    0xF8,
+    0x03,
+    0xF0,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @512 'H' (12 pixels wide) */
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0xFE,
+    0x07,
+    0xFE,
+    0x07,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x00,
+    0x00,
+
+    /* @544 'J' (12 pixels wide) */
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x00,
+    0x06,
+    0x04,
+    0x06,
+    0x07,
+    0x07,
+    0x8E,
+    0x07,
+    0xFC,
+    0x03,
+    0xF8,
+    0x01,
+    0x00,
+    0x00,
+
+    /* @576 'K' (12 pixels wide) */
+    0x0E,
+    0x0E,
+    0x0E,
+    0x07,
+    0x0E,
+    0x03,
+    0x8E,
+    0x01,
+    0xCE,
+    0x01,
+    0xEE,
+    0x00,
+    0x7E,
+    0x00,
+    0x3E,
+    0x00,
+    0x7E,
+    0x00,
+    0xEE,
+    0x00,
+    0xCE,
+    0x01,
+    0x8E,
+    0x03,
+    0x8E,
+    0x03,
+    0x0E,
+    0x07,
+    0x0E,
+    0x0E,
+    0x00,
+    0x00,
+
+    /* @608 'M' (12 pixels wide) */
+    0x0E,
+    0x0E,
+    0x0E,
+    0x0F,
+    0x1E,
+    0x0F,
+    0x9E,
+    0x0F,
+    0xB6,
+    0x0F,
+    0xB6,
+    0x0F,
+    0xF6,
+    0x0E,
+    0xE6,
+    0x0E,
+    0x66,
+    0x0E,
+    0x46,
+    0x0E,
+    0x26,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x00,
+    0x00,
+
+    /* @640 'N' (12 pixels wide) */
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x1E,
+    0x06,
+    0x3E,
+    0x06,
+    0x3E,
+    0x06,
+    0x76,
+    0x06,
+    0x66,
+    0x06,
+    0xE6,
+    0x06,
+    0xC6,
+    0x06,
+    0xC6,
+    0x07,
+    0x86,
+    0x07,
+    0x86,
+    0x07,
+    0x06,
+    0x07,
+    0x06,
+    0x07,
+    0x06,
+    0x06,
+    0x00,
+    0x00,
+
+    /* @672 'P' (12 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x03,
+    0x8C,
+    0x07,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x06,
+    0xFC,
+    0x07,
+    0xFC,
+    0x03,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x0C,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @704 'Q' (12 pixels wide) */
+    0xF0,
+    0x00,
+    0xF8,
+    0x03,
+    0x9C,
+    0x07,
+    0x0E,
+    0x07,
+    0x0E,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0x06,
+    0x0E,
+    0xEE,
+    0x0E,
+    0xCE,
+    0x07,
+    0x9C,
+    0x07,
+    0xFC,
+    0x07,
+    0xF0,
+    0x07,
+    0x00,
+    0x0A,
+
+    /* @736 'R' (12 pixels wide) */
+    0xFC,
+    0x00,
+    0xFC,
+    0x03,
+    0x8C,
+    0x07,
+    0x0C,
+    0x06,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x0E,
+    0x0C,
+    0x07,
+    0xFC,
+    0x07,
+    0xFC,
+    0x01,
+    0x8C,
+    0x01,
+    0x8C,
+    0x03,
+    0x0C,
+    0x03,
+    0x0C,
+    0x07,
+    0x0C,
+    0x06,
+    0x0C,
+    0x0E,
+    0x00,
+    0x00,
+
+    /* @768 'T' (12 pixels wide) */
+    0xFF,
+    0x0F,
+    0xFF,
+    0x0F,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @800 'V' (12 pixels wide) */
+    0x07,
+    0x0C,
+    0x06,
+    0x0C,
+    0x06,
+    0x0E,
+    0x0E,
+    0x06,
+    0x0C,
+    0x06,
+    0x0C,
+    0x07,
+    0x1C,
+    0x03,
+    0x1C,
+    0x03,
+    0x98,
+    0x03,
+    0x98,
+    0x03,
+    0xB8,
+    0x01,
+    0xB0,
+    0x01,
+    0xF0,
+    0x01,
+    0xF0,
+    0x00,
+    0xE0,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @832 'W' (12 pixels wide) */
+    0x67,
+    0x0C,
+    0xE6,
+    0x0C,
+    0xE6,
+    0x0C,
+    0xE6,
+    0x0C,
+    0xE6,
+    0x0C,
+    0xF6,
+    0x0C,
+    0xB6,
+    0x0E,
+    0xB6,
+    0x07,
+    0xB6,
+    0x07,
+    0x96,
+    0x07,
+    0x9C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x1C,
+    0x07,
+    0x0C,
+    0x07,
+    0x00,
+    0x00,
+
+    /* @864 'X' (12 pixels wide) */
+    0x0E,
+    0x0E,
+    0x0E,
+    0x06,
+    0x1C,
+    0x07,
+    0x98,
+    0x03,
+    0xB8,
+    0x01,
+    0xF0,
+    0x01,
+    0xF0,
+    0x00,
+    0xE0,
+    0x00,
+    0xF0,
+    0x00,
+    0xF0,
+    0x01,
+    0xB8,
+    0x03,
+    0x9C,
+    0x03,
+    0x0C,
+    0x07,
+    0x0E,
+    0x06,
+    0x07,
+    0x0E,
+    0x00,
+    0x00,
+
+    /* @896 'Y' (12 pixels wide) */
+    0x07,
+    0x0C,
+    0x0E,
+    0x0E,
+    0x0E,
+    0x06,
+    0x1C,
+    0x07,
+    0x18,
+    0x03,
+    0xB8,
+    0x03,
+    0xF0,
+    0x01,
+    0xF0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0xE0,
+    0x00,
+    0x00,
+    0x00,
+};
+
+/* Character descriptors for Red Hat Mono 16pt */
+/* { [Char width in bits], [Offset into redHatMono_16ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO redHatMono_16ptDescriptors[] = {
+    {12, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {12, 32}, /* 0 */
+    {12, 64}, /* 1 */
+    {12, 96}, /* 2 */
+    {12, 128}, /* 3 */
+    {12, 160}, /* 4 */
+    {12, 192}, /* 5 */
+    {12, 224}, /* 6 */
+    {12, 256}, /* 7 */
+    {12, 288}, /* 8 */
+    {12, 320}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {12, 352}, /* B */
+    {12, 384}, /* C */
+    {12, 416}, /* D */
+    {0, 0}, /* E */
+    {12, 448}, /* F */
+    {12, 480}, /* G */
+    {12, 512}, /* H */
+    {0, 0}, /* I */
+    {12, 544}, /* J */
+    {12, 576}, /* K */
+    {0, 0}, /* L */
+    {12, 608}, /* M */
+    {12, 640}, /* N */
+    {0, 0}, /* O */
+    {12, 672}, /* P */
+    {12, 704}, /* Q */
+    {12, 736}, /* R */
+    {0, 0}, /* S */
+    {12, 768}, /* T */
+    {0, 0}, /* U */
+    {12, 800}, /* V */
+    {12, 832}, /* W */
+    {12, 864}, /* X */
+    {12, 896}, /* Y */
+};
+
+/* Font information for Red Hat Mono 16pt */
+const FONT_INFO redHatMono_16ptFontInfo = {
+    "RedHat Mono",
+    16, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    redHatMono_16ptDescriptors, /*  Character descriptor array */
+    redHatMono_16ptBitmaps, /*  Character bitmap array */
+};

+ 8 - 0
lib/fonts/redhat_mono/redhat_mono.h

@@ -0,0 +1,8 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+/* Font data for Redhat Mono 16pt */
+extern const FONT_INFO redHatMono_16ptFontInfo;

+ 1057 - 0
lib/fonts/zector/zector.c

@@ -0,0 +1,1057 @@
+#include "zector.h"
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+/* 
+**  Font data for Zector 18pt
+*/
+
+/* Character bitmaps for Zector 18pt */
+const uint8_t zector_18ptBitmaps[] = {
+    /* @0 '-' (9 pixels wide) */
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xFF,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+
+    /* @32 '0' (9 pixels wide) */
+    0x3C,
+    0x00,
+    0xC2,
+    0x00,
+    0x83,
+    0x00,
+    0x85,
+    0x00,
+    0x85,
+    0x00,
+    0x89,
+    0x00,
+    0x89,
+    0x00,
+    0x91,
+    0x00,
+    0x91,
+    0x00,
+    0xA1,
+    0x00,
+    0xA1,
+    0x00,
+    0xC1,
+    0x00,
+    0xC1,
+    0x00,
+    0x81,
+    0x00,
+    0x42,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @64 '1' (9 pixels wide) */
+    0x10,
+    0x00,
+    0x18,
+    0x00,
+    0x18,
+    0x00,
+    0x14,
+    0x00,
+    0x14,
+    0x00,
+    0x12,
+    0x00,
+    0x12,
+    0x00,
+    0x11,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0xFF,
+    0x00,
+
+    /* @96 '2' (9 pixels wide) */
+    0x3C,
+    0x00,
+    0x42,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x40,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x10,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0x04,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0xFF,
+    0x00,
+
+    /* @128 '3' (9 pixels wide) */
+    0xFF,
+    0x01,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x40,
+    0x00,
+    0x40,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x10,
+    0x00,
+    0x60,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x00,
+    0x01,
+    0x01,
+    0x01,
+    0x81,
+    0x00,
+    0x42,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @160 '4' (9 pixels wide) */
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0xFF,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+
+    /* @192 '5' (9 pixels wide) */
+    0xFF,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x3F,
+    0x00,
+    0x40,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0xC2,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @224 '6' (9 pixels wide) */
+    0x3C,
+    0x00,
+    0x42,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x3D,
+    0x00,
+    0x43,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0xC2,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @256 '7' (9 pixels wide) */
+    0xFF,
+    0x01,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x40,
+    0x00,
+    0x40,
+    0x00,
+    0x20,
+    0x00,
+    0x20,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x08,
+    0x00,
+    0x08,
+    0x00,
+    0x04,
+    0x00,
+    0x04,
+    0x00,
+    0x02,
+    0x00,
+    0x02,
+    0x00,
+    0x01,
+    0x00,
+
+    /* @288 '8' (9 pixels wide) */
+    0x3C,
+    0x00,
+    0xC2,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x42,
+    0x00,
+    0x3C,
+    0x00,
+    0x42,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x42,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @320 '9' (9 pixels wide) */
+    0x3C,
+    0x00,
+    0x42,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0xC2,
+    0x00,
+    0xBC,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x42,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @352 'B' (9 pixels wide) */
+    0x3F,
+    0x00,
+    0xC1,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x41,
+    0x00,
+    0x3F,
+    0x00,
+    0x41,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x41,
+    0x00,
+    0x3F,
+    0x00,
+
+    /* @384 'C' (9 pixels wide) */
+    0x3C,
+    0x00,
+    0x42,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x42,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @416 'D' (9 pixels wide) */
+    0x3F,
+    0x00,
+    0x41,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x41,
+    0x00,
+    0x3F,
+    0x00,
+
+    /* @448 'F' (9 pixels wide) */
+    0xFF,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0xFF,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+
+    /* @480 'G' (9 pixels wide) */
+    0x3C,
+    0x00,
+    0xC2,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0xF1,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x42,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @512 'H' (9 pixels wide) */
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0xFF,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+
+    /* @544 'J' (9 pixels wide) */
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x80,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x42,
+    0x00,
+    0x3C,
+    0x00,
+
+    /* @576 'K' (9 pixels wide) */
+    0x81,
+    0x00,
+    0xC1,
+    0x00,
+    0x61,
+    0x00,
+    0x21,
+    0x00,
+    0x11,
+    0x00,
+    0x09,
+    0x00,
+    0x05,
+    0x00,
+    0x03,
+    0x00,
+    0x01,
+    0x00,
+    0x03,
+    0x00,
+    0x05,
+    0x00,
+    0x09,
+    0x00,
+    0x11,
+    0x00,
+    0x21,
+    0x00,
+    0x41,
+    0x00,
+    0x81,
+    0x00,
+
+    /* @608 'M' (9 pixels wide) */
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x83,
+    0x00,
+    0xC3,
+    0x00,
+    0xC5,
+    0x00,
+    0xA5,
+    0x00,
+    0xA9,
+    0x00,
+    0x91,
+    0x00,
+    0x91,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+
+    /* @640 'N' (9 pixels wide) */
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x83,
+    0x00,
+    0x83,
+    0x00,
+    0x85,
+    0x00,
+    0x85,
+    0x00,
+    0x89,
+    0x00,
+    0x89,
+    0x00,
+    0x91,
+    0x00,
+    0xA1,
+    0x00,
+    0xA1,
+    0x00,
+    0xC1,
+    0x00,
+    0xC1,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+
+    /* @672 'P' (9 pixels wide) */
+    0x3F,
+    0x00,
+    0x41,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x41,
+    0x00,
+    0x3F,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+    0x01,
+    0x00,
+
+    /* @704 'Q' (9 pixels wide) */
+    0x3C,
+    0x00,
+    0x42,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x91,
+    0x00,
+    0xA1,
+    0x00,
+    0xC2,
+    0x00,
+    0xBC,
+    0x00,
+
+    /* @736 'R' (9 pixels wide) */
+    0x3F,
+    0x00,
+    0x41,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x41,
+    0x00,
+    0x3F,
+    0x00,
+    0x03,
+    0x00,
+    0x05,
+    0x00,
+    0x09,
+    0x00,
+    0x11,
+    0x00,
+    0x21,
+    0x00,
+    0x41,
+    0x00,
+    0x81,
+    0x00,
+
+    /* @768 'T' (9 pixels wide) */
+    0xFF,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+
+    /* @800 'V' (9 pixels wide) */
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x82,
+    0x00,
+    0x42,
+    0x00,
+    0x44,
+    0x00,
+    0x24,
+    0x00,
+    0x28,
+    0x00,
+    0x18,
+    0x00,
+    0x10,
+    0x00,
+
+    /* @832 'W' (9 pixels wide) */
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+    0x91,
+    0x00,
+    0x91,
+    0x00,
+    0x99,
+    0x00,
+    0xA9,
+    0x00,
+    0xA5,
+    0x00,
+    0xC5,
+    0x00,
+    0x83,
+    0x00,
+    0x81,
+    0x00,
+    0x81,
+    0x00,
+
+    /* @864 'X' (9 pixels wide) */
+    0x81,
+    0x00,
+    0x82,
+    0x00,
+    0x82,
+    0x00,
+    0x44,
+    0x00,
+    0x44,
+    0x00,
+    0x28,
+    0x00,
+    0x28,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x28,
+    0x00,
+    0x28,
+    0x00,
+    0x44,
+    0x00,
+    0x44,
+    0x00,
+    0x82,
+    0x00,
+    0x82,
+    0x00,
+    0x81,
+    0x00,
+
+    /* @896 'Y' (9 pixels wide) */
+    0x81,
+    0x00,
+    0x82,
+    0x00,
+    0x82,
+    0x00,
+    0x44,
+    0x00,
+    0x44,
+    0x00,
+    0x28,
+    0x00,
+    0x28,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+    0x10,
+    0x00,
+};
+
+/* Character descriptors for Zector 18pt */
+/* { [Char width in bits], [Offset into zector_18ptCharBitmaps in bytes] } */
+const FONT_CHAR_INFO zector_18ptDescriptors[] = {
+    {9, 0}, /* - */
+    {0, 0}, /* . */
+    {0, 0}, /* / */
+    {9, 32}, /* 0 */
+    {9, 64}, /* 1 */
+    {9, 96}, /* 2 */
+    {9, 128}, /* 3 */
+    {9, 160}, /* 4 */
+    {9, 192}, /* 5 */
+    {9, 224}, /* 6 */
+    {9, 256}, /* 7 */
+    {9, 288}, /* 8 */
+    {9, 320}, /* 9 */
+    {0, 0}, /* : */
+    {0, 0}, /* ; */
+    {0, 0}, /* < */
+    {0, 0}, /* = */
+    {0, 0}, /* > */
+    {0, 0}, /* ? */
+    {0, 0}, /* @ */
+    {0, 0}, /* A */
+    {9, 352}, /* B */
+    {9, 384}, /* C */
+    {9, 416}, /* D */
+    {0, 0}, /* E */
+    {9, 448}, /* F */
+    {9, 480}, /* G */
+    {9, 512}, /* H */
+    {0, 0}, /* I */
+    {9, 544}, /* J */
+    {9, 576}, /* K */
+    {0, 0}, /* L */
+    {9, 608}, /* M */
+    {9, 640}, /* N */
+    {0, 0}, /* O */
+    {9, 672}, /* P */
+    {9, 704}, /* Q */
+    {9, 736}, /* R */
+    {0, 0}, /* S */
+    {9, 768}, /* T */
+    {0, 0}, /* U */
+    {9, 800}, /* V */
+    {9, 832}, /* W */
+    {9, 864}, /* X */
+    {9, 896}, /* Y */
+};
+
+/* Font information for Zector 18pt */
+const FONT_INFO zector_18ptFontInfo = {
+    "Zector",
+    16, /*  Character height */
+    '-', /*  Start character */
+    'Y', /*  End character */
+    2, /*  Width, in pixels, of space character */
+    zector_18ptDescriptors, /*  Character descriptor array */
+    zector_18ptBitmaps, /*  Character bitmap array */
+};

+ 8 - 0
lib/fonts/zector/zector.h

@@ -0,0 +1,8 @@
+#pragma once
+
+/* GENERATED BY https://github.com/pavius/the-dot-factory */
+
+#include "../font_info.h"
+
+/* Font information for Zector 18pt */
+extern const FONT_INFO zector_18ptFontInfo;

+ 22 - 0
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;
+}

+ 23 - 0
lib/polyfills/memset_s.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <errno.h>
+#include <stdint.h>
+
+#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);

+ 11 - 0
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
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
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
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
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
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
lib/wolfssl

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

+ 728 - 0
services/config/config.c

@@ -0,0 +1,728 @@
+#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 {
+        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;
+}

+ 104 - 0
services/config/config.h

@@ -0,0 +1,104 @@
+#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"
+
+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);

+ 3 - 0
services/config/config_file_context.h

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

+ 24 - 0
services/config/constants.h

@@ -0,0 +1,24 @@
+#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 (9)
+
+#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_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"

+ 199 - 0
services/config/migrations/common_migration.c

@@ -0,0 +1,199 @@
+#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);
+            }
+        }
+
+        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
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);

+ 552 - 0
services/config/token_info_iterator.c

@@ -0,0 +1,552 @@
+#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;
+        }
+
+        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_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;
+    }
+
+    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);
+}

+ 123 - 0
services/config/token_info_iterator.h

@@ -0,0 +1,123 @@
+#pragma once
+
+#include "../../types/token_info.h"
+#include <flipper_format/flipper_format.h>
+#include "constants.h"
+
+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 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);

+ 4 - 0
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
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
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
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");
+}

+ 59 - 0
services/crypto/crypto_facade.h

@@ -0,0 +1,59 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stddef.h>
+#include "../../types/crypto_settings.h"
+#include "common_types.h"
+
+/**
+ * @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);

+ 145 - 0
services/crypto/crypto_v1.c

@@ -0,0 +1,145 @@
+#include "crypto_v1.h"
+#ifdef TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+#include <stdlib.h>
+#include <furi.h>
+#include <furi_hal_crypto.h>
+#include <furi_hal_random.h>
+#include <furi_hal_version.h>
+#include "../../types/common.h"
+#include "memset_s.h"
+#include "polyfills.h"
+
+#define CRYPTO_KEY_SLOT (2)
+#define CRYPTO_VERIFY_KEY_LENGTH (16)
+#define CRYPTO_ALIGNMENT_FACTOR (16)
+#define TOTP_IV_SIZE (16)
+
+static const char* CRYPTO_VERIFY_KEY = "FFF_Crypto_pass";
+
+uint8_t* totp_crypto_encrypt_v1(
+    const uint8_t* plain_data,
+    const size_t plain_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* encrypted_data_length) {
+    uint8_t* encrypted_data;
+    size_t remain = plain_data_length % CRYPTO_ALIGNMENT_FACTOR;
+    if(remain) {
+        size_t plain_data_aligned_length = plain_data_length - remain + CRYPTO_ALIGNMENT_FACTOR;
+        uint8_t* plain_data_aligned = malloc(plain_data_aligned_length);
+        furi_check(plain_data_aligned != NULL);
+        memset(plain_data_aligned, 0, plain_data_aligned_length);
+        memcpy(plain_data_aligned, plain_data, plain_data_length);
+
+        encrypted_data = malloc(plain_data_aligned_length);
+        furi_check(encrypted_data != NULL);
+        *encrypted_data_length = plain_data_aligned_length;
+
+        furi_hal_crypto_enclave_load_key(CRYPTO_KEY_SLOT, crypto_settings->iv);
+        furi_hal_crypto_encrypt(plain_data_aligned, encrypted_data, plain_data_aligned_length);
+        furi_hal_crypto_enclave_unload_key(CRYPTO_KEY_SLOT);
+
+        memset_s(plain_data_aligned, plain_data_aligned_length, 0, plain_data_aligned_length);
+        free(plain_data_aligned);
+    } else {
+        encrypted_data = malloc(plain_data_length);
+        furi_check(encrypted_data != NULL);
+        *encrypted_data_length = plain_data_length;
+
+        furi_hal_crypto_enclave_load_key(CRYPTO_KEY_SLOT, crypto_settings->iv);
+        furi_hal_crypto_encrypt(plain_data, encrypted_data, plain_data_length);
+        furi_hal_crypto_enclave_unload_key(CRYPTO_KEY_SLOT);
+    }
+
+    return encrypted_data;
+}
+
+uint8_t* totp_crypto_decrypt_v1(
+    const uint8_t* encrypted_data,
+    const size_t encrypted_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* decrypted_data_length) {
+    *decrypted_data_length = encrypted_data_length;
+    uint8_t* decrypted_data = malloc(*decrypted_data_length);
+    furi_check(decrypted_data != NULL);
+    furi_hal_crypto_enclave_load_key(CRYPTO_KEY_SLOT, crypto_settings->iv);
+    furi_hal_crypto_decrypt(encrypted_data, decrypted_data, encrypted_data_length);
+    furi_hal_crypto_enclave_unload_key(CRYPTO_KEY_SLOT);
+    return decrypted_data;
+}
+
+CryptoSeedIVResult totp_crypto_seed_iv_v1(
+    CryptoSettings* crypto_settings,
+    const uint8_t* pin,
+    uint8_t pin_length) {
+    CryptoSeedIVResult result;
+    if(crypto_settings->crypto_verify_data == NULL) {
+        FURI_LOG_I(LOGGING_TAG, "Generating new IV");
+        furi_hal_random_fill_buf(&crypto_settings->salt[0], CRYPTO_SALT_LENGTH);
+    }
+
+    memcpy(&crypto_settings->iv[0], &crypto_settings->salt[0], TOTP_IV_SIZE);
+    if(pin != NULL && pin_length > 0) {
+        uint8_t max_i;
+        if(pin_length > TOTP_IV_SIZE) {
+            max_i = TOTP_IV_SIZE;
+        } else {
+            max_i = pin_length;
+        }
+
+        for(uint8_t i = 0; i < max_i; i++) {
+            crypto_settings->iv[i] = crypto_settings->iv[i] ^ (uint8_t)(pin[i] * (i + 1));
+        }
+    } else {
+        uint8_t max_i;
+        size_t uid_size = furi_hal_version_uid_size();
+        if(uid_size > TOTP_IV_SIZE) {
+            max_i = TOTP_IV_SIZE;
+        } else {
+            max_i = uid_size;
+        }
+
+        const uint8_t* uid = (const uint8_t*)UID64_BASE; //-V566
+        for(uint8_t i = 0; i < max_i; i++) {
+            crypto_settings->iv[i] = crypto_settings->iv[i] ^ uid[i];
+        }
+    }
+
+    result = CryptoSeedIVResultFlagSuccess;
+    if(crypto_settings->crypto_verify_data == NULL) {
+        FURI_LOG_I(LOGGING_TAG, "Generating crypto verify data");
+        crypto_settings->crypto_verify_data = malloc(CRYPTO_VERIFY_KEY_LENGTH);
+        furi_check(crypto_settings->crypto_verify_data != NULL);
+        crypto_settings->crypto_verify_data_length = CRYPTO_VERIFY_KEY_LENGTH;
+
+        crypto_settings->crypto_verify_data = totp_crypto_encrypt_v1(
+            (const uint8_t*)CRYPTO_VERIFY_KEY,
+            CRYPTO_VERIFY_KEY_LENGTH,
+            crypto_settings,
+            &crypto_settings->crypto_verify_data_length);
+
+        crypto_settings->pin_required = pin != NULL && pin_length > 0;
+
+        result |= CryptoSeedIVResultFlagNewCryptoVerifyData;
+    }
+
+    return result;
+}
+
+bool totp_crypto_verify_key_v1(const CryptoSettings* crypto_settings) {
+    size_t decrypted_key_length;
+    uint8_t* decrypted_key = totp_crypto_decrypt_v1(
+        crypto_settings->crypto_verify_data,
+        crypto_settings->crypto_verify_data_length,
+        crypto_settings,
+        &decrypted_key_length);
+
+    bool key_valid = true;
+    for(uint8_t i = 0; i < CRYPTO_VERIFY_KEY_LENGTH && key_valid; i++) {
+        if(decrypted_key[i] != CRYPTO_VERIFY_KEY[i]) key_valid = false;
+    }
+
+    free(decrypted_key);
+
+    return key_valid;
+}
+#endif

+ 55 - 0
services/crypto/crypto_v1.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include "../../config/app/config.h"
+#ifdef TOTP_OBSOLETE_CRYPTO_V1_COMPATIBILITY_ENABLED
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include "../../types/crypto_settings.h"
+#include "common_types.h"
+
+/**
+ * @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_v1(
+    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_v1(
+    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_v1(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_v1(const CryptoSettings* crypto_settings);
+#endif

+ 190 - 0
services/crypto/crypto_v2.c

@@ -0,0 +1,190 @@
+#include "crypto_v2.h"
+#ifdef TOTP_OBSOLETE_CRYPTO_V2_COMPATIBILITY_ENABLED
+#include <stdlib.h>
+#include <furi.h>
+#include <furi_hal_crypto.h>
+#include <furi_hal_random.h>
+#include <furi_hal_version.h>
+#include "../../types/common.h"
+#include "../../config/wolfssl/config.h"
+#include <wolfssl/wolfcrypt/hmac.h>
+#include "memset_s.h"
+#include "constants.h"
+#include "polyfills.h"
+
+#define CRYPTO_ALIGNMENT_FACTOR (16)
+
+static const uint8_t* get_device_uid() {
+    return (const uint8_t*)UID64_BASE; //-V566
+}
+
+static uint8_t get_device_uid_length() {
+    return furi_hal_version_uid_size();
+}
+
+static const uint8_t* get_crypto_verify_key() {
+    return get_device_uid();
+}
+
+static uint8_t get_crypto_verify_key_length() {
+    return get_device_uid_length();
+}
+
+uint8_t* totp_crypto_encrypt_v2(
+    const uint8_t* plain_data,
+    const size_t plain_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* encrypted_data_length) {
+    uint8_t* encrypted_data;
+    size_t remain = plain_data_length % CRYPTO_ALIGNMENT_FACTOR;
+    if(remain) {
+        size_t plain_data_aligned_length = plain_data_length - remain + CRYPTO_ALIGNMENT_FACTOR;
+        uint8_t* plain_data_aligned = malloc(plain_data_aligned_length);
+        furi_check(plain_data_aligned != NULL);
+        memset(plain_data_aligned, 0, plain_data_aligned_length);
+        memcpy(plain_data_aligned, plain_data, plain_data_length);
+
+        encrypted_data = malloc(plain_data_aligned_length);
+        furi_check(encrypted_data != NULL);
+        *encrypted_data_length = plain_data_aligned_length;
+
+        furi_check(
+            furi_hal_crypto_enclave_load_key(crypto_settings->crypto_key_slot, crypto_settings->iv),
+            "Encryption failed: enclave_load_key");
+        furi_check(
+            furi_hal_crypto_encrypt(plain_data_aligned, encrypted_data, plain_data_aligned_length),
+            "Encryption failed: encrypt");
+        furi_check(
+            furi_hal_crypto_enclave_unload_key(crypto_settings->crypto_key_slot),
+            "Encryption failed: enclave_unload_key");
+
+        memset_s(plain_data_aligned, plain_data_aligned_length, 0, plain_data_aligned_length);
+        free(plain_data_aligned);
+    } else {
+        encrypted_data = malloc(plain_data_length);
+        furi_check(encrypted_data != NULL);
+        *encrypted_data_length = plain_data_length;
+
+        furi_check(
+            furi_hal_crypto_enclave_load_key(crypto_settings->crypto_key_slot, crypto_settings->iv),
+            "Encryption failed: store_load_key");
+        furi_check(
+            furi_hal_crypto_encrypt(plain_data, encrypted_data, plain_data_length),
+            "Encryption failed: encrypt");
+        furi_check(
+            furi_hal_crypto_enclave_unload_key(crypto_settings->crypto_key_slot),
+            "Encryption failed: store_unload_key");
+    }
+
+    return encrypted_data;
+}
+
+uint8_t* totp_crypto_decrypt_v2(
+    const uint8_t* encrypted_data,
+    const size_t encrypted_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* decrypted_data_length) {
+    *decrypted_data_length = encrypted_data_length;
+    uint8_t* decrypted_data = malloc(*decrypted_data_length);
+    furi_check(decrypted_data != NULL);
+    furi_check(
+        furi_hal_crypto_enclave_load_key(crypto_settings->crypto_key_slot, crypto_settings->iv),
+        "Decryption failed: enclave_load_key");
+    furi_check(
+        furi_hal_crypto_decrypt(encrypted_data, decrypted_data, encrypted_data_length),
+        "Decryption failed: decrypt");
+    furi_check(
+        furi_hal_crypto_enclave_unload_key(crypto_settings->crypto_key_slot),
+        "Decryption failed: enclave_unload_key");
+    return decrypted_data;
+}
+
+CryptoSeedIVResult totp_crypto_seed_iv_v2(
+    CryptoSettings* crypto_settings,
+    const uint8_t* pin,
+    uint8_t pin_length) {
+    CryptoSeedIVResult result;
+    if(crypto_settings->crypto_verify_data == NULL) {
+        FURI_LOG_I(LOGGING_TAG, "Generating new salt");
+        furi_hal_random_fill_buf(&crypto_settings->salt[0], CRYPTO_SALT_LENGTH);
+    }
+
+    const uint8_t* device_uid = get_device_uid();
+    uint8_t device_uid_length = get_device_uid_length();
+
+    uint8_t hmac_key_length = device_uid_length;
+    if(pin != NULL && pin_length > 0) {
+        hmac_key_length += pin_length;
+    }
+
+    uint8_t* hmac_key = malloc(hmac_key_length);
+    furi_check(hmac_key != NULL);
+
+    memcpy(hmac_key, device_uid, device_uid_length);
+
+    if(pin != NULL && pin_length > 0) {
+        memcpy(hmac_key + device_uid_length, pin, pin_length);
+    }
+
+    uint8_t hmac[WC_SHA512_DIGEST_SIZE] = {0};
+
+    Hmac hmac_context;
+    wc_HmacSetKey(&hmac_context, WC_SHA512, hmac_key, hmac_key_length);
+    wc_HmacUpdate(&hmac_context, &crypto_settings->salt[0], CRYPTO_SALT_LENGTH);
+    int hmac_result_code = wc_HmacFinal(&hmac_context, &hmac[0]);
+    wc_HmacFree(&hmac_context);
+
+    memset_s(hmac_key, hmac_key_length, 0, hmac_key_length);
+    free(hmac_key);
+
+    if(hmac_result_code == 0) {
+        uint8_t offset =
+            hmac[WC_SHA512_DIGEST_SIZE - 1] % (WC_SHA512_DIGEST_SIZE - CRYPTO_IV_LENGTH - 1);
+        memcpy(&crypto_settings->iv[0], &hmac[offset], CRYPTO_IV_LENGTH);
+
+        result = CryptoSeedIVResultFlagSuccess;
+        if(crypto_settings->crypto_verify_data == NULL) {
+            const uint8_t* crypto_vkey = get_crypto_verify_key();
+            uint8_t crypto_vkey_length = get_crypto_verify_key_length();
+            FURI_LOG_I(LOGGING_TAG, "Generating crypto verify data");
+            crypto_settings->crypto_verify_data = malloc(crypto_vkey_length);
+            furi_check(crypto_settings->crypto_verify_data != NULL);
+            crypto_settings->crypto_verify_data_length = crypto_vkey_length;
+
+            crypto_settings->crypto_verify_data = totp_crypto_encrypt_v2(
+                crypto_vkey,
+                crypto_vkey_length,
+                crypto_settings,
+                &crypto_settings->crypto_verify_data_length);
+
+            crypto_settings->pin_required = pin != NULL && pin_length > 0;
+
+            result |= CryptoSeedIVResultFlagNewCryptoVerifyData;
+        }
+    } else {
+        result = CryptoSeedIVResultFailed;
+    }
+
+    return result;
+}
+
+bool totp_crypto_verify_key_v2(const CryptoSettings* crypto_settings) {
+    size_t decrypted_key_length;
+    uint8_t* decrypted_key = totp_crypto_decrypt_v2(
+        crypto_settings->crypto_verify_data,
+        crypto_settings->crypto_verify_data_length,
+        crypto_settings,
+        &decrypted_key_length);
+
+    const uint8_t* crypto_vkey = get_crypto_verify_key();
+    uint8_t crypto_vkey_length = get_crypto_verify_key_length();
+    bool key_valid = true;
+    for(uint8_t i = 0; i < crypto_vkey_length && key_valid; i++) {
+        if(decrypted_key[i] != crypto_vkey[i]) key_valid = false;
+    }
+
+    free(decrypted_key);
+
+    return key_valid;
+}
+#endif

+ 55 - 0
services/crypto/crypto_v2.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include "../../config/app/config.h"
+#ifdef TOTP_OBSOLETE_CRYPTO_V2_COMPATIBILITY_ENABLED
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include "../../types/crypto_settings.h"
+#include "common_types.h"
+
+/**
+ * @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_v2(
+    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_v2(
+    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_v2(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_v2(const CryptoSettings* crypto_settings);
+#endif

+ 195 - 0
services/crypto/crypto_v3.c

@@ -0,0 +1,195 @@
+#include "crypto_v3.h"
+#include <stdlib.h>
+#include <furi.h>
+#include <furi_hal_crypto.h>
+#include <furi_hal_random.h>
+#include <furi_hal_version.h>
+#include "../../types/common.h"
+#include "../../config/wolfssl/config.h"
+#include <wolfssl/wolfcrypt/hmac.h>
+#include <wolfssl/wolfcrypt/pwdbased.h>
+#include "memset_s.h"
+#include "constants.h"
+#include "polyfills.h"
+
+#define CRYPTO_ALIGNMENT_FACTOR (16)
+#define PBKDF2_ITERATIONS_COUNT (200)
+
+static const uint8_t* get_device_uid() {
+    return (const uint8_t*)UID64_BASE; //-V566
+}
+
+static uint8_t get_device_uid_length() {
+    return furi_hal_version_uid_size();
+}
+
+static const uint8_t* get_crypto_verify_key() {
+    return get_device_uid();
+}
+
+static uint8_t get_crypto_verify_key_length() {
+    return get_device_uid_length();
+}
+
+uint8_t* totp_crypto_encrypt_v3(
+    const uint8_t* plain_data,
+    const size_t plain_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* encrypted_data_length) {
+    uint8_t* encrypted_data;
+    size_t remain = plain_data_length % CRYPTO_ALIGNMENT_FACTOR;
+    if(remain) {
+        size_t plain_data_aligned_length = plain_data_length - remain + CRYPTO_ALIGNMENT_FACTOR;
+        uint8_t* plain_data_aligned = malloc(plain_data_aligned_length);
+        furi_check(plain_data_aligned != NULL);
+        memset(plain_data_aligned, 0, plain_data_aligned_length);
+        memcpy(plain_data_aligned, plain_data, plain_data_length);
+
+        encrypted_data = malloc(plain_data_aligned_length);
+        furi_check(encrypted_data != NULL);
+        *encrypted_data_length = plain_data_aligned_length;
+
+        furi_check(
+            furi_hal_crypto_enclave_load_key(crypto_settings->crypto_key_slot, crypto_settings->iv),
+            "Encryption failed: enclave_load_key");
+        furi_check(
+            furi_hal_crypto_encrypt(plain_data_aligned, encrypted_data, plain_data_aligned_length),
+            "Encryption failed: encrypt");
+        furi_check(
+            furi_hal_crypto_enclave_unload_key(crypto_settings->crypto_key_slot),
+            "Encryption failed: enclave_unload_key");
+
+        memset_s(plain_data_aligned, plain_data_aligned_length, 0, plain_data_aligned_length);
+        free(plain_data_aligned);
+    } else {
+        encrypted_data = malloc(plain_data_length);
+        furi_check(encrypted_data != NULL);
+        *encrypted_data_length = plain_data_length;
+
+        furi_check(
+            furi_hal_crypto_enclave_load_key(crypto_settings->crypto_key_slot, crypto_settings->iv),
+            "Encryption failed: enclave_load_key");
+        furi_check(
+            furi_hal_crypto_encrypt(plain_data, encrypted_data, plain_data_length),
+            "Encryption failed: encrypt");
+        furi_check(
+            furi_hal_crypto_enclave_unload_key(crypto_settings->crypto_key_slot),
+            "Encryption failed: enclave_unload_key");
+    }
+
+    return encrypted_data;
+}
+
+uint8_t* totp_crypto_decrypt_v3(
+    const uint8_t* encrypted_data,
+    const size_t encrypted_data_length,
+    const CryptoSettings* crypto_settings,
+    size_t* decrypted_data_length) {
+    *decrypted_data_length = encrypted_data_length;
+    uint8_t* decrypted_data = malloc(*decrypted_data_length);
+    furi_check(decrypted_data != NULL);
+    furi_check(
+        furi_hal_crypto_enclave_load_key(crypto_settings->crypto_key_slot, crypto_settings->iv),
+        "Decryption failed: enclave_load_key");
+    furi_check(
+        furi_hal_crypto_decrypt(encrypted_data, decrypted_data, encrypted_data_length),
+        "Decryption failed: decrypt");
+    furi_check(
+        furi_hal_crypto_enclave_unload_key(crypto_settings->crypto_key_slot),
+        "Decryption failed: enclave_unload_key");
+    return decrypted_data;
+}
+
+CryptoSeedIVResult totp_crypto_seed_iv_v3(
+    CryptoSettings* crypto_settings,
+    const uint8_t* pin,
+    uint8_t pin_length) {
+    CryptoSeedIVResult result;
+    if(crypto_settings->crypto_verify_data == NULL) {
+        FURI_LOG_I(LOGGING_TAG, "Generating new salt");
+        furi_hal_random_fill_buf(&crypto_settings->salt[0], CRYPTO_SALT_LENGTH);
+    }
+
+    const uint8_t* device_uid = get_device_uid();
+    uint8_t device_uid_length = get_device_uid_length();
+
+    uint8_t pbkdf_key_length = device_uid_length;
+    if(pin != NULL && pin_length > 0) {
+        pbkdf_key_length += pin_length;
+    }
+
+    uint8_t* pbkdf_key = malloc(pbkdf_key_length);
+    furi_check(pbkdf_key != NULL);
+
+    memcpy(pbkdf_key, device_uid, device_uid_length);
+
+    if(pin != NULL && pin_length > 0) {
+        memcpy(pbkdf_key + device_uid_length, pin, pin_length);
+    }
+
+    uint8_t pbkdf_output[WC_SHA512_DIGEST_SIZE] = {0};
+
+    int pbkdf_result_code = wc_PBKDF2(
+        &pbkdf_output[0],
+        pbkdf_key,
+        pbkdf_key_length,
+        &crypto_settings->salt[0],
+        CRYPTO_SALT_LENGTH,
+        PBKDF2_ITERATIONS_COUNT,
+        WC_SHA512_DIGEST_SIZE,
+        WC_SHA512);
+
+    memset_s(pbkdf_key, pbkdf_key_length, 0, pbkdf_key_length);
+    free(pbkdf_key);
+
+    if(pbkdf_result_code == 0) {
+        uint8_t offset = pbkdf_output[WC_SHA512_DIGEST_SIZE - 1] %
+                         (WC_SHA512_DIGEST_SIZE - CRYPTO_IV_LENGTH - 1);
+        memcpy(&crypto_settings->iv[0], &pbkdf_output[offset], CRYPTO_IV_LENGTH);
+        result = CryptoSeedIVResultFlagSuccess;
+        if(crypto_settings->crypto_verify_data == NULL) {
+            const uint8_t* crypto_vkey = get_crypto_verify_key();
+            uint8_t crypto_vkey_length = get_crypto_verify_key_length();
+            FURI_LOG_I(LOGGING_TAG, "Generating crypto verify data");
+            crypto_settings->crypto_verify_data = malloc(crypto_vkey_length);
+            furi_check(crypto_settings->crypto_verify_data != NULL);
+            crypto_settings->crypto_verify_data_length = crypto_vkey_length;
+
+            crypto_settings->crypto_verify_data = totp_crypto_encrypt_v3(
+                crypto_vkey,
+                crypto_vkey_length,
+                crypto_settings,
+                &crypto_settings->crypto_verify_data_length);
+
+            crypto_settings->pin_required = pin != NULL && pin_length > 0;
+
+            result |= CryptoSeedIVResultFlagNewCryptoVerifyData;
+        }
+    } else {
+        result = CryptoSeedIVResultFailed;
+    }
+
+    memset_s(&pbkdf_output[0], WC_SHA512_DIGEST_SIZE, 0, WC_SHA512_DIGEST_SIZE);
+
+    return result;
+}
+
+bool totp_crypto_verify_key_v3(const CryptoSettings* crypto_settings) {
+    size_t decrypted_key_length;
+    uint8_t* decrypted_key = totp_crypto_decrypt_v3(
+        crypto_settings->crypto_verify_data,
+        crypto_settings->crypto_verify_data_length,
+        crypto_settings,
+        &decrypted_key_length);
+
+    const uint8_t* crypto_vkey = get_crypto_verify_key();
+    uint8_t crypto_vkey_length = get_crypto_verify_key_length();
+    bool key_valid = true;
+    for(uint8_t i = 0; i < crypto_vkey_length && key_valid; i++) {
+        if(decrypted_key[i] != crypto_vkey[i]) key_valid = false;
+    }
+
+    free(decrypted_key);
+
+    return key_valid;
+}

+ 52 - 0
services/crypto/crypto_v3.h

@@ -0,0 +1,52 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include "../../types/crypto_settings.h"
+#include "common_types.h"
+
+/**
+ * @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_v3(
+    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_v3(
+    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_v3(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_v3(const CryptoSettings* crypto_settings);

+ 14 - 0
services/crypto/polyfills.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <furi_hal_crypto.h>
+
+#ifndef FURI_HAL_CRYPTO_ENCLAVE_USER_KEY_SLOT_START
+
+// FW Crypto API is outdated, let's polyfill it
+#define FURI_HAL_CRYPTO_ENCLAVE_USER_KEY_SLOT_START (12u)
+#define FURI_HAL_CRYPTO_ENCLAVE_USER_KEY_SLOT_END (100u)
+#define furi_hal_crypto_enclave_ensure_key furi_hal_crypto_verify_key
+#define furi_hal_crypto_enclave_load_key furi_hal_crypto_store_load_key
+#define furi_hal_crypto_enclave_unload_key furi_hal_crypto_store_unload_key
+
+#endif

+ 66 - 0
services/idle_timeout/idle_timeout.c

@@ -0,0 +1,66 @@
+#include "idle_timeout.h"
+#include <stdlib.h>
+#include <furi/core/timer.h>
+
+#define IDLE_TIMER_CHECK_PERIODICITY_SEC (1)
+#define SEC_TO_TICKS(sec) ((sec)*1000)
+
+struct IdleTimeoutContext {
+    FuriTimer* timer;
+    bool activity_reported;
+    void* on_idle_callback_context;
+    IDLE_TIMEOUT_CALLBACK on_idle_callback;
+    uint16_t timeout_sec;
+    uint16_t idle_period_sec;
+    bool idle_handled;
+};
+
+static void idle_timer_callback(void* context) {
+    IdleTimeoutContext* instance = context;
+    if(instance->activity_reported) {
+        instance->idle_period_sec = 0;
+        instance->idle_handled = false;
+        instance->activity_reported = false;
+    } else if(!instance->idle_handled) {
+        if(instance->idle_period_sec >= instance->timeout_sec) {
+            instance->idle_handled =
+                instance->on_idle_callback(instance->on_idle_callback_context);
+        } else {
+            instance->idle_period_sec += IDLE_TIMER_CHECK_PERIODICITY_SEC;
+        }
+    }
+}
+
+IdleTimeoutContext* idle_timeout_alloc(
+    uint16_t timeout_sec,
+    IDLE_TIMEOUT_CALLBACK on_idle_callback,
+    void* on_idle_callback_context) {
+    IdleTimeoutContext* instance = malloc(sizeof(IdleTimeoutContext));
+    if(instance == NULL) return NULL;
+
+    instance->timer = furi_timer_alloc(&idle_timer_callback, FuriTimerTypePeriodic, instance);
+    if(instance->timer == NULL) return NULL;
+
+    instance->timeout_sec = timeout_sec;
+    instance->on_idle_callback = on_idle_callback;
+    instance->on_idle_callback_context = on_idle_callback_context;
+    return instance;
+}
+
+void idle_timeout_start(IdleTimeoutContext* context) {
+    furi_timer_start(context->timer, SEC_TO_TICKS(IDLE_TIMER_CHECK_PERIODICITY_SEC));
+}
+
+void idle_timeout_stop(IdleTimeoutContext* context) {
+    furi_timer_stop(context->timer);
+}
+
+void idle_timeout_report_activity(IdleTimeoutContext* context) {
+    context->activity_reported = true;
+}
+
+void idle_timeout_free(IdleTimeoutContext* context) {
+    furi_timer_stop(context->timer);
+    furi_timer_free(context->timer);
+    free(context);
+}

+ 44 - 0
services/idle_timeout/idle_timeout.h

@@ -0,0 +1,44 @@
+#pragma once
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+typedef struct IdleTimeoutContext IdleTimeoutContext;
+
+typedef bool (*IDLE_TIMEOUT_CALLBACK)(void* context);
+
+/**
+ * @brief Initializes a new instance of IDLE timeout
+ * @param timeout_sec IDLE timeout in seconds
+ * @param on_idle_callback callback function to trigger when IDLE timeout happened
+ * @param on_idle_callback_context callback function context
+ * @return IDLE timeout context
+ */
+IdleTimeoutContext* idle_timeout_alloc(
+    uint16_t timeout_sec,
+    IDLE_TIMEOUT_CALLBACK on_idle_callback,
+    void* on_idle_callback_context);
+
+/**
+ * @brief Starts IDLE timeout
+ * @param context IDLE timeout context
+ */
+void idle_timeout_start(IdleTimeoutContext* context);
+
+/**
+ * @brief Stops IDLE timeout
+ * @param context IDLE timeout context
+ */
+void idle_timeout_stop(IdleTimeoutContext* context);
+
+/**
+ * @brief Reports activity to IDLE timeout
+ * @param context IDLE timeout context
+ */
+void idle_timeout_report_activity(IdleTimeoutContext* context);
+
+/**
+ * @brief Disposes IDLE timeout and releases all the resources
+ * @param context IDLE timeout context
+ */
+void idle_timeout_free(IdleTimeoutContext* context);

+ 125 - 0
services/totp/totp.c

@@ -0,0 +1,125 @@
+#include "totp.h"
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <math.h>
+#include <timezone_utils.h>
+#include "../../config/wolfssl/config.h"
+#include <wolfssl/wolfcrypt/hmac.h>
+
+#define HMAC_MAX_RESULT_SIZE WC_SHA512_DIGEST_SIZE
+
+static uint64_t swap_uint64(uint64_t val) {
+    val = ((val << 8) & 0xFF00FF00FF00FF00ULL) | ((val >> 8) & 0x00FF00FF00FF00FFULL);
+    val = ((val << 16) & 0xFFFF0000FFFF0000ULL) | ((val >> 16) & 0x0000FFFF0000FFFFULL);
+    return (val << 32) | (val >> 32);
+}
+
+/**
+ * @brief Generates the timeblock for a time in seconds.
+ *        Timeblocks are the amount of intervals in a given time. For example,
+ *        if 1,000,000 seconds has passed for 30 second intervals, you would get
+ *        33,333 timeblocks (intervals), where timeblock++ is effectively +30 seconds.
+ * @param interval in seconds
+ * @param for_time a time in seconds to get the current timeblocks
+ * @return Timeblock given \p for_time using \p interval
+ */
+uint64_t totp_timecode(uint8_t interval, uint64_t for_time) {
+    return for_time / interval;
+}
+
+/**
+ * @brief Generates an OTP (One Time Password)
+ * @param algo hashing algorithm to be used
+ * @param plain_secret plain token secret
+ * @param plain_secret_length plain token secret length
+ * @param input input data for OTP code generation
+ * @return OTP code if code was successfully generated; 0 otherwise
+ */
+uint64_t otp_generate(
+    TOTP_ALGO algo,
+    const uint8_t* plain_secret,
+    size_t plain_secret_length,
+    uint64_t input) {
+    uint8_t hmac[HMAC_MAX_RESULT_SIZE] = {0};
+
+    uint64_t input_swapped = swap_uint64(input);
+
+    int hmac_len =
+        (*algo)(plain_secret, plain_secret_length, (uint8_t*)&input_swapped, 8, &hmac[0]);
+    if(hmac_len == 0) {
+        return OTP_ERROR;
+    }
+
+    uint64_t offset = (hmac[hmac_len - 1] & 0xF);
+    uint64_t i_code =
+        ((hmac[offset] & 0x7F) << 24 | (hmac[offset + 1] & 0xFF) << 16 |
+         (hmac[offset + 2] & 0xFF) << 8 | (hmac[offset + 3] & 0xFF));
+
+    return i_code;
+}
+
+uint64_t totp_at(
+    TOTP_ALGO algo,
+    const uint8_t* plain_secret,
+    size_t plain_secret_length,
+    uint64_t for_time,
+    float timezone,
+    uint8_t interval) {
+    uint64_t for_time_adjusted =
+        timezone_offset_apply(for_time, timezone_offset_from_hours(timezone));
+    return otp_generate(
+        algo, plain_secret, plain_secret_length, totp_timecode(interval, for_time_adjusted));
+}
+
+static int totp_algo_common(
+    int type,
+    const uint8_t* key,
+    size_t key_length,
+    const uint8_t* input,
+    size_t input_length,
+    uint8_t* output) {
+    Hmac hmac;
+    int ret = wc_HmacSetKey(&hmac, type, key, key_length);
+    if(ret == 0) {
+        ret = wc_HmacUpdate(&hmac, input, input_length);
+    }
+
+    if(ret == 0) {
+        ret = wc_HmacFinal(&hmac, output);
+    }
+
+    wc_HmacFree(&hmac);
+    return ret == 0 ? wc_HmacSizeByType(type) : 0;
+}
+
+static int totp_algo_sha1(
+    const uint8_t* key,
+    size_t key_length,
+    const uint8_t* input,
+    size_t input_length,
+    uint8_t* output) {
+    return totp_algo_common(WC_SHA, key, key_length, input, input_length, output);
+}
+
+static int totp_algo_sha256(
+    const uint8_t* key,
+    size_t key_length,
+    const uint8_t* input,
+    size_t input_length,
+    uint8_t* output) {
+    return totp_algo_common(WC_SHA256, key, key_length, input, input_length, output);
+}
+
+static int totp_algo_sha512(
+    const uint8_t* key,
+    size_t key_length,
+    const uint8_t* input,
+    size_t input_length,
+    uint8_t* output) {
+    return totp_algo_common(WC_SHA512, key, key_length, input, input_length, output);
+}
+
+const TOTP_ALGO TOTP_ALGO_SHA1 = (TOTP_ALGO)(&totp_algo_sha1);
+const TOTP_ALGO TOTP_ALGO_SHA256 = (TOTP_ALGO)(&totp_algo_sha256);
+const TOTP_ALGO TOTP_ALGO_SHA512 = (TOTP_ALGO)(&totp_algo_sha512);

+ 55 - 0
services/totp/totp.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <stdlib.h>
+#include <stdint.h>
+
+#define OTP_ERROR (0)
+
+/**
+ * @brief Must compute HMAC using passed arguments, output as char array through output.
+ *        \p key is secret key buffer.
+ *        \p key_length is secret key buffer length.
+ *        \p input is input buffer.
+ *        \p input_length is input buffer length.
+ *	      \p output is an output buffer of the resulting HMAC operation.
+ *        Must return 0 if error, or the length in bytes of the HMAC operation.
+ */
+typedef int (*TOTP_ALGO)(
+    const uint8_t* key,
+    size_t key_length,
+    const uint8_t* input,
+    size_t input_length,
+    uint8_t* output);
+
+/**
+ * @brief Computes HMAC using SHA1
+ */
+extern const TOTP_ALGO TOTP_ALGO_SHA1;
+
+/**
+ * @brief Computes HMAC using SHA256
+ */
+extern const TOTP_ALGO TOTP_ALGO_SHA256;
+
+/**
+ * @brief Computes HMAC using SHA512
+ */
+extern const TOTP_ALGO TOTP_ALGO_SHA512;
+
+/**
+ * @brief Generates a OTP key using the totp algorithm.
+ * @param algo hashing algorithm to be used
+ * @param plain_secret plain token secret
+ * @param plain_secret_length plain token secret length
+ * @param for_time the time the generated key will be created for
+ * @param timezone UTC timezone adjustment for the generated key
+ * @param interval token lifetime in seconds
+ * @return TOTP code if code was successfully generated; 0 otherwise
+ */
+uint64_t totp_at(
+    TOTP_ALGO algo,
+    const uint8_t* plain_secret,
+    size_t plain_secret_length,
+    uint64_t for_time,
+    float timezone,
+    uint8_t interval);

BIN
totp_10px.png


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików