From: James Haggerty Date: Tue, 22 Jul 2025 10:32:38 +0000 (+1000) Subject: Move totp_lfs face from legacy X-Git-Url: https://git.earman.xyz/?a=commitdiff_plain;h=0eb96a637c95e12f32587c4ecba0e6e94e85d8aa;p=sensor-watch.git Move totp_lfs face from legacy Changes: - use 3 characters if possible for identifier - rename from totp_face_lfs to totp_lfs_face for consistency --- diff --git a/Makefile b/Makefile index 5d1fbd2..cae2b94 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,8 @@ TINYUSB_CDC=1 # Now we're all set to include gossamer's make rules. include $(GOSSAMER_PATH)/make.mk +CFLAGS+=-D_POSIX_C_SOURCE=200112L + define n diff --git a/legacy/watch_faces/complication/totp_face_lfs.c b/legacy/watch_faces/complication/totp_face_lfs.c deleted file mode 100644 index 439aca3..0000000 --- a/legacy/watch_faces/complication/totp_face_lfs.c +++ /dev/null @@ -1,320 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2022 Wesley Ellis (https://github.com/tahnok) - * - * 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. - */ - -#include -#include -#include - -#include "TOTP.h" -#include "base32.h" - -#include "watch.h" -#include "watch_utility.h" -#include "filesystem.h" - -#include "totp_face_lfs.h" - -#define MAX_TOTP_RECORDS 30 -#define MAX_TOTP_SECRET_SIZE 128 -#define TOTP_FILE "totp_uris.txt" - -const char* TOTP_URI_START = "otpauth://totp/"; - -struct totp_record { - char label[2]; - hmac_alg algorithm; - uint8_t period; - uint8_t secret_size; - - union { - uint8_t *secret; - struct { - uint16_t file_secret_offset; - uint16_t file_secret_length; - }; - }; -}; - -/* This is used if we're not storing all the secrets but instead - * calculating them on demand. Avoids malloc in normal operation. - */ -static uint8_t current_secret[MAX_TOTP_SECRET_SIZE]; - -static struct totp_record totp_records[MAX_TOTP_RECORDS]; -static uint8_t num_totp_records = 0; - -static void init_totp_record(struct totp_record *totp_record) { - totp_record->label[0] = 'A'; - totp_record->label[1] = 'A'; - totp_record->algorithm = SHA1; - totp_record->period = 30; - totp_record->secret_size = 0; -} - -static bool totp_face_lfs_read_param(struct totp_record *totp_record, char *param, char *value) { - if (!strcmp(param, "issuer")) { - if (value[0] == '\0' || value[1] == '\0') { - printf("TOTP issuer must be >= 2 chars, got '%s'\n", value); - return false; - } - totp_record->label[0] = value[0]; - totp_record->label[1] = value[1]; - } else if (!strcmp(param, "secret")) { - totp_record->file_secret_length = strlen(value); - if (UNBASE32_LEN(totp_record->file_secret_length) > MAX_TOTP_SECRET_SIZE) { - printf("TOTP secret too long: %s\n", value); - return false; - } - totp_record->secret_size = base32_decode((unsigned char *)value, current_secret); - if (totp_record->secret_size == 0) { - printf("TOTP can't decode secret: %s\n", value); - return false; - } - } else if (!strcmp(param, "digits")) { - if (!strcmp(param, "6")) { - printf("TOTP got %s, not 6 digits\n", value); - return false; - } - } else if (!strcmp(param, "period")) { - totp_record->period = atoi(value); - if (totp_record->period == 0) { - printf("TOTP invalid period %s\n", value); - return false; - } - } else if (!strcmp(param, "algorithm")) { - if (!strcmp(value, "SHA1")) { - totp_record->algorithm = SHA1; - } - else if (!strcmp(value, "SHA224")) { - totp_record->algorithm = SHA224; - } - else if (!strcmp(value, "SHA256")) { - totp_record->algorithm = SHA256; - } - else if (!strcmp(value, "SHA384")) { - totp_record->algorithm = SHA384; - } - else if (!strcmp(value, "SHA512")) { - totp_record->algorithm = SHA512; - } - else { - printf("TOTP ignored due to algorithm %s\n", value); - return false; - } - } - - return true; -} - -static void totp_face_lfs_read_file(char *filename) { - // For 'format' of file, see comment at top. - const size_t uri_start_len = strlen(TOTP_URI_START); - - if (!filesystem_file_exists(filename)) { - printf("TOTP file error: %s\n", filename); - return; - } - - char line[256]; - int32_t offset = 0, old_offset = 0; - while (old_offset = offset, filesystem_read_line(filename, line, &offset, 255) && strlen(line)) { - if (num_totp_records == MAX_TOTP_RECORDS) { - printf("TOTP max records: %d\n", MAX_TOTP_RECORDS); - break; - } - - // Check that it looks like a URI - if (strncmp(TOTP_URI_START, line, uri_start_len)) { - printf("TOTP invalid uri start: %s\n", line); - continue; - } - - // Check that we can find a '?' (to start our parameters) - char *param; - char *param_saveptr = NULL; - char *params = strchr(line + uri_start_len, '?'); - if (params == NULL) { - printf("TOTP no params: %s\n", line); - continue; - } - - // Process the parameters and put them in the record - init_totp_record(&totp_records[num_totp_records]); - bool error = false; - param = strtok_r(params + 1, "&", ¶m_saveptr); - do { - char *param_middle = strchr(param, '='); - *param_middle = '\0'; - if (totp_face_lfs_read_param(&totp_records[num_totp_records], param, param_middle + 1)) { - if (!strcmp(param, "secret")) { - totp_records[num_totp_records].file_secret_offset = old_offset + (param_middle + 1 - line); - } - } else { - error = true; - } - } while ((param = strtok_r(NULL, "&", ¶m_saveptr))); - - if (error) { - totp_records[num_totp_records].secret_size = 0; - continue; - } - - // If we found a probably valid TOTP record, keep it. - if (totp_records[num_totp_records].secret_size) { - num_totp_records += 1; - } else { - printf("TOTP missing secret: %s\n", line); - } - } -} - -void totp_face_lfs_setup(uint8_t watch_face_index, void ** context_ptr) { - (void) watch_face_index; - if (*context_ptr == NULL) { - *context_ptr = malloc(sizeof(totp_lfs_state_t)); - } - -#if !(__EMSCRIPTEN__) - if (num_totp_records == 0) { - totp_face_lfs_read_file(TOTP_FILE); - } -#endif -} - -static uint8_t *totp_face_lfs_get_file_secret(struct totp_record *record) { - char buffer[BASE32_LEN(MAX_TOTP_SECRET_SIZE) + 1]; - int32_t file_secret_offset = record->file_secret_offset; - - /* TODO filesystem_read_line is quite inefficient. Consider writing a new function, - * and keeping the file open? - */ - if (!filesystem_read_line(TOTP_FILE, buffer, &file_secret_offset, record->file_secret_length + 1)) { - /* Shouldn't happen at this point. Return current_secret, which is misleading but will not cause a crash. */ - printf("TOTP can't read expected secret from totp_uris.txt (failed readline)\n"); - return current_secret; - } - if (base32_decode((unsigned char *)buffer, current_secret) != record->secret_size) { - printf("TOTP can't properly decode secret '%s' from totp_uris.txt; failed at offset %d; read to %ld\n", buffer, record->file_secret_offset, file_secret_offset); - } - return current_secret; -} - -static void totp_face_set_record(totp_lfs_state_t *totp_state, int i) { - struct totp_record *record; - - if (num_totp_records == 0 && i >= num_totp_records) { - return; - } - - totp_state->current_index = i; - record = &totp_records[i]; - - TOTP( - totp_face_lfs_get_file_secret(record), - record->secret_size, - record->period, - record->algorithm - ); - totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp); - totp_state->steps = totp_state->timestamp / record->period; -} - -void totp_face_lfs_activate(void *context) { - memset(context, 0, sizeof(totp_lfs_state_t)); - totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context; - -#if __EMSCRIPTEN__ - if (num_totp_records == 0) { - // Doing this here rather than in setup makes things a bit more pleasant in the simulator, since there's no easy way to trigger - // setup again after uploading the data. - totp_face_lfs_read_file(TOTP_FILE); - } -#endif - - totp_state->timestamp = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), movement_get_current_timezone_offset()); - totp_face_set_record(totp_state, 0); -} - -static void totp_face_display(totp_lfs_state_t *totp_state) { - uint8_t index = totp_state->current_index; - char buf[14]; - - if (num_totp_records == 0) { - watch_display_string("No2F Codes", 0); - return; - } - - div_t result = div(totp_state->timestamp, totp_records[index].period); - if (result.quot != totp_state->steps) { - totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp); - totp_state->steps = result.quot; - } - uint8_t valid_for = totp_records[index].period - result.rem; - - sprintf(buf, "%c%c%2d%06lu", totp_records[index].label[0], totp_records[index].label[1], valid_for, totp_state->current_code); - - watch_display_string(buf, 0); -} - -bool totp_face_lfs_loop(movement_event_t event, void *context) { - - totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context; - - switch (event.event_type) { - case EVENT_TICK: - totp_state->timestamp++; - totp_face_display(totp_state); - break; - case EVENT_ACTIVATE: - totp_face_display(totp_state); - break; - case EVENT_TIMEOUT: - movement_move_to_face(0); - break; - case EVENT_ALARM_BUTTON_UP: - totp_face_set_record(totp_state, (totp_state->current_index + 1) % num_totp_records); - totp_face_display(totp_state); - break; - case EVENT_LIGHT_BUTTON_UP: - totp_face_set_record(totp_state, (totp_state->current_index + num_totp_records - 1) % num_totp_records); - totp_face_display(totp_state); - break; - case EVENT_ALARM_BUTTON_DOWN: - case EVENT_ALARM_LONG_PRESS: - case EVENT_LIGHT_BUTTON_DOWN: - break; - case EVENT_LIGHT_LONG_PRESS: - movement_illuminate_led(); - break; - default: - movement_default_loop_handler(event); - break; - } - - return true; -} - -void totp_face_lfs_resign(void *context) { - (void) context; -} diff --git a/legacy/watch_faces/complication/totp_face_lfs.h b/legacy/watch_faces/complication/totp_face_lfs.h deleted file mode 100644 index 16276a2..0000000 --- a/legacy/watch_faces/complication/totp_face_lfs.h +++ /dev/null @@ -1,77 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2022 Wesley Ellis (https://github.com/tahnok) - * - * 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. - */ - -#ifndef TOTP_FACE_LFS_H_ -#define TOTP_FACE_LFS_H_ - -/* - * TOTP-LFS face - * Time-based one-time password (TOTP) generator using LFS - * - * Reads from a file "totp_uris.txt", containing a single secret key in a - * series of URLs. Each line is what's in a QR code, e.g.: - * otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example - * otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 - * - * This is also the same as what Aegis exports in plain-text format. - * This face performs minimal sanitisation of input, however, and you - * will only see errors if you use the simulator or view the serial console. - * - * At the moment, to get the records onto the filesystem, start a serial connection and do: - * echo otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example > totp_uris.txt - * echo otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 >> totp_uris.txt - * (note the double >> in the second one) - * - * You may want to customise the characters that appear to identify the 2FA - * code. These are just the first two characters of the issuer, and it's fine - * to modify the URI. - * - * If you have more than one secret key, press ALARM to cycle through them. - * Press LIGHT to cycle in the other direction or keep it pressed longer to - * activate the light. - */ - -#include "movement.h" - -typedef struct { - uint32_t timestamp; - uint8_t steps; - uint32_t current_code; - uint8_t current_index; -} totp_lfs_state_t; - -void totp_face_lfs_setup(uint8_t watch_face_index, void ** context_ptr); -void totp_face_lfs_activate(void *context); -bool totp_face_lfs_loop(movement_event_t event, void *context); -void totp_face_lfs_resign(void *context); - -#define totp_face_lfs ((const watch_face_t){ \ - totp_face_lfs_setup, \ - totp_face_lfs_activate, \ - totp_face_lfs_loop, \ - totp_face_lfs_resign, \ - NULL, \ -}) - -#endif // TOTP_FACE_LFS_H_ diff --git a/movement_faces.h b/movement_faces.h index e8fb0fe..d0580fe 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -59,6 +59,7 @@ #include "periodic_table_face.h" #include "squash_face.h" #include "totp_face.h" +#include "totp_lfs_face.h" #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index 0c78fb4..dbd4ebd 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -14,7 +14,7 @@ SRCS += \ ./watch-faces/complication/days_since_face.c \ ./watch-faces/complication/breathing_face.c \ ./watch-faces/complication/squash_face.c \ - ./watch-faces/complication/totp_face.c \ + ./watch-faces/complication/totp_lfs_face.c \ ./watch-faces/complication/tally_face.c \ ./watch-faces/complication/wordle_face.c \ ./watch-faces/demo/all_segments_face.c \ diff --git a/watch-faces/complication/totp_lfs_face.c b/watch-faces/complication/totp_lfs_face.c new file mode 100644 index 0000000..8be3e28 --- /dev/null +++ b/watch-faces/complication/totp_lfs_face.c @@ -0,0 +1,323 @@ +/* + * MIT License + * + * Copyright (c) 2022 Wesley Ellis (https://github.com/tahnok) + * + * 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. + */ + +#include +#include +#include + +#include "TOTP.h" +#include "base32.h" + +#include "watch.h" +#include "watch_utility.h" +#include "filesystem.h" + +#include "totp_lfs_face.h" + +#define MAX_TOTP_RECORDS 30 +#define MAX_TOTP_SECRET_SIZE 128 +#define TOTP_FILE "totp_uris.txt" + +const char* TOTP_URI_START = "otpauth://totp/"; + +struct totp_record { + char label[4]; + hmac_alg algorithm; + uint8_t period; + uint8_t secret_size; + + union { + uint8_t *secret; + struct { + uint16_t file_secret_offset; + uint16_t file_secret_length; + }; + }; +}; + +/* This is used if we're not storing all the secrets but instead + * calculating them on demand. Avoids malloc in normal operation. + */ +static uint8_t current_secret[MAX_TOTP_SECRET_SIZE]; + +static struct totp_record totp_records[MAX_TOTP_RECORDS]; +static uint8_t num_totp_records = 0; + +static void init_totp_record(struct totp_record *totp_record) { + totp_record->label[0] = 'A'; + totp_record->label[1] = 'A'; + totp_record->label[2] = 'A'; + totp_record->label[3] = 0; + totp_record->algorithm = SHA1; + totp_record->period = 30; + totp_record->secret_size = 0; +} + +static bool totp_lfs_face_read_param(struct totp_record *totp_record, char *param, char *value) { + if (!strcmp(param, "issuer")) { + if (value[0] == '\0') { + printf("TOTP issuer must be a non-empty string\n"); + return false; + } + snprintf(totp_record->label, sizeof(totp_record->label), "%-3s", value); + } else if (!strcmp(param, "secret")) { + totp_record->file_secret_length = strlen(value); + if (UNBASE32_LEN(totp_record->file_secret_length) > MAX_TOTP_SECRET_SIZE) { + printf("TOTP secret too long: %s\n", value); + return false; + } + totp_record->secret_size = base32_decode((unsigned char *)value, current_secret); + if (totp_record->secret_size == 0) { + printf("TOTP can't decode secret: %s\n", value); + return false; + } + } else if (!strcmp(param, "digits")) { + if (!strcmp(param, "6")) { + printf("TOTP got %s, not 6 digits\n", value); + return false; + } + } else if (!strcmp(param, "period")) { + totp_record->period = atoi(value); + if (totp_record->period == 0) { + printf("TOTP invalid period %s\n", value); + return false; + } + } else if (!strcmp(param, "algorithm")) { + if (!strcmp(value, "SHA1")) { + totp_record->algorithm = SHA1; + } + else if (!strcmp(value, "SHA224")) { + totp_record->algorithm = SHA224; + } + else if (!strcmp(value, "SHA256")) { + totp_record->algorithm = SHA256; + } + else if (!strcmp(value, "SHA384")) { + totp_record->algorithm = SHA384; + } + else if (!strcmp(value, "SHA512")) { + totp_record->algorithm = SHA512; + } + else { + printf("TOTP ignored due to algorithm %s\n", value); + return false; + } + } + + return true; +} + +static void totp_lfs_face_read_file(char *filename) { + // For 'format' of file, see comment at top. + const size_t uri_start_len = strlen(TOTP_URI_START); + + if (!filesystem_file_exists(filename)) { + printf("TOTP file error: %s\n", filename); + return; + } + + char line[256]; + int32_t offset = 0, old_offset = 0; + while (old_offset = offset, filesystem_read_line(filename, line, &offset, 255) && strlen(line)) { + if (num_totp_records == MAX_TOTP_RECORDS) { + printf("TOTP max records: %d\n", MAX_TOTP_RECORDS); + break; + } + + // Check that it looks like a URI + if (strncmp(TOTP_URI_START, line, uri_start_len)) { + printf("TOTP invalid uri start: %s\n", line); + continue; + } + + // Check that we can find a '?' (to start our parameters) + char *param; + char *param_saveptr = NULL; + char *params = strchr(line + uri_start_len, '?'); + if (params == NULL) { + printf("TOTP no params: %s\n", line); + continue; + } + + // Process the parameters and put them in the record + init_totp_record(&totp_records[num_totp_records]); + bool error = false; + param = strtok_r(params + 1, "&", ¶m_saveptr); + do { + char *param_middle = strchr(param, '='); + *param_middle = '\0'; + if (totp_lfs_face_read_param(&totp_records[num_totp_records], param, param_middle + 1)) { + if (!strcmp(param, "secret")) { + totp_records[num_totp_records].file_secret_offset = old_offset + (param_middle + 1 - line); + } + } else { + error = true; + } + } while ((param = strtok_r(NULL, "&", ¶m_saveptr))); + + if (error) { + totp_records[num_totp_records].secret_size = 0; + continue; + } + + // If we found a probably valid TOTP record, keep it. + if (totp_records[num_totp_records].secret_size) { + num_totp_records += 1; + } else { + printf("TOTP missing secret: %s\n", line); + } + } +} + +void totp_lfs_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(totp_lfs_state_t)); + } + +#if !(__EMSCRIPTEN__) + if (num_totp_records == 0) { + totp_lfs_face_read_file(TOTP_FILE); + } +#endif +} + +static uint8_t *totp_lfs_face_get_file_secret(struct totp_record *record) { + char buffer[BASE32_LEN(MAX_TOTP_SECRET_SIZE) + 1]; + int32_t file_secret_offset = record->file_secret_offset; + + /* TODO filesystem_read_line is quite inefficient. Consider writing a new function, + * and keeping the file open? + */ + if (!filesystem_read_line(TOTP_FILE, buffer, &file_secret_offset, record->file_secret_length + 1)) { + /* Shouldn't happen at this point. Return current_secret, which is misleading but will not cause a crash. */ + printf("TOTP can't read expected secret from totp_uris.txt (failed readline)\n"); + return current_secret; + } + if (base32_decode((unsigned char *)buffer, current_secret) != record->secret_size) { + printf("TOTP can't properly decode secret '%s' from totp_uris.txt; failed at offset %d; read to %ld\n", buffer, record->file_secret_offset, file_secret_offset); + } + return current_secret; +} + +static void totp_face_set_record(totp_lfs_state_t *totp_state, int i) { + struct totp_record *record; + + if (num_totp_records == 0 && i >= num_totp_records) { + return; + } + + totp_state->current_index = i; + record = &totp_records[i]; + + TOTP( + totp_lfs_face_get_file_secret(record), + record->secret_size, + record->period, + record->algorithm + ); + totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp); + totp_state->steps = totp_state->timestamp / record->period; +} + +void totp_lfs_face_activate(void *context) { + memset(context, 0, sizeof(totp_lfs_state_t)); + totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context; + +#if __EMSCRIPTEN__ + if (num_totp_records == 0) { + // Doing this here rather than in setup makes things a bit more pleasant in the simulator, since there's no easy way to trigger + // setup again after uploading the data. + totp_lfs_face_read_file(TOTP_FILE); + } +#endif + + totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); + totp_face_set_record(totp_state, 0); +} + +static void totp_face_display(totp_lfs_state_t *totp_state) { + uint8_t index = totp_state->current_index; + char buf[7]; + + if (num_totp_records == 0) { + watch_display_text(WATCH_POSITION_FULL, "No2F Codes"); + return; + } + + div_t result = div(totp_state->timestamp, totp_records[index].period); + if (result.quot != totp_state->steps) { + totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp); + totp_state->steps = result.quot; + } + uint8_t valid_for = totp_records[index].period - result.rem; + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, totp_records[index].label, totp_records[index].label); + sprintf(buf, "%2d", valid_for); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + sprintf(buf, "%06lu", totp_state->current_code); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +bool totp_lfs_face_loop(movement_event_t event, void *context) { + + totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context; + + switch (event.event_type) { + case EVENT_TICK: + totp_state->timestamp++; + totp_face_display(totp_state); + break; + case EVENT_ACTIVATE: + totp_face_display(totp_state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + case EVENT_ALARM_BUTTON_UP: + totp_face_set_record(totp_state, (totp_state->current_index + 1) % num_totp_records); + totp_face_display(totp_state); + break; + case EVENT_LIGHT_BUTTON_UP: + totp_face_set_record(totp_state, (totp_state->current_index + num_totp_records - 1) % num_totp_records); + totp_face_display(totp_state); + break; + case EVENT_ALARM_BUTTON_DOWN: + case EVENT_ALARM_LONG_PRESS: + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +void totp_lfs_face_resign(void *context) { + (void) context; +} diff --git a/watch-faces/complication/totp_lfs_face.h b/watch-faces/complication/totp_lfs_face.h new file mode 100644 index 0000000..3e18a6a --- /dev/null +++ b/watch-faces/complication/totp_lfs_face.h @@ -0,0 +1,77 @@ +/* + * MIT License + * + * Copyright (c) 2022 Wesley Ellis (https://github.com/tahnok) + * + * 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. + */ + +#ifndef TOTP_FACE_LFS_H_ +#define TOTP_FACE_LFS_H_ + +/* + * TOTP-LFS face + * Time-based one-time password (TOTP) generator using LFS + * + * Reads from a file "totp_uris.txt", containing a single secret key in a + * series of URLs. Each line is what's in a QR code, e.g.: + * otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example + * otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 + * + * This is also the same as what Aegis exports in plain-text format. + * This face performs minimal sanitisation of input, however, and you + * will only see errors if you use the simulator or view the serial console. + * + * At the moment, to get the records onto the filesystem, start a serial connection and do: + * echo otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example > totp_uris.txt + * echo otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 >> totp_uris.txt + * (note the double >> in the second one) + * + * You may want to customise the characters that appear to identify the 2FA + * code. These are just the first two characters of the issuer, and it's fine + * to modify the URI. + * + * If you have more than one secret key, press ALARM to cycle through them. + * Press LIGHT to cycle in the other direction or keep it pressed longer to + * activate the light. + */ + +#include "movement.h" + +typedef struct { + uint32_t timestamp; + uint8_t steps; + uint32_t current_code; + uint8_t current_index; +} totp_lfs_state_t; + +void totp_lfs_face_setup(uint8_t watch_face_index, void ** context_ptr); +void totp_lfs_face_activate(void *context); +bool totp_lfs_face_loop(movement_event_t event, void *context); +void totp_lfs_face_resign(void *context); + +#define totp_lfs_face ((const watch_face_t){ \ + totp_lfs_face_setup, \ + totp_lfs_face_activate, \ + totp_lfs_face_loop, \ + totp_lfs_face_resign, \ + NULL, \ +}) + +#endif // TOTP_FACE_LFS_H_