Skip to content

Commit

Permalink
Improve the credential management and allow to cycle backwards.
Browse files Browse the repository at this point in the history
When storing many TOTP credentials, it could get bothersome to keep all
the arrays synchronized. Now there are the `credentials` array which
holds most of the parameters and the `keys` array which is a concession
to not have the storage waste or the malloc that would be required
when storing the key in the `totp_parameters_t` struct along with
everything else.
Additionally there is a helper program found at `utils/totp_face_helper.py`
which properly transforms the key from its base32 representation into
the form expected by the TOTP watch face.

It is also helpful to be able to go back to the previous credential by
pressing the light button.
Pressing the light button longer (for more than half a second)
activates the LED.
  • Loading branch information
maxz committed Jan 20, 2024
1 parent 7342fe6 commit d365df7
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 33 deletions.
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
34 changes: 29 additions & 5 deletions movement/watch_faces/complication/totp_face.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,27 @@
* 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;
Expand All @@ -63,6 +70,13 @@ typedef struct {
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
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)

0 comments on commit d365df7

Please sign in to comment.