Skip to content

Commit bb79147

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 8c165bf commit bb79147

File tree

23 files changed

+596
-0
lines changed

23 files changed

+596
-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)).toMatchObject(Uint8Array.from([0, 0, 0, 0]));
17+
});
18+
19+
it("should convert a failed random generation into a promise rejection", async () => {
20+
(window as any).msCrypto.getRandomValues = () => {
21+
throw new Error("PANIC PANIC");
22+
};
23+
24+
await randomValues(12).then(
25+
() => {
26+
throw new Error("The promise should have been rejected");
27+
},
28+
() => {
29+
/* promise rejected, just as expected */
30+
}
31+
);
32+
});
33+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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("should prefer standards-compliant WebCrypto over IE 11 WebCrypto", async () => {
55+
(supportsWebCrypto as any).mockImplementation(() => true);
56+
(isMsWindow as any).mockImplementation(() => true);
57+
58+
await randomValues(1);
59+
60+
expect((webCryptoRandomValues as any).mock.calls.length).toBe(1);
61+
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
62+
expect((jsRandomValues as any).mock.calls.length).toBe(0);
63+
});
64+
65+
it("should fall back on the SJCL", async () => {
66+
(supportsWebCrypto as any).mockImplementation(() => false);
67+
(isMsWindow as any).mockImplementation(() => false);
68+
69+
await randomValues(1);
70+
71+
expect((webCryptoRandomValues as any).mock.calls.length).toBe(0);
72+
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
73+
expect((jsRandomValues as any).mock.calls.length).toBe(1);
74+
});
75+
});
76+
77+
describe("global detection", () => {
78+
const _window = window;
79+
const _self = self;
80+
81+
beforeEach(() => {
82+
delete (global as any).window;
83+
delete (global as any).self;
84+
});
85+
86+
afterAll(() => {
87+
window = _window;
88+
self = _self;
89+
});
90+
91+
it("should fall back to the SJCL if neither window nor self is defined", async () => {
92+
await randomValues(1);
93+
94+
expect((webCryptoRandomValues as any).mock.calls.length).toBe(0);
95+
expect((ie11RandomValues as any).mock.calls.length).toBe(0);
96+
expect((jsRandomValues as any).mock.calls.length).toBe(1);
97+
});
98+
99+
it("should use `self` if window is not defined", async () => {
100+
(global as any).self = _self;
101+
102+
await randomValues(1);
103+
104+
expect((supportsWebCrypto as any).mock.calls.length).toBe(1);
105+
expect((supportsWebCrypto as any).mock.calls[0][0]).toBe(_self);
106+
});
107+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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)).toMatchObject(Uint8Array.from([0, 0, 0]));
28+
});
29+
30+
it("should convert a failed random generation into a promise rejection", async () => {
31+
(isReady as any).mockImplementation(() => true);
32+
(randomWords as any).mockImplementation(() => {
33+
throw new Error("PANIC PANIC");
34+
});
35+
36+
await randomValues(12).then(
37+
() => {
38+
throw new Error("The promise should have been rejected");
39+
},
40+
() => {
41+
/* promise rejected, just as expected */
42+
}
43+
);
44+
});
45+
46+
it("should start and stop entropy collection if the source is not ready", async () => {
47+
jest.useFakeTimers();
48+
49+
(isReady as any).mockImplementationOnce(() => false);
50+
(isReady as any).mockImplementationOnce(() => true);
51+
(randomWords as any).mockImplementation(() => [0]);
52+
53+
const promise = randomValues(3);
54+
55+
expect((isReady as any).mock.calls.length).toBe(1);
56+
expect((startCollectors as any).mock.calls.length).toBe(1);
57+
expect((stopCollectors as any).mock.calls.length).toBe(0);
58+
59+
jest.runAllTimers();
60+
61+
expect((isReady as any).mock.calls.length).toBe(2);
62+
expect((stopCollectors as any).mock.calls.length).toBe(1);
63+
64+
expect(await promise).toMatchObject(Uint8Array.from([0, 0, 0]));
65+
});
66+
});
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)).toMatchObject(Uint8Array.from([0, 0, 0, 0]));
17+
});
18+
19+
it("should convert a failed random generation into a promise rejection", async () => {
20+
window.crypto.getRandomValues = () => {
21+
throw new Error("PANIC PANIC");
22+
};
23+
24+
await randomValues(12).then(
25+
() => {
26+
throw new Error("The promise should have been rejected");
27+
},
28+
() => {
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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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).then(resolve, reject);
20+
}, 0);
21+
}
22+
23+
const words = randomWords(Math.ceil(byteLength / 4));
24+
stopCollectors();
25+
26+
resolve(new Uint8Array(fromBits(words, 0, 0).slice(0, byteLength)));
27+
});
28+
}
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)