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

Make the TOTP face use the filesystem for secret storage #95

Merged
merged 15 commits into from
Nov 1, 2022

Conversation

wryun
Copy link
Contributor

@wryun wryun commented Oct 13, 2022

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:

  • don't have to bake TOTP keys into code
  • understands normal totp format
  • understands files exported from Aegis

Why this is bad:

  • should we be encrypting? Probably no point (unless one can't easily extract the firmware? i.e. that it's a lot easier to pull this file that the code?)
  • 'parsing' code is a bit sketchy
  • not clear whether we want the watch to be doing so much work (could have a script to 'upload' the file into a form that's closer or identical to the in-memory representation)
  • with base32.c, there's a bunch more code (could easily cut the encode function from the base32 library, I guess)

}
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);
Copy link
Contributor Author

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.

rules.mk Outdated Show resolved Hide resolved
@joeycastillo
Copy link
Owner

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).

@wryun
Copy link
Contributor Author

wryun commented Oct 16, 2022

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).

@wryun
Copy link
Contributor Author

wryun commented Oct 19, 2022

When I finally tried to flash this to my watch, there were a couple of issues:

  • the serial connection was a bit problematic because of the way 'read' is working. Using picocom gave me character at a time into 'read' (i.e. couldn't execute any commands, since there's essentially no line buffering); the Arduino IDE's serial monitor allowed me to send almost a line in a quick chunk, but over 50 bytes got chopped off in the middle. i.e. it's pretty hard to get the secrets across.
  • when you get the secrets across, you get a different sequence to the emulator. I've tried to be pretty careful about making sure the time was set properly, yet I persistently get the wrong numbers. I was getting the correct numbers before this PR though when I hardcoded my secrets. Unfortunately, this is hard to debug, because 'cat' doesn't work either across the serial connection for larger files (you just get the first 50 or so bytes). EDIT: fixed. This was due to the base32 code I found on the internet assuming chars were signed (i.e. worked on x86 in the simulator/emulator, failed on the watch).

@wryun wryun marked this pull request as draft October 19, 2022 03:50
@joeycastillo
Copy link
Owner

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 totp_face_lfs; it occurs to me that some folks will still want to do it the old way, compiling their secrets into the firmware, so we should keep both watch faces available.

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 :)

Comment on lines 177 to 184
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;
}
}
Copy link
Collaborator

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

Copy link
Owner

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.

Copy link
Contributor Author

@wryun wryun left a 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 Show resolved Hide resolved
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));
Copy link
Contributor Author

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...

Copy link
Owner

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 :)

Copy link
Owner

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++) {
Copy link
Contributor Author

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);
Copy link
Contributor Author

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)

Copy link
Owner

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.

movement/watch_faces/complication/totp_face_lfs.c Outdated Show resolved Hide resolved
@wryun
Copy link
Contributor Author

wryun commented Oct 26, 2022

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?

@joeycastillo
Copy link
Owner

joeycastillo commented Oct 26, 2022

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.

@wryun
Copy link
Contributor Author

wryun commented Oct 27, 2022

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.

@wryun wryun marked this pull request as ready for review October 27, 2022 01:57
@wryun
Copy link
Contributor Author

wryun commented Oct 27, 2022

Also, totp_face_lfs doesn't follow the normal naming convention (totp_lfs_face?). Not sure if that's intentional or not.

@joeycastillo
Copy link
Owner

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 activate instead of setup, but since it checks against the static number of codes, it is effectively only called once. Just to be sure, I used a pair of #ifdefs so that the file is normally read in setup, and only read in activate on the simulator; this ensures that the mallocs in that function will only get called once at boot.

Also, totp_face_lfs doesn't follow the normal naming convention (totp_lfs_face?). Not sure if that's intentional or not.

This doesn't bother me; it's a variant of a face, I could see e.g. temperature_face_thermistor and temperature_face_i2c; as long as the meaning is clear and nothing in the header steps on another watch face's namespace, it should be fine.

Copy link
Contributor Author

@wryun wryun left a 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)));
Copy link
Contributor Author

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).

@bademux
Copy link

bademux commented Oct 31, 2022

Hello, is it ready for testing?
Will get grateful for quick manual how to configure it with FS

@wryun
Copy link
Contributor Author

wryun commented Nov 1, 2022

@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:

* At the moment, to get the records onto the filesystem, start a serial connection and do:

@wryun
Copy link
Contributor Author

wryun commented Nov 1, 2022

(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)

Copy link
Owner

@joeycastillo joeycastillo left a 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 :)

@joeycastillo joeycastillo merged commit b7a461d into joeycastillo:main Nov 1, 2022
@bademux
Copy link

bademux commented Nov 1, 2022

@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),
or it have to be fixed in the firmware @joeycastillo.
Thanks for the great software and hardware!

@joeycastillo
Copy link
Owner

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)

@wryun
Copy link
Contributor Author

wryun commented Nov 1, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants