hectorm / otpauth

One Time Password (HOTP/TOTP) library for Node.js, Deno, Bun and browsers.
https://hectorm.github.io/otpauth/
MIT License
952 stars 54 forks source link

Use WebCrypto instead for performance & bundle size improvements #464

Closed memcorrupt closed 4 months ago

memcorrupt commented 8 months ago

See #296 for original issue.


This package uses jsSHA as a dependency, which adds 21.4 kB to the bundle size (ESM minified). This is ~77.4% of this module's bundle size.

The WebCrypto API (including SubtleCrypto) is widely supported in browsers (while using HTTPS), and provides native implementations of crypto functions. Additionally, it supports all the currently supported algorithms in hmac-digest.js except SHA-224; however, only SHA-1, SHA-256, and SHA-512 are actually specified in the TOTP RFC.

Additionally, because the library's dependencies are currently bundled, jsSHA may be unnecessarily bundled in a final project multiple times if dependent projects contain files or dependencies that require jsSHA.

Since Web Crypto API is asynchronous only, this improvement could either be implemented as a breaking change, or with seperate asynchronous generate/validate methods that include the Web Crypto implementation.

hectorm commented 8 months ago

What I initially stated in the linked issue still applies, I prefer not to use the Web Crypto API if it means breaking compatibility, since jsSHA, while not native, is a widely used SHA implementation that works well.

But there is room for improvement here, the first thing would be to study the feasibility of creating an alternative build that uses the Web Crypto API (although it would not be trivial as this build would have to expose a slightly different API since it would have to use asynchronous methods) and the second thing would be to have a build that does not bundle jsSHA to avoid it being included multiple times in case another dependency imports it.

Right now this is not a priority for me, but I leave this issue open to review it in the future, a PR would also be welcome.

memcorrupt commented 7 months ago

@hectorm I had started some progress here, but I wasn't quite sure how to produce a WebCrypto build & a jsSHA build to maintain the browser compatbility required to upstream the changes.

If this is a good start, you could possibly finish it or provide the insight needed to continue.

hectorm commented 4 months ago

After a benchmark with jsSHA, @noble/hashes and your SubtleCrypto fork, I noticed that the latter is much slower (possibly due to the overhead of the async function), so I don't think I will use the Web Crypto API for the time being.

Although thanks to this I'm thinking about switching to @noble/hashes as it would reduce the minified bundle size from 30 KB to 24 KB (without compression). The idea of providing a variant that doesn't bundle the HMAC library to avoid duplication still stands, but that's outside the scope of this issue, so I'll close this one and create another.

Bun 1.1.10:

Task Name ops/sec Average Time (ns) Margin Samples
totpValidate 55 18172903.26860257 ±0.29% 551
totpNobleValidate 103 9625471.101058695 ±1.04% 1039
totpSubtleValidate 23 42465075.48305099 ±2.07% 236

Deno 1.43.5:

Task Name ops/sec Average Time (ns) Margin Samples
totpValidate 74 13454301.075268818 ±0.85% 744
totpNobleValidate 73 13608163.265306123 ±1.55% 735
totpSubtleValidate 19 51517948.71794872 ±0.81% 195

Chromium 125:

Task Name ops/sec Average Time (ns) Margin Samples
totpValidate 82 12078045.838318126 ±0.57% 829
totpNobleValidate 71 14077918.424788447 ±0.60% 711
totpSubtleValidate 8 122246341.46336012 ±2.72% 82

Firefox 126:

Task Name ops/sec Average Time (ns) Margin Samples
totpValidate 62 16059390.048154093 ±0.66% 623
totpNobleValidate 39 25173366.834170856 ±0.92% 398
totpSubtleValidate 11 88796460.17699115 ±7.37% 113
Source ```js import { Bench } from "tinybench"; import * as otpauth from "otpauth"; import * as otpauthNoble from "otpauthNoble"; import * as otpauthSubtle from "otpauthSubtle"; (async () => { const bench = new Bench({ time: 10000, warmupTime: 1000, }); const totp = new otpauth.TOTP({ secret: "NB2W45DFOIZA" }); bench.add("totpValidate", () => { totp.validate({ token: "000000", window: 1000 }); }); const totpNoble = new otpauthNoble.TOTP({ secret: "NB2W45DFOIZA" }); bench.add("totpNobleValidate", () => { totpNoble.validate({ token: "000000", window: 1000 }); }); const totpSubtle = new otpauthSubtle.TOTP({ secret: "NB2W45DFOIZA" }); bench.add("totpSubtleValidate", async () => { await totpSubtle.validate({ token: "000000", window: 1000 }); }); await bench.warmup(); await bench.run(); console.table(bench.table()); })(); ```