Skip to content

Commit

Permalink
feat: Remove default Node.js support
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Default Node.js support is removed. It is hard to use
different APIs in different environments right in today's bundled world.

Closes #116.
  • Loading branch information
luczsoma committed Oct 4, 2019
1 parent 8ef3b05 commit 8423e88
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 402 deletions.
59 changes: 47 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async function main() {
}
```

Node.js and browser environments are both supported. For more information, see the **Entropy sources** section below.
By default, only browser environments are supported as of version 2.0, but you can inject your own entropy source (e.g. in a Node.js environment). For more information, see the **Entropy sources** section below.

## API

Expand Down Expand Up @@ -200,21 +200,13 @@ boolean(): Promise<boolean>;

### Used entropy sources

In a web browser environment, `window.crypto.getRandomValues`, in a Node.js environment, `crypto.randomFill` is used as the underlying CSPRNG. This is automatically detected by the `RandomGenerator`.

### Using a custom entropy source

**WARNING!** Unless you are a seasoned cryptography expert possessing comprehensive knowledge about random/pseudo-random value generation, **DO NOT use any custom entropy source implementation other than the default**, or found in well-tested, popular libraries survived many years under public scrutiny. Cryptography — and mostly random generation — can be messed up very easily. If you use anything else than a CSPRNG/TRNG for gathering entropy, the values you generate using that entropy source will not be random in the cryptographic meaning, and thus will NOT be suitable for being used as passwords/keys/nonces/etc.

Providing no arguments in the constructor, the `RandomGenerator` is instantiated using the default `EnvironmentDetectingEntropyProvider` as its entropy source. This detects if the code is run in a web browser or in a Node.js process, and uses the available cryptography API on the given platform as its underlying random source. (As stated above: in a web browser, `window.crypto.getRandomValues`, in Node.js, `crypto.randomFill` is used.)

As long as it implements the `EntropyProvider` interface specified below, you can use any kind of entropy source by providing it to the constructor at instantiating the `RandomGenerator`.
Providing no arguments in the constructor, the `RandomGenerator` is instantiated using the default `BrowserEntropyProvider` as its entropy source. This will look for `window.crypto.getRandomValues`.

```
type UnsignedTypedArray = Uint8Array | Uint16Array | Uint32Array;
interface EntropyProvider {
getRandomValues<T extends UnsignedTypedArray>(array: T): Promise<T>;
getRandomValues<T extends UnsignedTypedArray>(array: T): T | Promise<T>;
}
```

Expand All @@ -225,14 +217,57 @@ class RandomGenerator {
*/
private readonly entropyProvider: EntropyProvider;
constructor(entropyProvider: EntropyProvider = new EnvironmentDetectingEntropyProvider()) {
constructor(entropyProvider: EntropyProvider = new BrowserEntropyProvider()) {
this.entropyProvider = entropyProvider;
}
// …
}
```

However, you can inject your own entropy source into the `RandomGenerator` as long as it implements the required `EntropyProvider` interface specified above.

E.g. in Node.js, you can create `nodeJsEntropyProvider.ts`:

```
import { randomFill } from 'crypto';
import { EntropyProvider, UnsignedTypedArray } from '@diplomatiq/crypto-random';
export class NodeJsEntropyProvider implements EntropyProvider {
public async getRandomValues<T extends UnsignedTypedArray>(array: T): Promise<T> {
return new Promise<T>((resolve, reject): void => {
randomFill(array, (error: Error | null, array: T) => {
if (error !== null) {
reject(error);
return;
}
resolve(array);
});
});
}
}
```

And than in your Node.js application, use `RandomGenerator` as follows:

```
import { RandomGenerator } from '@diplomatiq/crypto-random';
import { NodeJsEntropyProvider } from './nodeJsEntropyProvider';
// …
async function main() {
const entropyProvider = new NodeJsEntropyProvider();
const randomGenerator = new RandomGenerator(entropyProvider);
const randomString = await randomGenerator.alphanumeric(32);
// randomString will contain a 32-character-long alphanumeric string
}
```

### Using a custom entropy source

**WARNING!** Unless you are a seasoned cryptography expert possessing comprehensive knowledge about random/pseudo-random value generation, **DO NOT use any custom entropy source implementation other than the default**, or found in well-tested, popular _cryptographic_ libraries survived many years under public scrutiny. Cryptography — and mostly random generation — can be messed up very easily. If you use anything else than a CSPRNG/TRNG for gathering entropy, the values you generate using that entropy source will not be random in the cryptographic meaning, and thus will NOT be suitable for being used as passwords/keys/nonces/etc.

## Discrete uniform distribution

In this library's context, discrete uniform distribution means that any character from a given alphabet will be chosen with equal probability into the generated random value. At generating any kind of cryptographic keys (passwords, authentication tokens, nonces), uniform distribution is crucial: in every other case the size of the key space decreases in some degree (thus finding the key is easier).
Expand Down
54 changes: 54 additions & 0 deletions src/browserEntropyProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { EntropyProvider } from './entropyProvider';
import { UnsignedTypedArray } from './unsignedTypedArray';

export class BrowserEntropyProvider implements EntropyProvider {
/**
* The crypto implementation used in the browser. The window.crypto object is used.
*/
private readonly crypto?: Crypto;

/**
* According to the Web Crypto standard, there is a 2 ** 16 bytes quota for requesting entropy at once.
*/
private static readonly BROWSER_ENTROPY_QUOTA_BYTES = 65536;

public constructor() {
if (
typeof window === 'undefined' ||
typeof window.crypto === 'undefined' ||
typeof window.crypto.getRandomValues === 'undefined'
) {
throw new Error('window.crypto.getRandomValues is not available');
}

this.crypto = window.crypto;
}

/**
* Puts random values into the given @param array, and returns the array.
* If the array's length is greater than the general @member BROWSER_ENTROPY_QUOTA_BYTES,
* it is divided into chunks, and filled chunk-by-chunk.
*/
public getRandomValues<T extends UnsignedTypedArray>(array: T): T {
if (this.crypto === undefined) {
throw new Error('AssertError: no crypto');
}

if (array.byteLength <= BrowserEntropyProvider.BROWSER_ENTROPY_QUOTA_BYTES) {
return this.crypto.getRandomValues(array);
}

let remainingBytes = array.byteLength;
while (remainingBytes > 0) {
const availableEntropyBytes = Math.min(remainingBytes, BrowserEntropyProvider.BROWSER_ENTROPY_QUOTA_BYTES);
const chunkStart = array.byteLength - remainingBytes;
const chunkLength = availableEntropyBytes / array.BYTES_PER_ELEMENT;
const chunkEnd = chunkStart + chunkLength;
const chunkToFill = array.subarray(chunkStart, chunkEnd);
this.crypto.getRandomValues(chunkToFill);
remainingBytes -= availableEntropyBytes;
}

return array;
}
}
2 changes: 1 addition & 1 deletion src/entropyProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UnsignedTypedArray } from './unsignedTypedArray';

export interface EntropyProvider {
getRandomValues<T extends UnsignedTypedArray>(array: T): Promise<T>;
getRandomValues<T extends UnsignedTypedArray>(array: T): T | Promise<T>;
}
125 changes: 0 additions & 125 deletions src/environmentDetectingEntropyProvider.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/randomGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Alphabets } from './alphabets';
import { BrowserEntropyProvider } from './browserEntropyProvider';
import { ConfigurableUniquenessStore } from './configurableUniquenessStore';
import { EntropyProvider } from './entropyProvider';
import { EnvironmentDetectingEntropyProvider } from './environmentDetectingEntropyProvider';
import { RandomGeneratorErrorCodes } from './randomGeneratorErrorCodes';
import { UnsignedTypedArray } from './unsignedTypedArray';

Expand All @@ -17,7 +17,7 @@ export class RandomGenerator {
*/
private readonly entropyProvider: EntropyProvider;

public constructor(entropyProvider: EntropyProvider = new EnvironmentDetectingEntropyProvider()) {
public constructor(entropyProvider: EntropyProvider = new BrowserEntropyProvider()) {
this.entropyProvider = entropyProvider;
}

Expand Down
96 changes: 96 additions & 0 deletions test/specs/browserEntropyProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { expect } from 'chai';
import { beforeEach } from 'mocha';
import { BrowserEntropyProvider } from '../../src/browserEntropyProvider';
import { EntropyProvider } from '../../src/entropyProvider';
import { windowMock } from '../utils/windowMock';

describe('BrowserEntropyProvider', () => {
let entropyProvider: EntropyProvider;

before(() => {
// @ts-ignore
global.window = windowMock();
});

after(() => {
// @ts-ignore
global.window = undefined;
});

beforeEach(() => {
entropyProvider = new BrowserEntropyProvider();
});

it('should work', async () => {
const array = new Uint8Array(10);
expect(array.every(v => v === 0)).to.be.true;
const sameArray = await entropyProvider.getRandomValues(array);
expect(array).to.deep.equal(sameArray);
expect(array.some(v => v !== 0)).to.be.true;
});

it('should work for arrays larger than 65536 bytes', async () => {
const array = new Uint8Array(100000);
expect(array.every(v => v === 0)).to.be.true;
const sameArray = await entropyProvider.getRandomValues(array);
expect(array).to.deep.equal(sameArray);
expect(array.some(v => v !== 0)).to.be.true;
expect(array.subarray(80000, 90000).some(v => v !== 0)).to.be.true;
});

it('should throw if window is not available', () => {
// @ts-ignore
global.window = undefined;

try {
new BrowserEntropyProvider();
expect.fail('did not throw');
} catch (e) {
expect(e.message).to.equal('window.crypto.getRandomValues is not available');
}

// @ts-ignore
global.window = windowMock();
});

it('should throw if window.crypto is not available', () => {
// @ts-ignore
global.window.crypto = undefined;

try {
new BrowserEntropyProvider();
expect.fail('did not throw');
} catch (e) {
expect(e.message).to.equal('window.crypto.getRandomValues is not available');
}

// @ts-ignore
global.window = windowMock();
});

it('should throw if window.crypto.getRandomValues is not available', () => {
// @ts-ignore
global.window.crypto.getRandomValues = undefined;

try {
new BrowserEntropyProvider();
expect.fail('did not throw');
} catch (e) {
expect(e.message).to.equal('window.crypto.getRandomValues is not available');
}

// @ts-ignore
global.window = windowMock();
});

it('should throw if browserCrypto is undefined', async () => {
// @ts-ignore
entropyProvider.crypto = undefined;

try {
await entropyProvider.getRandomValues(new Uint8Array(1));
} catch (e) {
expect(e.message).to.equal('AssertError: no crypto');
}
});
});
Loading

0 comments on commit 8423e88

Please sign in to comment.