From 1e9ad63dbd040d7908e75c8b36edd63cf95019e8 Mon Sep 17 00:00:00 2001 From: "Joakim L. Christiansen" <5984627+JoakimCh@users.noreply.github.com> Date: Mon, 6 Sep 2021 14:12:38 +0200 Subject: [PATCH] Version 2.0.0 --- changelog.md | 9 ++ package.json | 7 +- readme.md | 28 +++---- readmeTemplate.md | 28 +++---- source/pluggablePrng.js | 4 +- source/webCrypto.js | 70 ++++++++++------ tests/{perf_all_but_one.js => perf_all.js} | 10 ++- tests/perf_web_crypto.js | 97 ---------------------- tests/{test_all_but_one.js => test_all.js} | 10 ++- tests/test_web_crypto.js | 73 ---------------- 10 files changed, 105 insertions(+), 231 deletions(-) create mode 100644 changelog.md rename tests/{perf_all_but_one.js => perf_all.js} (90%) delete mode 100644 tests/perf_web_crypto.js rename tests/{test_all_but_one.js => test_all.js} (89%) delete mode 100644 tests/test_web_crypto.js diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..d7a97e9 --- /dev/null +++ b/changelog.md @@ -0,0 +1,9 @@ +# Changelog + +Changelog for the [pluggable-prng](https://www.npmjs.com/package/pluggable-prng) NPM package. + +## v2.0.0 - 2021.09.06: + +### Changed + +* Optimized the performance of `RandomGenerator_WebCrypto` by encrypting more data at each call to `crypto.subtle.encrypt` and buffering it for later calls to `randomUint32`. It's now 5110% faster! This change makes it incompatible with previosly exported states, hence I upped the version number to 2.0.0. diff --git a/package.json b/package.json index 40def0e..4a49ec2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pluggable-prng", - "version": "1.0.1", + "version": "2.0.0", "author": "Joakim L. Christiansen (https://joakimch.github.io)", "license": "MIT", "type": "module", @@ -10,7 +10,7 @@ ], "repository": "github:JoakimCh/pluggable-prng", "funding": "https://joakimch.github.io/funding.html", - "description": "An ES module with a class providing a \"Pseudo-random number generator\" which is \"pluggable\" meaning you can plug-in any PRNG algorithm. It's also \"seedable\" meaning that it can have a reproducible (deterministic) output based on its starting seed. The module includes plugins for some fast and good insecure PRNG's (Alea, Sfc32, Mulberry32, Pcg32), but also a much slower cryptographically secure PRNG which is using the Web Crypto API. Compatible with Node.js, Deno¹ and the browser².", + "description": "An ES module with a class providing a \"Pseudo-random number generator\" which is \"pluggable\" meaning you can plug-in any PRNG algorithm. It's also \"seedable\" meaning that it can have a reproducible (deterministic) output based on its starting seed. The module includes plugins for some fast and good insecure PRNG's (Alea, Sfc32, Mulberry32, Pcg32), but also a fast cryptographically secure PRNG which is using the Web Crypto API. Compatible with Node.js, Deno¹ and the browser².", "keywords": [ "prng", "pluggable", @@ -40,7 +40,8 @@ "scripts": { "test": "node tests/node_test_runAll.js", "perf": "node tests/node_perf_runAll.js", - "docs": "jsdoc2md --template readmeTemplate.md source/pluggablePrng.js > readme.md" + "docs": "jsdoc2md --template readmeTemplate.md source/pluggablePrng.js > readme.md", + "prepublishOnly": "npm run test && npm run docs" }, "devDependencies": { "jsdoc-to-markdown": "^7.0.1" diff --git a/readme.md b/readme.md index 154bf54..efdee64 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,14 @@ # pluggable-prng ### Description -An [ES module](https://flaviocopes.com/es-modules/) with a class providing a [Pseudo-random number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) which is "pluggable", meaning you can plug-in any PRNG algorithm. It's also ["seedable"](https://en.wikipedia.org/wiki/Random_seed) meaning that it can have a reproducible ([deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm)) output based on its starting seed. The module includes plugins for some fast and good (insecure) PRNGs ([Alea](https://github.com/nquinlan/better-random-numbers-for-javascript-mirror#alea), [Sfc32](http://pracrand.sourceforge.net/RNG_engines.txt), [Mulberry32](https://gist.github.com/tommyettinger/46a874533244883189143505d203312c), [Pcg32](https://www.pcg-random.org/download.html)), but also a much slower [cryptographically secure PRNG](https://en.wikipedia.org/wiki/Cryptographically-secure_pseudorandom_number_generator) which is using the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). It's compatible with Node.js, [Deno](https://deno.land)¹ and the browser². +An [ES module](https://flaviocopes.com/es-modules/) with a class providing a [Pseudo-random number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) which is "pluggable", meaning you can plug-in any PRNG algorithm. It's also ["seedable"](https://en.wikipedia.org/wiki/Random_seed) meaning that it can have a reproducible ([deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm)) output based on its starting seed. The module includes plugins for some fast and good (insecure) PRNGs ([Alea](https://github.com/nquinlan/better-random-numbers-for-javascript-mirror#alea), [Sfc32](http://pracrand.sourceforge.net/RNG_engines.txt), [Mulberry32](https://gist.github.com/tommyettinger/46a874533244883189143505d203312c), [Pcg32](https://www.pcg-random.org/download.html)), but also a fast [cryptographically secure PRNG](https://en.wikipedia.org/wiki/Cryptographically-secure_pseudorandom_number_generator) which is using the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). It's compatible with Node.js, [Deno](https://deno.land)¹ and the browser². -1. The Web Crypto PRNG is not compatible with Deno as of Deno v1.9.2, everything else is. +1. The Web Crypto PRNG is not compatible with Deno as of Deno v1.13.2, everything else is. 2. Everything runs fine in a [Chromium based browser](https://en.wikipedia.org/wiki/Chromium_(web_browser)), for other browsers use [Babel](https://babeljs.io). ### Funding -If you find this useful then please consider helping me out (I'm jobless and sick). For more information visit my [GitHub sponsors page](https://github.com/sponsors/JoakimCh), my [profile](https://github.com/JoakimCh) or my [simple website](https://joakimch.github.io/funding.html). +If you find this useful then please consider helping me out (I'm jobless and sick). For more information visit my [GitHub profile](https://github.com/JoakimCh). ### Some features @@ -54,18 +54,18 @@ The 4 last exports in this list are used internally but was made available to an On my `Intel® Core™ i5-4200U CPU @ 1.60GHz × 4` this is a typical result (notice the runtime optimization kicking in after some iterations): ``` Iterations: 10000 -Alea: 112.943ms -Mulberry32: 101.558ms -Sfc32: 52.976ms -Pcg32: 74.819ms -WebCrypto: 4.813s +Alea: 102.733ms +Mulberry32: 86.849ms +Sfc32: 49.118ms +Pcg32: 75.141ms +WebCrypto: 151.346ms Iterations: 10000 -Alea: 29.493ms -Mulberry32: 28.869ms -Sfc32: 41.551ms -Pcg32: 59.258ms -WebCrypto: 4.689s +Alea: 30.941ms +Mulberry32: 25.302ms +Sfc32: 35.392ms +Pcg32: 53.336ms +WebCrypto: 90.856ms ``` ### How to use @@ -512,6 +512,6 @@ This function allows you to change the seed without having to create a new `Plug ### End of readme ``` -Consciousness was not a creation of your brain, it's the opposite. +Your consciousness is not a creation of your brain, it's the opposite. Remember who you are, you have every answer inside of you. ``` diff --git a/readmeTemplate.md b/readmeTemplate.md index 6221d39..589e5f7 100644 --- a/readmeTemplate.md +++ b/readmeTemplate.md @@ -1,14 +1,14 @@ # pluggable-prng ### Description -An [ES module](https://flaviocopes.com/es-modules/) with a class providing a [Pseudo-random number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) which is "pluggable", meaning you can plug-in any PRNG algorithm. It's also ["seedable"](https://en.wikipedia.org/wiki/Random_seed) meaning that it can have a reproducible ([deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm)) output based on its starting seed. The module includes plugins for some fast and good (insecure) PRNGs ([Alea](https://github.com/nquinlan/better-random-numbers-for-javascript-mirror#alea), [Sfc32](http://pracrand.sourceforge.net/RNG_engines.txt), [Mulberry32](https://gist.github.com/tommyettinger/46a874533244883189143505d203312c), [Pcg32](https://www.pcg-random.org/download.html)), but also a much slower [cryptographically secure PRNG](https://en.wikipedia.org/wiki/Cryptographically-secure_pseudorandom_number_generator) which is using the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). It's compatible with Node.js, [Deno](https://deno.land)¹ and the browser². +An [ES module](https://flaviocopes.com/es-modules/) with a class providing a [Pseudo-random number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) which is "pluggable", meaning you can plug-in any PRNG algorithm. It's also ["seedable"](https://en.wikipedia.org/wiki/Random_seed) meaning that it can have a reproducible ([deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm)) output based on its starting seed. The module includes plugins for some fast and good (insecure) PRNGs ([Alea](https://github.com/nquinlan/better-random-numbers-for-javascript-mirror#alea), [Sfc32](http://pracrand.sourceforge.net/RNG_engines.txt), [Mulberry32](https://gist.github.com/tommyettinger/46a874533244883189143505d203312c), [Pcg32](https://www.pcg-random.org/download.html)), but also a fast [cryptographically secure PRNG](https://en.wikipedia.org/wiki/Cryptographically-secure_pseudorandom_number_generator) which is using the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). It's compatible with Node.js, [Deno](https://deno.land)¹ and the browser². -1. The Web Crypto PRNG is not compatible with Deno as of Deno v1.9.2, everything else is. +1. The Web Crypto PRNG is not compatible with Deno as of Deno v1.13.2, everything else is. 2. Everything runs fine in a [Chromium based browser](https://en.wikipedia.org/wiki/Chromium_(web_browser)), for other browsers use [Babel](https://babeljs.io). ### Funding -If you find this useful then please consider helping me out (I'm jobless and sick). For more information visit my [GitHub sponsors page](https://github.com/sponsors/JoakimCh), my [profile](https://github.com/JoakimCh) or my [simple website](https://joakimch.github.io/funding.html). +If you find this useful then please consider helping me out (I'm jobless and sick). For more information visit my [GitHub profile](https://github.com/JoakimCh). ### Some features @@ -54,18 +54,18 @@ The 4 last exports in this list are used internally but was made available to an On my `Intel® Core™ i5-4200U CPU @ 1.60GHz × 4` this is a typical result (notice the runtime optimization kicking in after some iterations): ``` Iterations: 10000 -Alea: 112.943ms -Mulberry32: 101.558ms -Sfc32: 52.976ms -Pcg32: 74.819ms -WebCrypto: 4.813s +Alea: 102.733ms +Mulberry32: 86.849ms +Sfc32: 49.118ms +Pcg32: 75.141ms +WebCrypto: 151.346ms Iterations: 10000 -Alea: 29.493ms -Mulberry32: 28.869ms -Sfc32: 41.551ms -Pcg32: 59.258ms -WebCrypto: 4.689s +Alea: 30.941ms +Mulberry32: 25.302ms +Sfc32: 35.392ms +Pcg32: 53.336ms +WebCrypto: 90.856ms ``` ### How to use @@ -342,6 +342,6 @@ export class SeedInitializer_Uint64 { ### End of readme ``` -Consciousness was not a creation of your brain, it's the opposite. +Your consciousness is not a creation of your brain, it's the opposite. Remember who you are, you have every answer inside of you. ``` diff --git a/source/pluggablePrng.js b/source/pluggablePrng.js index ddb6170..a5bc5e7 100644 --- a/source/pluggablePrng.js +++ b/source/pluggablePrng.js @@ -59,7 +59,7 @@ export class PluggablePRNG { value.then(() => { // when value is ready this.#initialState = randomGenerator.exportState?.() readyPromiseResolve() - }) + }).catch(error => {throw error}) //#region async implementation this.randomBytes = async function(numBytes = mandatory('numBytes')) { const intsToGet = Math.ceil(numBytes / 4) @@ -158,7 +158,7 @@ export class PluggablePRNG { } randomGenerator = new this.#RandomGenerator(...seeds) rndGenInit(randomGenerator) - }) + }).catch(error => {throw error}) } else { const seeds = [generatedSeed] if (this.#RandomGenerator.seedsNeeded) { diff --git a/source/webCrypto.js b/source/webCrypto.js index b4639b5..828b081 100644 --- a/source/webCrypto.js +++ b/source/webCrypto.js @@ -1,12 +1,18 @@ -const nodejs = (globalThis.Buffer && globalThis.process) ? process.version : undefined +const nodejs = globalThis.process?.versions?.node if (nodejs) globalThis['crypto'] = (await import('crypto')).webcrypto /** * A cryptographically secure `RandomGenerator` using the Web Crypto API's AES-CTR encryption to achieve this. Designed to be used with `SeedInitializer_WebCrypto` for the generation of a secure encryption key used as its input seed. Compared to the other PRNGs it's extremely slow, depending on your usage this might be OK or NOT. Use cases could be card and casino games, etc. */ +const bufferSize = 200_000 // buffered random uint32's +// const zeroedData = new Uint32Array(bufferSize) // no performance gain in doing this export class RandomGenerator_WebCrypto { - #key; #counter; #prevValue // these keeps the state + #key + #iv = new Uint32Array(4) // 4x4 = 16 = 128-bits + #bufferIndex = bufferSize // indicating end of buffer (forcing it to refill) + #bufferedUint32s + #stateJustImported constructor(key) { if (nodejs) { if (!(key instanceof crypto.CryptoKey)) throw Error('The seed must be an instance of a CryptoKey (AES-CTR).') @@ -14,34 +20,50 @@ export class RandomGenerator_WebCrypto { if (!(key instanceof CryptoKey)) throw Error('The seed must be an instance of a CryptoKey (AES-CTR).') } this.#key = key - this.#counter = new Uint32Array(4) } + async #bufferRandomData(incrementCounter = true) { + if (incrementCounter && this.#iv[0]++ == 0xFFFF_FFFF) { // increment our 128-bit counter + this.#iv[0] = 0 + if (this.#iv[1]++ == 0xFFFF_FFFF) { + this.#iv[1] = 0 + if (this.#iv[2]++ == 0xFFFF_FFFF) { + this.#iv[2] = 0 + if (this.#iv[3]++ == 0xFFFF_FFFF) { + this.#iv[3] = 0 + } + } + } + } + const encrypted = await crypto.subtle.encrypt({ + name: 'AES-CTR', + counter: this.#iv, // the rightmost length bits of this block are used for the counter, and the rest is used for the nonce + length: 64 // then the first half of counter is the nonce and the second half is used for the counter + }, this.#key, new Uint32Array(bufferSize) /* zeroedData */) // encrypt zeroed data + this.#bufferedUint32s = new Uint32Array(encrypted) + } async randomUint32() { - // Good luck getting it to repeat, even if it does the usage of #prevValue keeps this PRNG secure. - if (this.#counter[0]++ == 0xFFFF_FFFF) { - this.#counter[0] = 0 - if (this.#counter[1]++ == 0xFFFF_FFFF) this.#counter[1] = 0 - } - const encrypted = await crypto.subtle.encrypt({ - name: 'AES-CTR', - counter: this.#counter, // (a BufferSource) - length: this.#counter.byteLength * 8 - }, this.#key, this.#prevValue || new Uint32Array(1)) - - this.#prevValue = encrypted - return new Uint32Array(encrypted)[0] + if (this.#bufferIndex == bufferSize) { // if more random uint32s are needed + await this.#bufferRandomData() + this.#bufferIndex = 0 + this.#stateJustImported = false + } else if (this.#stateJustImported) { + this.#stateJustImported = false + await this.#bufferRandomData(false) // does not increment the counter in the IV + } + return this.#bufferedUint32s[this.#bufferIndex++] } exportState() { return [ - this.#key, - new Uint32Array(this.#counter.buffer.slice(0)), // slice creates a copy - this.#prevValue + this.#key, + new Uint32Array(this.#iv.buffer.slice(0)), // slice creates a copy + this.#bufferIndex ] } importState(state) { - this.#key = state[0] - this.#counter = new Uint32Array(state[1].buffer.slice(0)) - this.#prevValue = state[2] + this.#key = state[0] + this.#iv = new Uint32Array(state[1].buffer.slice(0)) + this.#bufferIndex = state[2] + this.#stateJustImported = true // so this doesn't have to be async } } @@ -60,7 +82,7 @@ export class SeedInitializer_WebCrypto { } this.#salt = salt if (this.#seed == undefined && this.#salt == undefined) { - this.#seed = crypto.getRandomValues(new Uint8Array(32)) + this.#seed = crypto.getRandomValues(new Uint8Array(32)) } } @@ -82,7 +104,7 @@ export class SeedInitializer_WebCrypto { } else if (this.#salt.byteLength < 32) { throw Error('The salt needs to contain at least 256 bits of data to ensure cryptographically secure random values, bits given: '+this.#salt.byteLength*8) } - + const seedKey = await crypto.subtle.importKey( 'raw', this.#seed, 'HKDF', false, ['deriveKey', 'deriveBits'] ) diff --git a/tests/perf_all_but_one.js b/tests/perf_all.js similarity index 90% rename from tests/perf_all_but_one.js rename to tests/perf_all.js index d7d16db..3266c69 100644 --- a/tests/perf_all_but_one.js +++ b/tests/perf_all.js @@ -7,7 +7,9 @@ import { RandomGenerator_Sfc32, RandomGenerator_Pcg32, SeedInitializer_Uint32, - SeedInitializer_Uint64 + SeedInitializer_Uint64, + RandomGenerator_WebCrypto, + SeedInitializer_WebCrypto } from '../source/pluggablePrng.js' import {assert, assertMoreOrEqual, assertLessOrEqual} from './shared.js' @@ -29,6 +31,10 @@ const prngs = [ 'Pcg32', RandomGenerator_Pcg32, SeedInitializer_Uint64 + ], [ + 'WebCrypto', + RandomGenerator_WebCrypto, + SeedInitializer_WebCrypto ] ] @@ -37,7 +43,7 @@ for (const iterations of [10_000, 10_000, 20_000, 20_000, 30_000, 30_000, 200_00 console.log('Iterations:', iterations) for (const p of prngs) { const prng = new PluggablePRNG({ - seed: 'test', + seed: p[1] != RandomGenerator_WebCrypto ? 'test' : {seed: 'test', salt: 'A secure salt so that we are OK with the weak seed...'}, RandomGenerator: p[1], SeedInitializer: p[2] }) diff --git a/tests/perf_web_crypto.js b/tests/perf_web_crypto.js deleted file mode 100644 index adc345d..0000000 --- a/tests/perf_web_crypto.js +++ /dev/null @@ -1,97 +0,0 @@ - -import { - PluggablePRNG, - RandomGenerator_Alea, - SeedInitializer_Alea, - RandomGenerator_WebCrypto, - SeedInitializer_WebCrypto -} from '../source/pluggablePrng.js' - -import {assert, assertMoreOrEqual, assertLessOrEqual} from './shared.js' - -const prngs = [ - [ - 'Alea (for reference)', - RandomGenerator_Alea, - SeedInitializer_Alea - ], [ - 'WebCrypto', - RandomGenerator_WebCrypto, - SeedInitializer_WebCrypto - ] -] - -if (globalThis.Deno?.version?.deno) { - console.log('Deno doesn\'t support the Web Crypto API, at least not when I wrote this performance test...') -} else { - // Through these iterations you'll notice how the JS optimizer will eventually make some of the code much faster. - for (const iterations of [1000, 1000, 2000, 2000, 5000, 5000, 10_000, 10_000]) { - console.log('Iterations:', iterations) - for (const p of prngs) { - const prng = new PluggablePRNG({ - seed: p[1] == RandomGenerator_WebCrypto ? {seed: 'test', salt: 'it is a cryptographically secure salt'} : 'test', - RandomGenerator: p[1], - SeedInitializer: p[2] - }) - await prngOutputTest(p[0], prng, iterations) - } - console.log() - } -} - -async function prngOutputTest(title, prng, iterations) { - await prng.readyPromise // if set - console.time(title) - for (let i=0; i