Skip to content

Commit

Permalink
Version 2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
JoakimCh committed Sep 6, 2021
1 parent a551104 commit 1e9ad63
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 231 deletions.
9 changes: 9 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
28 changes: 14 additions & 14 deletions readme.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
```
28 changes: 14 additions & 14 deletions readmeTemplate.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
```
4 changes: 2 additions & 2 deletions source/pluggablePrng.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
70 changes: 46 additions & 24 deletions source/webCrypto.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,69 @@

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).')
} else {
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
}
}

Expand All @@ -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))
}
}

Expand All @@ -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']
)
Expand Down
10 changes: 8 additions & 2 deletions tests/perf_all_but_one.js → tests/perf_all.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -29,6 +31,10 @@ const prngs = [
'Pcg32',
RandomGenerator_Pcg32,
SeedInitializer_Uint64
], [
'WebCrypto',
RandomGenerator_WebCrypto,
SeedInitializer_WebCrypto
]
]

Expand All @@ -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]
})
Expand Down
Loading

0 comments on commit 1e9ad63

Please sign in to comment.