├── LICENSE
├── README.md
├── index.html
├── math-random-polyfill.js
├── test-setup.js
└── test.js
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 David Anson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # math-random-polyfill
2 |
3 | > A browser-based polyfill for JavaScript's `Math.random()` that tries to make it more random
4 |
5 | [The MDN documentation for Math.random()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) explicitly warns that return values should not be used for cryptographic purposes.
6 | Failing to heed that advice can lead to problems, such as those documented in the article [TIFU by using Math.random()](https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.lf1mchyk9).
7 | However, there are scenarios - especially involving legacy code - that don't lend themselves to easily replacing `Math.random()` with [`crypto.getRandomValues()`](https://developer.mozilla.org/en-US/docs/Web/API/RandomSource/getRandomValues).
8 | For those scenarios, `math-random-polyfill.js` attempts to provide a more random implementation of `Math.random()` to mitigate some of its disadvantages.
9 |
10 | > **Important**: If at all possible, use `crypto.getRandomValues()` directly.
11 | > `math-random-polyfill.js` tries to improve the security of legacy scripts, but is not a substitute for properly implemented cryptography.
12 |
13 | ## Usage
14 |
15 | Add `math-random-polyfill.js` to your project and reference it from a web page just like any other script:
16 |
17 | ```html
18 |
19 | ```
20 |
21 | Do this as early as possible for the broadest impact - some scripts capture `Math.random()` during initial load and won't benefit if loaded before `math-random-polyfill.js`.
22 |
23 | ## Implementation
24 |
25 | `math-random-polyfill.js` works by intercepting calls to `Math.random()` and returning the same `0 <= value < 1` based on random data provided by `crypto.getRandomValues()`.
26 | Values returned by `Math.random()` should be completely unpredictable and evenly distributed - both of which are true of the random bits returned by `crypto.getRandomValues()`.
27 | The [polyfill](https://en.wikipedia.org/wiki/Polyfill) maps those values into [floating point numbers](https://en.wikipedia.org/wiki/Floating_point) by using the random bits to create integers distributed evenly across the range `0 <= value < Number.MAX_SAFE_INTEGER` then dividing by `Number.MAX_SAFE_INTEGER + 1`.
28 | This maintains the greatest amount of randomness and precision during the transfer from the integer domain to the floating point domain.
29 |
30 | > An alternate approach that uses random bits to create floating point numbers directly suffers from the problem that the binary representation of those numbers is non-linear and therefore the resulting values would not be uniformly distributed.
31 |
32 | The code and tests for `math-random-polyfill.js` are implemented in [ECMAScript 5](https://en.wikipedia.org/wiki/ECMAScript#5th_Edition) and should work on all browsers that implement `crypto.getRandomValues()` and [`Uint32Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint32Array).
33 |
34 | ## Testing
35 |
36 | Tests are known to pass on the following browsers:
37 |
38 | - Chrome
39 | - Edge
40 | - Firefox
41 | - Internet Explorer 11
42 | - Safari
43 |
44 | They may pass on other browsers as well; [run the `math-random-polyfill.js` test suite](https://davidanson.github.io/math-random-polyfill/) to check.
45 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Tests for math-random-polyfill.js
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/math-random-polyfill.js:
--------------------------------------------------------------------------------
1 | // math-random-polyfill.js
2 | // https://github.com/DavidAnson/math-random-polyfill
3 | // 2016-12-03
4 |
5 | (function iife () {
6 | "use strict";
7 | // Feature detection
8 | var crypto = window.crypto || window.msCrypto;
9 | if (window.Uint32Array && crypto && crypto.getRandomValues) {
10 | // Capture functions and values
11 | var Math_random = Math.random.bind(Math);
12 | var crypto_getRandomValues = crypto.getRandomValues.bind(crypto);
13 | var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;
14 | var highShift = Math.pow(2, 32);
15 | var highMask = Math.pow(2, 53 - 32) - 1;
16 | // Polyfill Math.random
17 | Math.random = function math_random_polyfill () {
18 | try {
19 | // Get random bits for numerator
20 | var array = new Uint32Array(2);
21 | crypto_getRandomValues(array);
22 | var numerator = ((array[0] & highMask) * highShift) + array[1];
23 | // Divide by maximum-value denominator
24 | var denominator = MAX_SAFE_INTEGER + 1;
25 | return numerator / denominator;
26 | } catch (ex) {
27 | // Exception in crypto.getRandomValues, fall back to Math.random
28 | return Math_random();
29 | }
30 | };
31 | }
32 | }());
33 |
--------------------------------------------------------------------------------
/test-setup.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | window.Math_random_mock = null;
4 | window.crypto_getRandomValues_mock = null;
5 |
6 | (function () {
7 | var Math_random = Math.random.bind(Math);
8 | Math.random = function () {
9 | return (Math_random_mock || Math_random)();
10 | };
11 |
12 | var crypto = window.crypto || window.msCrypto;
13 | if (crypto && crypto.getRandomValues) {
14 | var crypto_getRandomValues = crypto.getRandomValues.bind(crypto);
15 | crypto.getRandomValues = function (arr) {
16 | (crypto_getRandomValues_mock || crypto_getRandomValues)(arr);
17 | };
18 | }
19 | }());
20 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | window.number_iterations = 1000;
4 | window.number_buckets = 100;
5 |
6 | QUnit.testDone(function () {
7 | Math_random_mock = null;
8 | crypto_getRandomValues_mock = null;
9 | });
10 |
11 | QUnit.test("Dependencies are present", function (assert) {
12 | assert.expect(2);
13 | assert.ok(window.Uint32Array, "Uint32Array is present");
14 | var crypto = window.crypto || window.msCrypto;
15 | assert.ok(crypto && crypto.getRandomValues, "crypto.getRandomValues is present");
16 | });
17 |
18 | QUnit.test("Minimum value from crypto.getRandomValues", function (assert) {
19 | assert.expect(2);
20 | Math_random_mock = function () {
21 | assert.ok(false, "Should not call Math.random mock");
22 | };
23 | crypto_getRandomValues_mock = function (arr) {
24 | arr[0] = 0;
25 | arr[1] = 0;
26 | assert.ok(true, "Called crypto.getRandomValues mock");
27 | };
28 | assert.strictEqual(Math.random(), 0.0, "Minimum value is 0.0");
29 | });
30 |
31 | QUnit.test("Maximum value from crypto.getRandomValues", function (assert) {
32 | assert.expect(2);
33 | Math_random_mock = function () {
34 | assert.ok(false, "Should not call Math.random mock");
35 | };
36 | crypto_getRandomValues_mock = function (arr) {
37 | arr[0] = Math.pow(2, 53 - 32) - 1;
38 | arr[1] = Math.pow(2, 32) - 1;
39 | assert.ok(true, "Called crypto.getRandomValues mock");
40 | };
41 | assert.strictEqual(Math.random(), 0.9999999999999999, "Maximum value is just under 1.0");
42 | });
43 |
44 | QUnit.test("Middle value from crypto.getRandomValues", function (assert) {
45 | assert.expect(2);
46 | Math_random_mock = function () {
47 | assert.ok(false, "Should not call Math.random mock");
48 | };
49 | crypto_getRandomValues_mock = function (arr) {
50 | arr[0] = Math.pow(2, 53 - 32 - 1);
51 | arr[1] = 0;
52 | assert.ok(true, "Called crypto.getRandomValues mock");
53 | };
54 | assert.strictEqual(Math.random(), 0.5, "Middle value is 0.5");
55 | });
56 |
57 | QUnit.test("Exception from crypto.getRandomValues", function (assert) {
58 | assert.expect(3);
59 | Math_random_mock = function () {
60 | assert.ok(true, "Called Math.random mock");
61 | return 0.123;
62 | };
63 | crypto_getRandomValues_mock = function () {
64 | assert.ok(true, "Called crypto.getRandomValues mock");
65 | throw new Error();
66 | };
67 | assert.strictEqual(Math.random(), 0.123, "Fall-back value is 0.123");
68 | });
69 |
70 | QUnit.test("Random values are distributed evenly across buckets", function (assert) {
71 | assert.expect(1);
72 | var buckets = new Array(number_buckets);
73 | for (var i = 0; i < number_iterations; i++) {
74 | var random = Math.random();
75 | var bucket = Math.floor(random * number_buckets);
76 | buckets[bucket] = 1;
77 | }
78 | var sum = buckets.reduce(function (previous, current) {
79 | return previous + current;
80 | }, 0);
81 | assert.strictEqual(sum, number_buckets, "All buckets filled");
82 | });
83 |
84 | QUnit.test("Random values are at least 0 and less than 1", function (assert) {
85 | assert.expect(2 * number_iterations);
86 | for (var i = 0; i < number_iterations; i++) {
87 | var random = Math.random();
88 | assert.ok(0 <= random, "Value is at least 0");
89 | assert.ok(random < 1, "Value is less than 1");
90 | }
91 | });
92 |
93 | QUnit.test("Minimum random values are unique", function (assert) {
94 | assert.expect(number_iterations);
95 | var value = 0;
96 | crypto_getRandomValues_mock = function (arr) {
97 | arr[0] = 0;
98 | arr[1] = value;
99 | value++;
100 | };
101 | var previous = -1;
102 | for (var i = 0; i < number_iterations; i++) {
103 | var random = Math.random();
104 | assert.ok(previous < random, "Value is greater than previous value");
105 | previous = random;
106 | }
107 | });
108 |
109 | QUnit.test("Maximum random values are unique", function (assert) {
110 | assert.expect(number_iterations);
111 | var value = Math.pow(2, 32) - 1;
112 | crypto_getRandomValues_mock = function (arr) {
113 | arr[0] = Math.pow(2, 53 - 32) - 1;
114 | arr[1] = value;
115 | value--;
116 | };
117 | var previous = 1;
118 | for (var i = 0; i < number_iterations; i++) {
119 | var random = Math.random();
120 | assert.ok(previous > random, "Value is less than previous value");
121 | previous = random;
122 | }
123 | });
124 |
--------------------------------------------------------------------------------