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

Faster hmac sha1 token generation #13

Merged
merged 3 commits into from
Aug 8, 2019
Merged

Conversation

nhinds
Copy link
Contributor

@nhinds nhinds commented May 19, 2019

Switch from the jsSHA library to js-sha1, which is roughly twice as fast on my watch. js-sha1 doesn't have HMAC support built in, so that's now a custom implementation in common/hmac-sha1.js. Using Uint8Array everywhere instead of encoding and decoding hex/base32 strings gives a significant performance boost while computing tokens.

To avoid decoding the tokens from base32 each time the app launches, this PR changes the stored CBOR file from storing tokens as base32-encoded strings to storing the decoded byte arrays. Existing stored tokens will be converted when the app is first launched, and new tokens will be converted when they are added in settings. It would be possible to re-encode the byte arrays back to base32 if you want to switch out the libraries for OTP/HMAC/SHA1/etc. in the future.

For me, when I have 3 codes the current app takes around 4s to initially launch, and 2.5s to regenerate codes (tested by turning the screen off and on). This branch brings that to around 2.5s for the initial launch and less than 1s to regenerate codes.

@nhinds
Copy link
Contributor Author

nhinds commented Jun 8, 2019

Hi @Lixxia, is this something you would be interested in merging? Is there anything else you would like me to change / explain as part of this pull request?

@Lixxia
Copy link
Owner

Lixxia commented Jun 8, 2019

Hiya, sorry been on vacation past few weeks. This looks great! I'll take a look this weekend and run some tests. Maybe you could give me a quick overview of which situations you tested?

@nhinds
Copy link
Contributor Author

nhinds commented Jun 8, 2019

I tested starting with a couple of existing codes from AWS (which seems to have longer than usual secrets) and one shorter code, and those got picked up and turned into the array buffer format. I also removed them and added the same codes back from the settings page to test that saving new codes is okay.

I've been using this version of the app myself for a couple of weeks and the codes have been accepted, so at least for AWS the codes are still being generated correctly.


let hex = parsehex(bits);

if (hex.length % 2 !== 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to add support for the hexpad function/check or an alternative in your new base32ToUint8Array function. With your current proposed changes 26 or other odd length tokens no longer work. This issue (#3) was previously solved with the addition of this function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, sorry. I thought I had covered that, since I had been testing with Amazon tokens, but tokens with 26 characters are definitely broken with my changes. I'll get that sorted and update the branch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out just removing the check for leftover bits allows base32 strings with weird lengths to work. Tested with the 26 char test string from app/index.js and some other random-length base32 strings and it seems to decode properly now.

app/tokens.js Outdated
}

// Convert any existing string/base32-encoded tokens to a Uint8Array, and wrap raw ArrayBuffer tokens
function convertTokensToUint8Array(data) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this isn't a utility function and you're only operating on the this.file.data object I'd prefer that this function be integrated into the AuthToken class.

@Lixxia
Copy link
Owner

Lixxia commented Jun 8, 2019

Overall looks good, very solid approach and definitely reduces speeds on load/token regeneration. I've added a few comments in-line regarding a reintroduced bug and some code style preferences. Let me know if you have any questions!

nhinds added 2 commits June 14, 2019 19:35
… boost

Roughly doubles the performance of the HMAC-SHA1 part of TOTP generation.
This avoids spending ~50-100ms converting from base32 to the
bytes used for the TOTP HMAC-SHA1 every time a code is generated.

Existing stored tokens will be converted when the app is first
launched, and new tokens will be converted when they are
added in settings.
@nhinds nhinds force-pushed the faster-hmac-sha1 branch from 478a617 to a37e97c Compare June 14, 2019 08:03
@nhinds
Copy link
Contributor Author

nhinds commented Jun 14, 2019

Re-tested by removing all codes, reverting to master, loading in a bunch of codes (including a normal-sized code, AWS codes, and the 26-character test code from app/index.js), and switching back over to this branch. All the codes got migrated over to uint8array format.

Also tested removing and re-adding the same codes as above, which seemed to work fine.

@Lixxia
Copy link
Owner

Lixxia commented Aug 4, 2019

Hey, not sure if you're still looking at this or not. I did a fresh clone and install of your branch and seems there's still a few issues. There's a leftover reference to the nodeWrap (p) function in js-sha1.js that causes a ReferenceError. A quick attempt at trying to remove it and re-minify causes some other token handling errors down the road. Mind taking another look?

@nhinds
Copy link
Contributor Author

nhinds commented Aug 5, 2019

It has been working fine for me for months after the latest push, not sure what would be causing reference errors. The nodeWrap/p function should never be called in Fitbit because it's guarded by a check for node.js, which should not be true for Fitbit's js interpreter... Is something broken or are there logs somewhere that show the errors?

I'll take a closer look later this week when I have time to debug.

The simulator's companion app appears to be written in node.js,
or at least triggers js-sha1's node.js detection.
@nhinds nhinds force-pushed the faster-hmac-sha1 branch from 3832052 to a32bb1d Compare August 7, 2019 09:20
@nhinds
Copy link
Contributor Author

nhinds commented Aug 7, 2019

I found what you were referring to. It worked fine on my watch and phone, but I got this error when running under the simulator:

[8:23:36 PM] Companion: ReferenceError: p is not defined
  c at common/js-sha1.js:16,925
  ? at common/js-sha1.js:16,5266
  ? at common/js-sha1.js:16,2

Looks like the simulator's companion is running under node.js, or at least something that looks like node.js to the js-sha1 library. I've added the settings which disable node.js/commonjs support in js-sha1 and it seems to work fine in the simulator now. Are you able to check whether it is working for you as well?

@Lixxia
Copy link
Owner

Lixxia commented Aug 8, 2019

Everything looks good, thanks so much for your work!

@Lixxia Lixxia merged commit 3f107b8 into Lixxia:master Aug 8, 2019
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.

2 participants