Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for more TOTP credentials and enable cycling backwards through codes #356

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 65 additions & 27 deletions movement/watch_faces/complication/totp_face.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down
36 changes: 30 additions & 6 deletions movement/watch_faces/complication/totp_face.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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_
15 changes: 14 additions & 1 deletion movement/watch_faces/complication/totp_face_lfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions movement/watch_faces/complication/totp_face_lfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
74 changes: 74 additions & 0 deletions utils/totp_face_helper.py
Original file line number Diff line number Diff line change
@@ -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)
Loading