Skip to content

Commit bb8033a

Browse files
authored
Add random source modules for node, browsers, and cross-platform usage (#19)
* Add random source modules for node, browsers, and cross-platform usage * Use util-locate-window instead of directly dereferencing "self"
1 parent 0e19925 commit bb8033a

File tree

23 files changed

+602
-0
lines changed

23 files changed

+602
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.js
2+
*.js.map
3+
*.d.ts
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {randomValues} from '../lib/ie11RandomValues';
2+
3+
beforeEach(() => {
4+
(window as any).msCrypto = {
5+
getRandomValues(toFill: Uint8Array) {
6+
const view = new DataView(toFill.buffer);
7+
for (let i = 0; i < toFill.byteLength; i++) {
8+
view.setUint8(i, 0x00);
9+
}
10+
}
11+
};
12+
});
13+
14+
describe('randomValues', () => {
15+
it('should call the random source built into IE 11', async () => {
16+
expect(await randomValues(4))
17+
.toMatchObject(Uint8Array.from([0, 0, 0, 0]));
18+
});
19+
20+
it(
21+
'should convert a failed random generation into a promise rejection',
22+
async () => {
23+
(window as any).msCrypto.getRandomValues = () => {
24+
throw new Error('PANIC PANIC');
25+
};
26+
27+
await randomValues(12).then(
28+
() => { throw new Error('The promise should have been rejected'); },
29+
() => { /* promise rejected, just as expected */ }
30+
);
31+
}
32+
);
33+
});
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {randomValues} from '../';
2+
3+
jest.mock('@aws/crypto-ie11-detection', () => {
4+
return { isMsWindow: jest.fn() };
5+
});
6+
import {isMsWindow} from '@aws/crypto-ie11-detection';
7+
jest.mock('@aws/crypto-supports-webCrypto', () => {
8+
return { supportsWebCrypto: jest.fn() };
9+
});
10+
import {supportsWebCrypto} from '@aws/crypto-supports-webCrypto';
11+
12+
jest.mock('../lib/ie11RandomValues', () => {
13+
return { randomValues: jest.fn() };
14+
});
15+
import {randomValues as ie11RandomValues} from '../lib/ie11RandomValues';
16+
17+
jest.mock('../lib/jsRandomValues', () => {
18+
return { randomValues: jest.fn() };
19+
});
20+
import {randomValues as jsRandomValues} from '../lib/jsRandomValues';
21+
22+
jest.mock('../lib/webCryptoRandomValues', () => {
23+
return { randomValues: jest.fn() };
24+
});
25+
import {randomValues as webCryptoRandomValues} from '../lib/webCryptoRandomValues';
26+
27+
beforeEach(() => {
28+
(isMsWindow as any).mockReset();
29+
(supportsWebCrypto as any).mockReset();
30+
(ie11RandomValues as any).mockReset();
31+
(jsRandomValues as any).mockReset();
32+
(webCryptoRandomValues as any).mockReset();
33+
});
34+
35+
describe('implementation selection', () => {
36+
it('should use WebCrypto when available', async () => {
37+
(supportsWebCrypto as any).mockImplementation(() => true);
38+
39+
await randomValues(1);
40+
41+
expect((webCryptoRandomValues as any).mock.calls.length).toBe(1);
42+
expect((jsRandomValues as any).mock.calls.length).toBe(0);
43+
});
44+
45+
it('should use IE 11 WebCrypto when available', async () => {
46+
(isMsWindow as any).mockImplementation(() => true);
47+
48+
await randomValues(1);
49+
50+
expect((ie11RandomValues as any).mock.calls.length).toBe(1);
51+
expect((jsRandomValues as any).mock.calls.length).toBe(0);
52+
});
53+
54+
it(
55+
'should prefer standards-compliant WebCrypto over IE 11 WebCrypto',
56+
async () => {
57+
(supportsWebCrypto as any).mockImplementation(() => true);
58+
(isMsWindow as any).mockImplementation(() => true);
59+
60+
await randomValues(1);
61+
62+
expect((webCryptoRandomValues as any).mock.calls.length).toBe(1);
63+
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
64+
expect((jsRandomValues as any).mock.calls.length).toBe(0);
65+
}
66+
);
67+
68+
it('should fall back on the SJCL', async () => {
69+
(supportsWebCrypto as any).mockImplementation(() => false);
70+
(isMsWindow as any).mockImplementation(() => false);
71+
72+
await randomValues(1);
73+
74+
expect((webCryptoRandomValues as any).mock.calls.length).toBe(0);
75+
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
76+
expect((jsRandomValues as any).mock.calls.length).toBe(1);
77+
});
78+
});
79+
80+
describe('global detection', () => {
81+
const _window = window;
82+
const _self = self;
83+
84+
beforeEach(() => {
85+
delete (global as any).window;
86+
delete (global as any).self;
87+
});
88+
89+
afterAll(() => {
90+
window = _window;
91+
self = _self;
92+
});
93+
94+
it(
95+
'should fall back to the SJCL if neither window nor self is defined',
96+
async () => {
97+
await randomValues(1);
98+
99+
expect((webCryptoRandomValues as any).mock.calls.length).toBe(0);
100+
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
101+
expect((jsRandomValues as any).mock.calls.length).toBe(1);
102+
}
103+
);
104+
105+
it('should use `self` if window is not defined', async () => {
106+
(global as any).self = _self;
107+
108+
await randomValues(1);
109+
110+
expect((supportsWebCrypto as any).mock.calls.length).toBe(1);
111+
expect((supportsWebCrypto as any).mock.calls[0][0]).toBe(_self);
112+
});
113+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {randomValues} from '../lib/jsRandomValues';
2+
3+
jest.mock('@aws/crypto-sjcl-random', () => {
4+
return {
5+
isReady: jest.fn(),
6+
randomWords: jest.fn(),
7+
startCollectors: jest.fn(),
8+
stopCollectors: jest.fn(),
9+
};
10+
});
11+
import {
12+
isReady,
13+
randomWords,
14+
startCollectors,
15+
stopCollectors,
16+
} from '@aws/crypto-sjcl-random';
17+
18+
beforeEach(() => {
19+
jest.resetAllMocks();
20+
});
21+
22+
describe('randomValues', () => {
23+
it('should call the SJCL random source', async () => {
24+
(isReady as any).mockImplementation(() => true);
25+
(randomWords as any).mockImplementation(() => [0]);
26+
27+
expect(await randomValues(3))
28+
.toMatchObject(Uint8Array.from([0, 0, 0]));
29+
});
30+
31+
it(
32+
'should convert a failed random generation into a promise rejection',
33+
async () => {
34+
(isReady as any).mockImplementation(() => true);
35+
(randomWords as any).mockImplementation(() => {
36+
throw new Error('PANIC PANIC')
37+
});
38+
39+
await randomValues(12).then(
40+
() => { throw new Error('The promise should have been rejected'); },
41+
() => { /* promise rejected, just as expected */ }
42+
);
43+
}
44+
);
45+
46+
it(
47+
'should start and stop entropy collection if the source is not ready',
48+
async () => {
49+
jest.useFakeTimers();
50+
51+
(isReady as any).mockImplementationOnce(() => false);
52+
(isReady as any).mockImplementationOnce(() => true);
53+
(randomWords as any).mockImplementation(() => [0]);
54+
55+
const promise = randomValues(3);
56+
57+
expect((isReady as any).mock.calls.length).toBe(1);
58+
expect((startCollectors as any).mock.calls.length).toBe(1);
59+
expect((stopCollectors as any).mock.calls.length).toBe(0);
60+
61+
jest.runAllTimers();
62+
63+
expect((isReady as any).mock.calls.length).toBe(2);
64+
expect((stopCollectors as any).mock.calls.length).toBe(1);
65+
66+
expect(await promise)
67+
.toMatchObject(Uint8Array.from([0, 0, 0]));
68+
}
69+
);
70+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {randomValues} from '../lib/webCryptoRandomValues';
2+
3+
beforeEach(() => {
4+
(window as any).crypto = {
5+
getRandomValues(toFill: Uint8Array) {
6+
const view = new DataView(toFill.buffer);
7+
for (let i = 0; i < toFill.byteLength; i++) {
8+
view.setUint8(i, 0x00);
9+
}
10+
}
11+
};
12+
});
13+
14+
describe('randomValues', () => {
15+
it('should call the random source built into most browsers', async () => {
16+
expect(await randomValues(4))
17+
.toMatchObject(Uint8Array.from([0, 0, 0, 0]));
18+
});
19+
20+
it(
21+
'should convert a failed random generation into a promise rejection',
22+
async () => {
23+
window.crypto.getRandomValues = () => {
24+
throw new Error('PANIC PANIC');
25+
};
26+
27+
await randomValues(12).then(
28+
() => { throw new Error('The promise should have been rejected'); },
29+
() => { /* promise rejected, just as expected */ }
30+
);
31+
}
32+
);
33+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {randomValues as ie11RandomValues} from './lib/ie11RandomValues';
2+
import {randomValues as webCryptoRandomValues} from './lib/webCryptoRandomValues';
3+
import {randomValues as sjclRandomValues} from './lib/jsRandomValues';
4+
import {isMsWindow} from '@aws/crypto-ie11-detection';
5+
import {supportsWebCrypto} from '@aws/crypto-supports-webCrypto';
6+
import {locateWindow} from '@aws/util-locate-window';
7+
8+
export {ie11RandomValues, webCryptoRandomValues, sjclRandomValues};
9+
10+
export function randomValues(byteLength: number): Promise<Uint8Array> {
11+
// Find the global scope for this runtime
12+
const globalScope = locateWindow();
13+
14+
if (supportsWebCrypto(globalScope)) {
15+
return webCryptoRandomValues(byteLength);
16+
} else if (isMsWindow(globalScope)) {
17+
return ie11RandomValues(byteLength);
18+
}
19+
20+
return sjclRandomValues(byteLength);
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {MsWindow} from '@aws/crypto-ie11-detection';
2+
import {randomValues as IRandomValues} from '@aws/types';
3+
import {locateWindow} from '@aws/util-locate-window';
4+
5+
/**
6+
* @implements {IRandomValues}
7+
*/
8+
export function randomValues(byteLength: number): Promise<Uint8Array> {
9+
return new Promise(resolve => {
10+
const randomBytes = new Uint8Array(byteLength);
11+
(locateWindow() as MsWindow).msCrypto.getRandomValues(randomBytes);
12+
13+
resolve(randomBytes);
14+
});
15+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {fromBits} from '@aws/crypto-sjcl-codecArrayBuffer'
2+
import {
3+
isReady,
4+
randomWords,
5+
startCollectors,
6+
stopCollectors,
7+
} from '@aws/crypto-sjcl-random';
8+
import {randomValues as IRandomValues} from '@aws/types';
9+
10+
/**
11+
* @implements {IRandomValues}
12+
*/
13+
export function randomValues(byteLength: number): Promise<Uint8Array> {
14+
return new Promise((resolve, reject) => {
15+
if (!isReady()) {
16+
startCollectors();
17+
18+
return setTimeout(() => {
19+
randomValues(byteLength)
20+
.then(resolve, reject);
21+
}, 0);
22+
}
23+
24+
const words = randomWords(Math.ceil(byteLength / 4));
25+
stopCollectors();
26+
27+
resolve(new Uint8Array(fromBits(words, 0, 0).slice(0, byteLength)));
28+
});
29+
}
30+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {randomValues as IRandomValues} from '@aws/types';
2+
import {locateWindow} from '@aws/util-locate-window';
3+
4+
/**
5+
* @implements {IRandomValues}
6+
*/
7+
export function randomValues(byteLength: number): Promise<Uint8Array> {
8+
return new Promise(resolve => {
9+
const randomBytes = new Uint8Array(byteLength);
10+
locateWindow().crypto.getRandomValues(randomBytes);
11+
12+
resolve(randomBytes);
13+
});
14+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@aws/crypto-random-source-browser",
3+
"private": true,
4+
"version": "0.0.1",
5+
"scripts": {
6+
"prepublishOnly": "tsc",
7+
"pretest": "tsc",
8+
"test": "jest"
9+
},
10+
"author": "[email protected]",
11+
"license": "UNLICENSED",
12+
"dependencies": {
13+
"@aws/crypto-ie11-detection": "^0.0.1",
14+
"@aws/crypto-sjcl-random": "^0.0.1",
15+
"@aws/crypto-sjcl-codecArrayBuffer": "^0.0.1",
16+
"@aws/crypto-supports-webCrypto": "^0.0.1",
17+
"@aws/types": "^0.0.1",
18+
"@aws/util-locate-window": "^0.0.1"
19+
},
20+
"devDependencies": {
21+
"@types/jest": "^19.2.2",
22+
"@types/node": "^7.0.12",
23+
"jest": "^19.0.2",
24+
"typescript": "^2.3"
25+
},
26+
"jest": {
27+
"globals": {
28+
"window": true,
29+
"self": true
30+
}
31+
}
32+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es5",
4+
"module": "commonjs",
5+
"lib": [
6+
"DOM",
7+
"ES5",
8+
"ES2015.Promise"
9+
],
10+
"declaration": true,
11+
"sourceMap": true,
12+
"strict": true
13+
}
14+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.js
2+
*.js.map
3+
*.d.ts

0 commit comments

Comments
 (0)