csrf icon indicating copy to clipboard operation
csrf copied to clipboard

Speed up token generation

Open ai opened this issue 7 years ago • 51 comments

This PR reduces dependencies number (from 3 to 1) and should a little improve performance according to this benchmark with the same security (nanoid use the same hardware random generator; it’s popular and well maintained).

nanoid                    413,579 ops/sec
uid.sync                  354,882 ops/sec

nanoid/async               85,168 ops/sec
uid                        78,426 ops/sec

nanoid/non-secure       2,718,186 ops/sec
rndm                    2,544,612 ops/sec

@dougwilson what do you think? Should I rename nanoidFast to nanoidNonSecure in R to be more clear?

ai avatar Oct 13 '18 21:10 ai

Needs to at least support Node.js 0.8. What makes it quicker? Is it not possible to speed up our existing dependencies? I have not had great experience with third party dependencies, especially when they seem to very quickly discard support for old Node.js versions. Now, that's their own opinion, though I try to support as far down as is reasonable with the CI infrastructure (which is 0.8 currently).

dougwilson avatar Oct 13 '18 21:10 dougwilson

Needs to at least support Node.js 0.8

Yeap, I support Promise-less environment and it is written by the simple ES5. I especially made few changes to Nano ID to pass csrf tests (great tests, BTW).

What makes it quicker?

Thanks for answering =^_^= In secure generator it happens because of we manager with Buffers in a more optimized way. In non-secure we used a cool trick with Math.floor and old V8 optimizations for asm.js.

Is it not possible to speed up our existing dependencies?

It is possible in theory. But do you want to spend time on maintaining multiple projects and copy ideas every time, when there is a good well-maintained project, which always tries to find a tricky way of optimization? =^_^= (we are focusing not only on performance but also of client-side size)

ai avatar Oct 13 '18 21:10 ai

Yeap, I support Promise-less environment and it is written by the simple ES5. I especially made few changes to Nano ID to pass csrf tests (great tests, BTW).

Ah, sorry, I assumed it didn't because the Node.js 0.8 CI failed for this PR and I looked at the module and it doesn't have any CI for Node.js 0.8 so assumed there is no support. If there is no CI, what is the guarantee that 0.8 is a supported platform in an ongoing fashion?

It is possible in theory. But do you want to spend time on maintaining multiple projects and copy ideas every time, when there is a good well-maintained project, which always tries to find a tricky way of optimization? =^_^= (we are focusing not only on performance but also of client-side size)

Right, I definitely would rather use external deps when possible if they exist and they seem to be aligned with the same target support environments.

dougwilson avatar Oct 13 '18 21:10 dougwilson

If there is no CI, what is the guarantee that 0.8 is a supported platform in an ongoing fashion?

Jest doesn’t support Node.js 0.8 to run a test on CI :(. I think right now most of the test runners drop even Node.js 4 support.

Maybe I can write a script to test critical things manually. Or maybe you have a better idea?

ai avatar Oct 13 '18 21:10 ai

Yea, I don't have a better idea besides just using a testing framework that does support 0.8 (which is what this module does). In my opinion testing frameworks should be supporting pretty much every possible version since they are going to influence what environments real modules can support. But maybe those test frameworks want to use their power to dictate the versions of Node.js people will support, idk.

Coming from the Perl system Node.js feels very fractured to me, with constant major versions of Node.js and module authors quickly discarding support for Node.js versions :(

dougwilson avatar Oct 13 '18 21:10 dougwilson

In my opinion testing frameworks should be supporting pretty much every possible version since they are going to influence what environments real modules can support.

Totally agree. I fight with Jest community for a few months for Node.js 4 even at the moment when it was still LTS.

Fair enough note about CI and Node.js 0.8. I will try to fix it soon.

ai avatar Oct 13 '18 21:10 ai

Done. I added Node.js 4, 0.12 and 0.8 to CI:

https://travis-ci.org/ai/nanoid

It was tricky, since yarn install doesn’t work for many reasons. So I used custom installer and custom task runner with manually testing core features for Node 4-0.8.

ai avatar Oct 13 '18 22:10 ai

@dougwilson I thought about how to fix npm error in csrf CI and do not have a solution right now.

But maybe it is just a cache issue and if we will call it again, it will be installed?

ai avatar Oct 15 '18 03:10 ai

We released Nano ID 1.3.1. It contains client-side optimization (not important for Node.js version). But maybe you will like the way. We wrote a bruteforcer to find the most gzip-friendly alphabet symbols order.

Also, your suggestion testing Nodejs 0.8 found an issue in Travis CI. Not it is fixed.

ai avatar Oct 20 '18 07:10 ai

@dougwilson if you don’t want to speed up it by some reason, just say it. It is not a big deal for me, but I will be able to remove Node.js 0.8—4 support code (it was added only to fit csrf Node.js supporting policies)

ai avatar Oct 30 '18 05:10 ai

I just need the CI to pass here to merge...

dougwilson avatar Oct 30 '18 12:10 dougwilson

@dougwilson sure. I wrote to npm support to ask about storage npm behaviour on Node.js 0.8.

ai avatar Oct 30 '18 23:10 ai

@dougwilson done, I fixed CI 🎉

ai avatar Oct 31 '18 00:10 ai

Hi @ai, awesome just saw that 🎉 . But this looks like a breaking change: it would no longer install on machines that it used to install on. Our current dependencies don't have any issue with the verison of npm that is included with Node.js 0.8. Is there any particular reason why this new module does not support the same npm version?

At minimum, if a higher version of npm is required, can you update the engines field to add this requirement? If merging this is going to require a new major version, at that point it would likely just be time to remove Node.js 0.8 support and just make Node.js 0.10 the minimum version., though. I'll need to then cascade that support drop through the modules I have that uses this one, too, of course.

Circling back around, is there any way that this new module can just run under the same version of npm the rest do? Or should this just become a larger discussion around it being a major breaking change?

dougwilson avatar Oct 31 '18 00:10 dougwilson

Circling back around, is there any way that this new module can just run under the same version of npm the rest do?

I asked npm support.

I think Node.js 0.8 is too old for npm and they serve old registry for this old npm.

ai avatar Oct 31 '18 00:10 ai

Strange. Most of all my modules are Node.js 0.8+ and even ones I released last week install on Node.js 0.8 without issue, with the npm that is included out-of-the-box (npm 1.2.30).

dougwilson avatar Oct 31 '18 00:10 dougwilson

This is a fresh build of master on Node.js 0.8 and npm 1.2.30: https://travis-ci.org/pillarjs/csrf/jobs/448623371 . It didn't seem to have any issues downloading and installing modules from the npm registry. It's just using the standard registry https://registry.npmjs.org/

dougwilson avatar Oct 31 '18 00:10 dougwilson

@dougwilson I agree that it is strange. Let’s wait for npm support answer.

ai avatar Oct 31 '18 00:10 ai

👍 there is an unreleased change to master, so I'm going to release a simple patch version of this module in the meantime, don't panic if you see it. I still think this module is a better method. I may also work to add a simple benchmark to this module in the meantime as well, so (from the usage of the two libs through this module) we can add what the perf improvement is to the changelog too 🎉

dougwilson avatar Oct 31 '18 00:10 dougwilson

So speaking of Node.js 0.8, I was playing around with this PR on Node.js 0.8 on my machine and I encountered a bug I basically forgot about: https://github.com/pillarjs/csrf/pull/10#issuecomment-220851160

Basically in Node.js 0.8 (and 0.10, perhaps some more), the underlying crypto engine in OpenSSL will sometimes throw an error that it's not seeded when the process starts and you try to grab random bytes too fast. To solve that issue, I ended up creating the module https://github.com/crypto-utils/random-bytes and that is used in turn by uid-safe. Since this PR is replacing uid-safe, but it ends up just calling crypto.randomBytes directly (https://github.com/ai/nanoid/blob/master/random.js#L14) this error resurfaces.

Is it possible to get this work-around added to the nanoid module as well?

dougwilson avatar Oct 31 '18 01:10 dougwilson

Basically in Node.js 0.8 (and 0.10, perhaps some more), the underlying crypto engine in OpenSSL will sometimes throw an error that it's not seeded when the process starts and you try to grab random bytes too fast.

Sure, I can add it.

ai avatar Oct 31 '18 01:10 ai

@dougwilson I found a way to fix npm issue in Node.js 0.8 without updating npm (npm 1.x can’t understand ^ version selector)

ai avatar Oct 31 '18 01:10 ai

I will release new Nano ID with OpenSSL issue fix in next hour

ai avatar Oct 31 '18 01:10 ai

Oh, nice, I didn't even realize that was there in the PR. I always pin dependency versions anyway to specific versions such that I can see what the changes are before users installing these modules suddenly encounter changes. It has helped a lot in the past especially when a dependency accidentally releases a breaking change to a subset of users but it takes a long time to get it resolved, and users usually are not sure where to report the issue to.

dougwilson avatar Oct 31 '18 01:10 dougwilson

@dougwilson done:

  1. I added workaround for RNG not seeded error.
  2. I replace ~ to the direct version in dependencies

ai avatar Oct 31 '18 02:10 ai

Awesome, @ai ! Let me just finish up this release of csrf and then I'll test out this PR again on the same machine to double-check the error is gone 👍

dougwilson avatar Oct 31 '18 02:10 dougwilson

Hi @ai , sorry to come back again 😢. So I tested the updated PR (and yes, it installed nanoid 1.3.2), and I still had the same issue, the seed error. I hooked up a debugger and stepping through the code, it looks like there is still no handling to retry for the error in the code.

The very high-level code that triggers the code path is node -pe 'require("csrf")().secretSync()'. When I walked it through, it seems like it's just calling crypto.randomBytes directly.

What I'm finding is that secretSync is calling this function: https://github.com/ai/nanoid/blob/master/index.js#L21

That in turn calls the random function, defined at https://github.com/ai/nanoid/blob/master/index.js#L1

This just ends up being a direct reference to crypto.randomBytes here: https://github.com/ai/nanoid/blob/master/random.js#L14

I hope that helps! I also finished the simple benchmarks I promised and pushed to master. They are runnable with just npm run bench. I really quickly on 8.12.0 just ran them on the current master and then again on this PR branch (rebased onto master so the benchmarks are there). This is what I found:

verify is basically the same speed (that's fine) secretSync is much faster with nanoid, that is awesome! create is a bit slower with nanoid, which is weird

A typical application embedding this module has the flow of (1) call secret* once per user to initialize their session (2) call create on every page load that needs a token (but a lot of people through csurf just end up calling it on every request) and then (3) call verify on a page submit with a token.

What this means that the typical hot path in an app is the create call, as that is typically on every page load that needs a token (though there are apps out there that incorrectly call it on every request). In contrast, the secret* is only once per user session.

Ideally the hot path is the best one to get a speed boost, so I was kind of surprised (at least on my machine) to see the performance go down in the spot that is going to be the hottest path.

I'm not sure yet what is causing the decrease, but the increase in secret* is awesome. It's possible we can just sub nanoid in the secret* functions and keep the other module in the create function, to get the fastest path on all of them. Does that sound like a good solution, or no?

For reference these are the comparisons on my machine:

master
> [email protected] bench ./csrf
> node benchmark/index.js

  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  modules@57
  [email protected]
  napi@3
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  tz@2018e

> node benchmark/create.js

  create

  1 test completed.

  create x 405,126 ops/sec ±0.48% (191 runs sampled)

> node benchmark/secret.js

  secret

  3 tests completed.

  secretSync        x 334,375 ops/sec ±0.76% (183 runs sampled)
  secret - callback x  53,006 ops/sec ±0.73% (177 runs sampled)
  secret - promise  x  49,499 ops/sec ±1.00% (176 runs sampled)

> node benchmark/verify.js

  verify

  2 tests completed.

  verify - valid   x 79,736 ops/sec ±1.07% (177 runs sampled)
  verify - invalid x 78,411 ops/sec ±1.30% (180 runs sampled)
pr-14
> [email protected] bench ./csrf
> node benchmark/index.js

  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  modules@57
  [email protected]
  napi@3
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  tz@2018e

> node benchmark/create.js

  create

  1 test completed.

  create x 388,440 ops/sec ±0.43% (190 runs sampled)

> node benchmark/secret.js

  secret

  3 tests completed.

  secretSync        x 454,359 ops/sec ±0.48% (190 runs sampled)
  secret - callback x  58,837 ops/sec ±0.64% (178 runs sampled)
  secret - promise  x  56,138 ops/sec ±0.60% (178 runs sampled)

> node benchmark/verify.js

  verify

  2 tests completed.

  verify - valid   x 78,578 ops/sec ±1.15% (182 runs sampled)
  verify - invalid x 77,687 ops/sec ±1.32% (181 runs sampled)

P.S. thanks for being so responsive. You rock!

dougwilson avatar Oct 31 '18 03:10 dougwilson

What I'm finding is that secretSync is calling this function: https://github.com/ai/nanoid/blob/master/index.js#L21

Oh, yeap. I add not seeded fix only for async version. I will add it for sync version tomorrow.

I also will think about verify performance drop

ai avatar Oct 31 '18 03:10 ai

@dougwilson what Node.js version do you? Some optimization (like that asm.js optimisation for nanoid/non-secure) will work only on modern Node.js version (it is a good to question, what to do in this case)

ai avatar Oct 31 '18 03:10 ai

Hi @ai I was using Node.js 8.12.0, which was the version "recommended for most users" just as of last week, though they rolled over the versions now. I was just testing on what I have most of my production projects running on just as a quick test.

dougwilson avatar Oct 31 '18 03:10 dougwilson