├── 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 | --------------------------------------------------------------------------------