diff --git a/movement/watch_faces/complication/totp_face.c b/movement/watch_faces/complication/totp_face.c index 242820d79..54196f2be 100644 --- a/movement/watch_faces/complication/totp_face.c +++ b/movement/watch_faces/complication/totp_face.c @@ -31,42 +31,64 @@ //////////////////////////////////////////////////////////////////////////////// // Enter your TOTP key data below -static const uint8_t num_keys = 2; -static uint8_t keys[] = { - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0xde, 0xad, 0xbe, 0xef, // 1 - JBSWY3DPEHPK3PXP - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0xde, 0xad, 0xbe, 0xef, // 2 - JBSWY3DPEHPK3PXP -}; -static const uint8_t key_sizes[] = { - 10, - 10, -}; -static const uint32_t timesteps[] = { - 30, - 30, -}; -static const char labels[][2] = { - { '2', 'F' }, - { 'A', 'C' }, + +/* You can optionally edit and execute `utils/totp_face_helper.py` to + * properly transform all your credentials to the expected format. + * + * The default key size is 20, the default algorithm is SHA1 and + * the default time-step is 30 seconds. + * + * A label is made up of two characters (Which can be entered as a string.) + * Due to the structure of the display, the first character can be + * displayed as anything but an uppercase R. + * The second character can be displayed as the letters A, B, C, D, E, + * F, H, I, J, L, N, O, R, T, U and X, and the numbers 0, 1, 3, 7 and 8. + * (See: https://www.sensorwatch.net/docs/wig/display/) + * + * Ignore the initializer-overrides warning for the credentials array. + * It is wanted behaviour in this instance. + */ +#pragma GCC diagnostic ignored "-Winitializer-overrides" +const static totp_parameters_t credentials[] = { + CREDENTIAL(.label = "2F", .key_size = 10), + CREDENTIAL(.label = "AC", .key_size = 10), + CREDENTIAL(.label = "GL"), // Using the default key size (20). + CREDENTIAL(.label = "TF", .key_size = 35, .algorithm = SHA512), + CREDENTIAL(.label = "EB", .time_step = 40), }; -static const hmac_alg algorithms[] = { - SHA1, - SHA1, + +static uint8_t keys[] = { + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0xde, 0xad, 0xbe, 0xef, // 2F - JBSWY3DPEHPK3PXP + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0xde, 0xad, 0xbe, 0xef, // AC - JBSWY3DPEHPK3PXP + 0x75, 0x1f, 0xf2, 0xbb, 0xd5, 0x72, 0xd1, 0xef, 0xa2, 0x1d, 0x93, 0x95, 0x8d, 0xe2, 0x3c, 0x0c, 0x8d, 0x87, 0xd1, 0x7e, // GL - OUP7FO6VOLI67IQ5SOKY3YR4BSGYPUL6 + 0xf9, 0x86, 0x3a, 0xdd, 0xd7, 0xc6, 0xb2, 0x79, 0x9b, 0x5d, 0xdc, 0xea, 0xc3, 0xbd, 0xc4, 0xef, 0x15, 0x0a, 0xeb, 0xa3, 0x6d, 0x79, 0x00, 0x48, 0xa0, 0x15, 0xd8, 0xf1, 0xaa, 0xd1, 0x2b, 0x97, 0x57, 0x4f, 0xa4, // TF - 7GDDVXOXY2ZHTG253TVMHPOE54KQV25DNV4QASFACXMPDKWRFOLVOT5E + 0xd4, 0xcf, 0xd8, 0x5c, 0xca, 0xc7, 0x8c, 0x29, 0x75, 0xd5, 0x8b, 0xf6, 0xa3, 0xdb, 0xad, 0x6b, 0x27, 0x58, 0x1b, 0xbf, // EB - 2TH5QXGKY6GCS5OVRP3KHW5NNMTVQG57 }; // END OF KEY DATA. //////////////////////////////////////////////////////////////////////////////// +#define NUMBER_OF_CREDENTIALS (sizeof(credentials) / sizeof(totp_parameters_t)) + +static uint16_t key_offset(uint8_t credential_index) { + uint16_t offset = 0; + for (uint8_t i = 0; i < credential_index; ++i) { + offset += credentials[i].key_size; + } + return offset; +} + static void _update_display(totp_state_t *totp_state) { char buf[14]; div_t result; uint8_t valid_for; - result = div(totp_state->timestamp, timesteps[totp_state->current_index]); + result = div(totp_state->timestamp, credentials[totp_state->current_index].time_step); if (result.quot != totp_state->steps) { totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp); totp_state->steps = result.quot; } - valid_for = timesteps[totp_state->current_index] - result.rem; - sprintf(buf, "%c%c%2d%06lu", labels[totp_state->current_index][0], labels[totp_state->current_index][1], valid_for, totp_state->current_code); + valid_for = credentials[totp_state->current_index].time_step - result.rem; + sprintf(buf, "%c%c%2d%06lu", credentials[totp_state->current_index].label[0], credentials[totp_state->current_index].label[1], valid_for, totp_state->current_code); watch_display_string(buf, 0); } @@ -81,7 +103,7 @@ void totp_face_activate(movement_settings_t *settings, void *context) { (void) settings; memset(context, 0, sizeof(totp_state_t)); totp_state_t *totp_state = (totp_state_t *)context; - TOTP(keys, key_sizes[0], timesteps[0], algorithms[0]); + TOTP(keys, credentials[0].key_size, credentials[0].time_step, credentials[0].algorithm); totp_state->timestamp = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), movement_timezone_offsets[settings->bit.time_zone] * 60); totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp); } @@ -101,19 +123,35 @@ bool totp_face_loop(movement_event_t event, movement_settings_t *settings, void movement_move_to_face(0); break; case EVENT_ALARM_BUTTON_UP: - if (totp_state->current_index + 1 < num_keys) { - totp_state->current_key_offset += key_sizes[totp_state->current_index]; + if (totp_state->current_index + 1 < NUMBER_OF_CREDENTIALS) { + totp_state->current_key_offset += credentials[totp_state->current_index].key_size; totp_state->current_index++; } else { - // wrap around to first key + // Wrap around to the first credential. totp_state->current_key_offset = 0; totp_state->current_index = 0; } - TOTP(keys + totp_state->current_key_offset, key_sizes[totp_state->current_index], timesteps[totp_state->current_index], algorithms[totp_state->current_index]); + TOTP(keys + totp_state->current_key_offset, credentials[totp_state->current_index].key_size, credentials[totp_state->current_index].time_step, credentials[totp_state->current_index].algorithm); + _update_display(totp_state); + break; + case EVENT_LIGHT_BUTTON_UP: + if (totp_state->current_index - 1 >= 0) { + totp_state->current_key_offset -= credentials[totp_state->current_index].key_size; + totp_state->current_index--; + } else { + // Wrap around to the last credential. + totp_state->current_index = NUMBER_OF_CREDENTIALS - 1; + totp_state->current_key_offset = key_offset(totp_state->current_index); + } + TOTP(keys + totp_state->current_key_offset, credentials[totp_state->current_index].key_size, credentials[totp_state->current_index].time_step, credentials[totp_state->current_index].algorithm); _update_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, settings); diff --git a/movement/watch_faces/complication/totp_face.h b/movement/watch_faces/complication/totp_face.h index 1248f716e..69d3a709f 100644 --- a/movement/watch_faces/complication/totp_face.h +++ b/movement/watch_faces/complication/totp_face.h @@ -40,29 +40,43 @@ * o SHA512 * * Instructions: + * o Optionally edit and execute `utils/totp_face_helper.py` to + * properly transform all your credentials to the expected format. + * OR * o Find your secret key(s) and convert them to the required format. * o Use https://cryptii.com/pipes/base32-to-hex to convert base32 to hex * o Use https://github.com/susam/mintotp to generate test codes for verification * o Edit global variables in "totp_face.c" to configure your stored keys: - * o "keys", "key_sizes", "timesteps", and "algorithms" set the - * cryptographic parameters for each secret key. - * o "labels" sets the two-letter label for each key - * (This replaces the day-of-week indicator) - * o Once finished, remove the two provided examples. + * o "keys", and the members of "totp_parameters_t": "key_size", + * "time_step", and "algorithm" set the cryptographic parameters + * for each secret key. + * o The member "label" of "totp_parameters_t" sets the two-letter label + * for each key (This replaces the day-of-week indicator) + * o Once finished, remove the five provided examples. * * 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" +#include "TOTP.h" typedef struct { uint32_t timestamp; uint8_t steps; uint32_t current_code; uint8_t current_index; - uint8_t current_key_offset; + uint16_t current_key_offset; } totp_state_t; +typedef struct { + char label[2]; + uint8_t key_size; + uint8_t time_step; + hmac_alg algorithm; +} totp_parameters_t; + void totp_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); void totp_face_activate(movement_settings_t *settings, void *context); bool totp_face_loop(movement_event_t event, movement_settings_t *settings, void *context); @@ -76,4 +90,14 @@ void totp_face_resign(movement_settings_t *settings, void *context); NULL, \ }) +/* A key size of 20 bytes, a time-step of 30 seconds and the algorithm + * SHA1 seem to be the most common parameters in the wild. + */ +#define CREDENTIAL(...) ((const totp_parameters_t) { \ + .key_size = 20, \ + .time_step = 30, \ + .algorithm = SHA1, \ + __VA_ARGS__ \ +}) + #endif // TOTP_FACE_H_ diff --git a/movement/watch_faces/complication/totp_face_lfs.c b/movement/watch_faces/complication/totp_face_lfs.c index 4066ac48f..820ad52ef 100644 --- a/movement/watch_faces/complication/totp_face_lfs.c +++ b/movement/watch_faces/complication/totp_face_lfs.c @@ -163,7 +163,7 @@ static void totp_face_lfs_read_file(char *filename) { continue; } - // If we found a probably valid TOTP record, keep it. + // If we found a probably valid TOTP record, keep it. if (totp_records[num_totp_records].secret_size) { num_totp_records += 1; } else { @@ -255,8 +255,21 @@ bool totp_face_lfs_loop(movement_event_t event, movement_settings_t *settings, v 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: + if (totp_state->current_index - 1 >= 0) { + totp_face_set_record(totp_state, totp_state->current_index - 1); + } else { + // Wrap around to the last record. + totp_face_set_record(totp_state, num_totp_records - 1); + } + 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, settings); diff --git a/movement/watch_faces/complication/totp_face_lfs.h b/movement/watch_faces/complication/totp_face_lfs.h index 64c6ce154..72ae24607 100644 --- a/movement/watch_faces/complication/totp_face_lfs.h +++ b/movement/watch_faces/complication/totp_face_lfs.h @@ -47,6 +47,8 @@ * 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" diff --git a/utils/totp_face_helper.py b/utils/totp_face_helper.py new file mode 100755 index 000000000..9fc365aff --- /dev/null +++ b/utils/totp_face_helper.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +# Transform TOTP credentials into the format expected by +# movement/watch/faces/complication/totp_face.c +# +# Edit the credentials list below if __name__ == '__main__': + +from base64 import b32decode +from collections import namedtuple + +(SHA1, SHA224, SHA256, SHA384, SHA512) = ('SHA1', 'SHA224', 'SHA256', + 'SHA384', 'SHA512') + +Credential = namedtuple('Credential', + ('label', 'key', 'algorithm', 'time_step'), + defaults=(SHA1, 30)) + +def key_to_octet_array_line(key, keys): + key_bytes = [f'0x{byte:02x},' for byte in b32decode(key)] + keys.append(' '.join(key_bytes)) + return len(key_bytes) + +def to_c_array(keys, credentials): + print('static uint8_t keys[] = {') + for (key, credential) in zip(keys, credentials): + print(f' {key} // {credential.label}') + print('};') + +def add_field(name, value): + print(f', .{name} = {value}', end='') + +def to_totpc_credentials(credentials): + keys = [] + print(('Replace everything between `Enter your TOTP key data below`' + ' and `END OF KEY DATA` in `movement/watch/faces/complication/totp_face.c`' + ' by the following:\n')) + print('#pragma GCC diagnostic ignored "-Winitializer-overrides"') + print('const static totp_parameters_t credentials[] = {') + for credential in credentials: + print(f' CREDENTIAL(.label = "{credential.label}"', end='') + key_size = key_to_octet_array_line(credential.key, keys) + if key_size != 20: + add_field('key_size', key_size) + if credential.time_step != 30: + add_field('time_step', credential.time_step) + if credential.algorithm != SHA1: + add_field('algorithm', credential.algorithm) + print('),') + print('};\n') + to_c_array(keys, credentials) + +if __name__ == '__main__': + # Replace these credentials by your credentials, + # either by using their positions Credential(LABEL, KEY[, ALGORITHM][, TIME_STEP]) + # or by using the keyword arguments label, key, algorithm and time_step. + # + # The default key size is 20, the default algorithm is SHA1 and + # the default time-step is 30 seconds. + # + # A label is made up of two characters (Which can be entered as a string.) + # Due to the structure of the display, the first character can be + # displayed as anything but an uppercase R. + # The second character can be displayed as the letters A, B, C, D, E, + # F, H, I, J, L, N, O, R, T, U and X, and the numbers 0, 1, 3, 7 and 8. + # (See: https://www.sensorwatch.net/docs/wig/display/) + credentials = (Credential('2F', 'JBSWY3DPEHPK3PXP'), + Credential('AC', 'JBSWY3DPEHPK3PXP'), + Credential('GL', 'OUP7FO6VOLI67IQ5SOKY3YR4BSGYPUL6'), + Credential('TF', + '7GDDVXOXY2ZHTG253TVMHPOE54KQV25DNV4QASFACXMPDKWRFOLVOT5E', + algorithm=SHA512), + Credential('EB', '2TH5QXGKY6GCS5OVRP3KHW5NNMTVQG57', time_step=40),) + + to_totpc_credentials(credentials)