-
Notifications
You must be signed in to change notification settings - Fork 250
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
Make the TOTP face use the filesystem for secret storage #95
Conversation
No encryption. Unclear if this is useful.
} | ||
TOTP(keys + totp_state->current_key_offset, key_sizes[totp_state->current_index], timesteps[totp_state->current_index]); | ||
totp_state->current_index = (index + 1) % num_totp_records; | ||
TOTP(totp_records[totp_state->current_index].secret, totp_records[totp_state->current_index].secret_size, totp_records[totp_state->current_index].period); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: line looks a bit long. Probably should just set 'index' as above. Hmm, and feels like I should reset steps to force getCodeFromTimestamp to happen again.
Thanks for this! It's a good start. I'm going to get in there and make some changes, though. My main issues center around the extra malloc's and free's that are happening here. My vision for Movement is that watch faces generally shouldn't call malloc except the once, in setup, to claim space for their watch face state. At this time all watch faces have one call to malloc, and no call to free; this effectively means all memory is statically allocated at boot and sticks around in exactly the same place until the watch battery dies. I think there is a refactor that will allow us to do that here in the TOTP face, but I'd prefer to hack on it myself (which will also give me a chance to review the PR in depth). |
Thanks! I'll be interested to see how you get around not using malloc. The only thing I can really think of is to read the file into a static buffer and then just point to the secrets in that buffer, but that'll end up wasting quite a bit of space after the init (vs malloc/freeing just in the init)... EDIT: or I guess reading line at a time into a static buffer, then over-catering the secret inside the struct (usually secrets are 10 bytes, but I don't believe there's an upper limit; I think the longest I have is 42 bytes). |
When I finally tried to flash this to my watch, there were a couple of issues:
|
Random base32 code copied off internet assumed chars were signed. Not on ARM, folks. Also improved error output (\n).
OKAY! I've made the changes we discussed: static allocation of all the secrets (it uses up a little over a kilobyte of RAM, which could worry me but not right now) and it reads the file line by line into a fixed-length buffer (max line length is 255). I also renamed the watch face to Feel free to play around with this and let me know if you see any issues; when you're ready, mark as ready for review and I can merge it in :) |
movement/filesystem.c
Outdated
err = lfs_file_read(&lfs, &file, buf, min(length, file_size - offset)); | ||
if (err < 0) return false; | ||
for(int i = 0; i < length; i++) { | ||
if (buf[i] == '\n') { | ||
buf[i] = 0; | ||
break; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like there's a subtle bug here where you don't get null terminated string if you read length and don't hit a new line or if your file is less than length and it doesn't end in a newline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call; it works as long as you have a buffer one byte longer than the maximum length you're requesting, but it's probably safer to just max out at length - 1
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good.
Some thoughts; haven't run the code yet. Happy to make suggested changes if they seem ok.
movement/filesystem.c
Outdated
if (err < 0) return false; | ||
err = lfs_file_seek(&lfs, &file, offset, LFS_SEEK_SET); | ||
if (err < 0) return false; | ||
err = lfs_file_read(&lfs, &file, buf, min(length, file_size - offset)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To fix the issue tahnok mentioned, one approach would be to read length - 1 here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this seems like the move :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of reading to length - 1, I clarified the documentation to state that you should provide a buffer of length + 1, and that the last character will be a null terminator; this is how we were currently using the function anyway, and it feels more obvious than reading one character less than we were asked for.
if (err < 0) return false; | ||
err = lfs_file_read(&lfs, &file, buf, min(length, file_size - offset)); | ||
if (err < 0) return false; | ||
for(int i = 0; i < length; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or:
char *end_of_line = strchr(buf, '\n');
if (end_of_line)
*end_of_line = '\0';
I think. But if the function signature is changed to return the offset, then I guess there's more pay-off in doing our own loop.
totp_record->label[0] = value[0]; | ||
totp_record->label[1] = value[1]; | ||
} else if (!strcmp(param, "secret")) { | ||
totp_record->secret_size = base32_decode((unsigned char *)value, totp_record->secret); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like checking BASE32_LEN is the responsible thing to do here... (i.e. make sure it's <= 48, now that the max secret size is fixed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also a good call, I can make this change.
Re keeping the old TOTP face, is there a reason to prefer that approach aside from a bit of 'security through obscurity' for the secrets? |
The main reason is that there are already blog posts out there documenting the current workflow, and I am hesitant to break things for folks who are already using the existing functionality as provided at launch. I think both faces present valid options for folks who want to use this function, and there's no harm in including both; watch faces don't take up any flash or RAM unless included in the list. |
I've made most of the changes discussed except the filesystem API one. Haven't resolved in case you want to go through the comments. |
Also, |
Okay! I made the changes we discussed; take a look and if you think I haven't mangled it up too badly, I can merge it in :) Upon further reflection, I think you were correct to use malloc instead of a fixed maximum secret size, so I restored that logic. I had gotten spooked because the call was happening in
This doesn't bother me; it's a variant of a face, I could see e.g. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks - LGTM aside from my mistake with BASE32_LEN vs UNBASE32_LEN.
printf("TOTP secret too long: %s\n", value); | ||
return false; | ||
} | ||
totp_record->secret = malloc(BASE32_LEN(strlen(value))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, my mistake earlier - this should be UNBASE32_LEN (as above).
Hello, is it ready for testing? |
@bademux unfortunately, due to issues with the serial interface to the Sensor Watch, your best bet is to test it in the simulator (I do have it on my watch, but it wasn't fun getting to that point; the easiest thing to do is probably hack a watch face into the firmware to write the necessary files, then remove that face!). There are instructions at the top of the the face for how to use it:
|
(I believe it's basically ready to merge, aside from the issue with BASE32_LEN - which just causes it to use a bit more space than it should) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right on, I think we're there! Thanks for all your hard work on this one @wryun :)
@wryun Is the serial console something that can be temporary hackfixed from client side (some app that will send totp uri in a special way), |
It’s something that will need a firmware fix, @wryun if you can characterize the issue with some specifics, can you open an issue? I know it has to do with my USB code, and might require someone with a better knowledge of tinyUSB to dig into it (or I’ll take a look eventually) |
Thanks @joeycastillo ! I've raised #117 As mentioned there, there's a relatively straightforward issue in movement.c, but I suspect there are also deeper problems with how TinyUSB works. But I may be misremembering; I think I got dissuaded from doing the simple fix after hitting other issues. |
Messed around with using LFS more nicely, then decided to stop getting distracted since the memory usage from reading the entire file in was acceptable on startup.
Only limited testing on simulator so far, pending feedback on approach. Don't merge as is.
Why this is good:
Why this is bad: