Skip to content

Add random source modules for node, browsers, and cross-platform usage #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/crypto-random-source-browser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.js
*.js.map
*.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {randomValues} from '../lib/ie11RandomValues';

beforeEach(() => {
(window as any).msCrypto = {
getRandomValues(toFill: Uint8Array) {
const view = new DataView(toFill.buffer);
for (let i = 0; i < toFill.byteLength; i++) {
view.setUint8(i, 0x00);
}
}
};
});

describe('randomValues', () => {
it('should call the random source built into IE 11', async () => {
expect(await randomValues(4))
.toMatchObject(Uint8Array.from([0, 0, 0, 0]));
});

it(
'should convert a failed random generation into a promise rejection',
async () => {
(window as any).msCrypto.getRandomValues = () => {
throw new Error('PANIC PANIC');
};

await randomValues(12).then(
() => { throw new Error('The promise should have been rejected'); },
() => { /* promise rejected, just as expected */ }
);
}
);
});
113 changes: 113 additions & 0 deletions packages/crypto-random-source-browser/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {randomValues} from '../';

jest.mock('@aws/crypto-ie11-detection', () => {
return { isMsWindow: jest.fn() };
});
import {isMsWindow} from '@aws/crypto-ie11-detection';
jest.mock('@aws/crypto-supports-webCrypto', () => {
return { supportsWebCrypto: jest.fn() };
});
import {supportsWebCrypto} from '@aws/crypto-supports-webCrypto';

jest.mock('../lib/ie11RandomValues', () => {
return { randomValues: jest.fn() };
});
import {randomValues as ie11RandomValues} from '../lib/ie11RandomValues';

jest.mock('../lib/jsRandomValues', () => {
return { randomValues: jest.fn() };
});
import {randomValues as jsRandomValues} from '../lib/jsRandomValues';

jest.mock('../lib/webCryptoRandomValues', () => {
return { randomValues: jest.fn() };
});
import {randomValues as webCryptoRandomValues} from '../lib/webCryptoRandomValues';

beforeEach(() => {
(isMsWindow as any).mockReset();
(supportsWebCrypto as any).mockReset();
(ie11RandomValues as any).mockReset();
(jsRandomValues as any).mockReset();
(webCryptoRandomValues as any).mockReset();
});

describe('implementation selection', () => {
it('should use WebCrypto when available', async () => {
(supportsWebCrypto as any).mockImplementation(() => true);

await randomValues(1);

expect((webCryptoRandomValues as any).mock.calls.length).toBe(1);
expect((jsRandomValues as any).mock.calls.length).toBe(0);
});

it('should use IE 11 WebCrypto when available', async () => {
(isMsWindow as any).mockImplementation(() => true);

await randomValues(1);

expect((ie11RandomValues as any).mock.calls.length).toBe(1);
expect((jsRandomValues as any).mock.calls.length).toBe(0);
});

it(
'should prefer standards-compliant WebCrypto over IE 11 WebCrypto',
async () => {
(supportsWebCrypto as any).mockImplementation(() => true);
(isMsWindow as any).mockImplementation(() => true);

await randomValues(1);

expect((webCryptoRandomValues as any).mock.calls.length).toBe(1);
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
expect((jsRandomValues as any).mock.calls.length).toBe(0);
}
);

it('should fall back on the SJCL', async () => {
(supportsWebCrypto as any).mockImplementation(() => false);
(isMsWindow as any).mockImplementation(() => false);

await randomValues(1);

expect((webCryptoRandomValues as any).mock.calls.length).toBe(0);
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
expect((jsRandomValues as any).mock.calls.length).toBe(1);
});
});

describe('global detection', () => {
const _window = window;
const _self = self;

beforeEach(() => {
delete (global as any).window;
delete (global as any).self;
});

afterAll(() => {
window = _window;
self = _self;
});

it(
'should fall back to the SJCL if neither window nor self is defined',
async () => {
await randomValues(1);

expect((webCryptoRandomValues as any).mock.calls.length).toBe(0);
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
expect((jsRandomValues as any).mock.calls.length).toBe(1);
}
);

it('should use `self` if window is not defined', async () => {
(global as any).self = _self;

await randomValues(1);

expect((supportsWebCrypto as any).mock.calls.length).toBe(1);
expect((supportsWebCrypto as any).mock.calls[0][0]).toBe(_self);
});
});
70 changes: 70 additions & 0 deletions packages/crypto-random-source-browser/__tests__/jsRandomValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {randomValues} from '../lib/jsRandomValues';

jest.mock('@aws/crypto-sjcl-random', () => {
return {
isReady: jest.fn(),
randomWords: jest.fn(),
startCollectors: jest.fn(),
stopCollectors: jest.fn(),
};
});
import {
isReady,
randomWords,
startCollectors,
stopCollectors,
} from '@aws/crypto-sjcl-random';

beforeEach(() => {
jest.resetAllMocks();
});

describe('randomValues', () => {
it('should call the SJCL random source', async () => {
(isReady as any).mockImplementation(() => true);
(randomWords as any).mockImplementation(() => [0]);

expect(await randomValues(3))
.toMatchObject(Uint8Array.from([0, 0, 0]));
});

it(
'should convert a failed random generation into a promise rejection',
async () => {
(isReady as any).mockImplementation(() => true);
(randomWords as any).mockImplementation(() => {
throw new Error('PANIC PANIC')
});

await randomValues(12).then(
() => { throw new Error('The promise should have been rejected'); },
() => { /* promise rejected, just as expected */ }
);
}
);

it(
'should start and stop entropy collection if the source is not ready',
async () => {
jest.useFakeTimers();

(isReady as any).mockImplementationOnce(() => false);
(isReady as any).mockImplementationOnce(() => true);
(randomWords as any).mockImplementation(() => [0]);

const promise = randomValues(3);

expect((isReady as any).mock.calls.length).toBe(1);
expect((startCollectors as any).mock.calls.length).toBe(1);
expect((stopCollectors as any).mock.calls.length).toBe(0);

jest.runAllTimers();

expect((isReady as any).mock.calls.length).toBe(2);
expect((stopCollectors as any).mock.calls.length).toBe(1);

expect(await promise)
.toMatchObject(Uint8Array.from([0, 0, 0]));
}
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {randomValues} from '../lib/webCryptoRandomValues';

beforeEach(() => {
(window as any).crypto = {
getRandomValues(toFill: Uint8Array) {
const view = new DataView(toFill.buffer);
for (let i = 0; i < toFill.byteLength; i++) {
view.setUint8(i, 0x00);
}
}
};
});

describe('randomValues', () => {
it('should call the random source built into most browsers', async () => {
expect(await randomValues(4))
.toMatchObject(Uint8Array.from([0, 0, 0, 0]));
});

it(
'should convert a failed random generation into a promise rejection',
async () => {
window.crypto.getRandomValues = () => {
throw new Error('PANIC PANIC');
};

await randomValues(12).then(
() => { throw new Error('The promise should have been rejected'); },
() => { /* promise rejected, just as expected */ }
);
}
);
});
21 changes: 21 additions & 0 deletions packages/crypto-random-source-browser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {randomValues as ie11RandomValues} from './lib/ie11RandomValues';
import {randomValues as webCryptoRandomValues} from './lib/webCryptoRandomValues';
import {randomValues as sjclRandomValues} from './lib/jsRandomValues';
import {isMsWindow} from '@aws/crypto-ie11-detection';
import {supportsWebCrypto} from '@aws/crypto-supports-webCrypto';
import {locateWindow} from '@aws/util-locate-window';

export {ie11RandomValues, webCryptoRandomValues, sjclRandomValues};

export function randomValues(byteLength: number): Promise<Uint8Array> {
// Find the global scope for this runtime
const globalScope = locateWindow();

if (supportsWebCrypto(globalScope)) {
return webCryptoRandomValues(byteLength);
} else if (isMsWindow(globalScope)) {
return ie11RandomValues(byteLength);
}

return sjclRandomValues(byteLength);
}
15 changes: 15 additions & 0 deletions packages/crypto-random-source-browser/lib/ie11RandomValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {MsWindow} from '@aws/crypto-ie11-detection';
import {randomValues as IRandomValues} from '@aws/types';
import {locateWindow} from '@aws/util-locate-window';

/**
* @implements {IRandomValues}
*/
export function randomValues(byteLength: number): Promise<Uint8Array> {
return new Promise(resolve => {
const randomBytes = new Uint8Array(byteLength);
(locateWindow() as MsWindow).msCrypto.getRandomValues(randomBytes);

resolve(randomBytes);
});
}
30 changes: 30 additions & 0 deletions packages/crypto-random-source-browser/lib/jsRandomValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {fromBits} from '@aws/crypto-sjcl-codecArrayBuffer'
import {
isReady,
randomWords,
startCollectors,
stopCollectors,
} from '@aws/crypto-sjcl-random';
import {randomValues as IRandomValues} from '@aws/types';

/**
* @implements {IRandomValues}
*/
export function randomValues(byteLength: number): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
if (!isReady()) {
startCollectors();

return setTimeout(() => {
randomValues(byteLength)
.then(resolve, reject);
}, 0);
}

const words = randomWords(Math.ceil(byteLength / 4));
stopCollectors();

resolve(new Uint8Array(fromBits(words, 0, 0).slice(0, byteLength)));
});
}

14 changes: 14 additions & 0 deletions packages/crypto-random-source-browser/lib/webCryptoRandomValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {randomValues as IRandomValues} from '@aws/types';
import {locateWindow} from '@aws/util-locate-window';

/**
* @implements {IRandomValues}
*/
export function randomValues(byteLength: number): Promise<Uint8Array> {
return new Promise(resolve => {
const randomBytes = new Uint8Array(byteLength);
locateWindow().crypto.getRandomValues(randomBytes);

resolve(randomBytes);
});
}
32 changes: 32 additions & 0 deletions packages/crypto-random-source-browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@aws/crypto-random-source-browser",
"private": true,
"version": "0.0.1",
"scripts": {
"prepublishOnly": "tsc",
"pretest": "tsc",
"test": "jest"
},
"author": "[email protected]",
"license": "UNLICENSED",
"dependencies": {
"@aws/crypto-ie11-detection": "^0.0.1",
"@aws/crypto-sjcl-random": "^0.0.1",
"@aws/crypto-sjcl-codecArrayBuffer": "^0.0.1",
"@aws/crypto-supports-webCrypto": "^0.0.1",
"@aws/types": "^0.0.1",
"@aws/util-locate-window": "^0.0.1"
},
"devDependencies": {
"@types/jest": "^19.2.2",
"@types/node": "^7.0.12",
"jest": "^19.0.2",
"typescript": "^2.3"
},
"jest": {
"globals": {
"window": true,
"self": true
}
}
}
14 changes: 14 additions & 0 deletions packages/crypto-random-source-browser/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [
"DOM",
"ES5",
"ES2015.Promise"
],
"declaration": true,
"sourceMap": true,
"strict": true
}
}
3 changes: 3 additions & 0 deletions packages/crypto-random-source-node/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.js
*.js.map
*.d.ts
Loading