Don't use Math.random()
Despite what the accepted answer on Stack Overflow tells you, using Math.random for generating passwords is always a bad idea.
- By Shivam
- ·
- Security
- Open Source
- JavaScript
Once in a while, you'll find the need to generate some secrets for your users on the browser. It might seem tempting to write a one-liner like Math.random().toString(36).slice(2)
and call it a day. The generated strings might seem random but are not cryptographically secure. I'm sure you are not surprised.
In most browsers, Math.random()
is implemented using a pseudo-random number generator (PRNG). This means that the random number is derived from an internal state, mangled by a deterministic algorithm for every new random number. It seems random to the user because the algorithm is tuned in such a way that it appears so. However, if you know the generator's internal state, you know all the future numbers generated by it.
About Math.random
As the ECMAScript specifications don't define the algorithm for a Math.random
implementation, developers building browsers can choose the implementation they see fit. Ideally, the algorithm should use as little memory as possible and be quick to perform while having a significant period length.
The period length of a PRNG is the number of output bits after which the algorithm begins to repeat itself.
Most modern browsers today use the xorshift128+
. (Read why Chrome V8 switched to this algorithm). It is one of the fastest non-cryptographically-secure random number generators. You can read more about the implementation of Math.random() in this article.
These algorithms, including the xorshift
can be broken — here is an article about it. The internet has seen many instances where older implementations have been susceptible to simple prediction attacks, the most popular one I know from my college gaming days is from CSGOJackpot. Here's more to read on that. More such stories such as this and this tell us why using Math.random() is not a great idea. While these hacks may not be exploitable today, it does surface the weakness of PRNGs to be used in this context. A deterministic algorithm can and will be broken.
The Web Cryptography API has got you covered
In 2017, World Wide Web Consortium (W3C) presented the Web Cryptography API. It provides a set of cryptographic primitives that can perform basic operations such as hashing, signature generation and verification, and encryption and decryption. Additionally, it describes an API for applications to generate and manage the keying material necessary to perform these operations. Read more on MDN.
All popular browsers provide an implementation of the Web Crypto API to JavaScript applications through the semi-global crypto
object. For generating secrets, the getRandomValues
method is all we need. The method is widely supported and generates cryptographically secure random numbers. It takes in a typedArray
as an input and returns the same array, with its contents replaced with the newly generated random numbers.
crypto.getRandomValues(new Uint8Array(10))
// Uint8Array(10) [137, 32, 62, 244, 157, 240, 103, 42, 47, 57]
Implementations don't use a truly random number generator to guarantee enough performance. Instead, they use a pseudo-random number generator seeded with a value with enough entropy. This ensures there's a good balance between performance and the quality of the randomness generated. Since the seed value is random, the output is cryptographically secure.
While the methods and primitives provided by this API are inherently secure, it's easy to use them wrong. While using it, ensure your work is reviewed thoroughly by someone knowledgeable in this subject.
Using the API to generate secrets
Step 1: Generating the seed
The seed is a Uint8 Array that is filled with random numbers using getRandomValues
. Uint8Array
generates an array of 8-bit unsigned integers.
generateSeed() {
return window.crypto.getRandomValues(new Uint8Array(256));
}
Step 2: Looping over the seed
We loop over the generated seed until the character length is satisfied. If we've exhausted the seed before that, the seed is generated again. We use String.fromCharCode
to generate a character inside the loop. If it is valid, it is appended to the secret string returned at the end of the function.
generate(length = 32): string {
let secret = "";
let randomSeed;
while (secret.length < length) {
randomSeed = generateSeed()
for (let ii = 0; ii < randomSeed.length; ii++) {
const char = String.fromCharCode(randomSeed[ii]);
// Append the character to secret if it is valid
if (validateCharacter(char)) {
secret += char;
}
if (secret.length === length) {
break;
}
}
}
return secret;
}
The generated character will always belong to the ASCII set. The validate function simply checks if it belongs to the ASCII printable characters, i.e., letters, digits, punctuation marks, and a few miscellaneous symbols.
Introducing Shifty
Sometimes the browser may not support the web crypto API. In such cases, developers have no other choice but to fall back on Math.random
. Recently, we published a library that takes care of this in a tiny package. It's called Shifty.
Shifty is a tiny zero-dependency secrets generator built for the web using TypeScript. Shifty is made for the browser and won't work with Node. You can use the built-in crypto module instead.
It provides a straightforward API to generate a secret of any length. To start using shifty, just install it using the package manager of your choice.
yarn add @deepsource/shifty
Using Shifty is quite straightforward. Just initialize the Shifty
class with the defaults and use the generate
method to generate secrets of any length. Shifty falls back to using Math.random
if the crypto
object is not present on window
. It shows a warning on the developer console when using the fallback method.
import Shifty from '@deepsource/shifty'
const shifty = new Shifty((harden = true), (defaultLength = 16))
shifty.generate((length = 12)) // G8qZt7PEha^s
Shifty is just a single TypeScript file. You can peek into the source code here. Give it a star if you like it!