Secure Random Javascript Password

Hanno Böck

  function makePasswd() {
    var passwd = '';
    var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    for (i=0; i<15; i++) {
      var c = Math.floor(Math.random()*chars.length + 1);
      passwd += chars.charAt(c)
    }
    return passwd;
  }
  function makePasswd() {
    var passwd = '';
    var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    for (i=0; i<15; i++) {
      var c = Math.floor(Math.random()*chars.length + 1);
      passwd += chars.charAt(c)
    }
    return passwd;
  }

This does not look good

  function makePasswd() {
    var passwd = '';
    var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    for (i=0; i<15; i++) {
      var c = Math.floor(Math.random()*chars.length + 1);
      passwd += chars.charAt(c)
    }
    return passwd;
  }

I want to have the same functionality, but secure

Password Requirements

  • 15 characters
  • Only ASCII letters and numbers [A-Za-z0-9]
  • Generated from secure random number generator
  • Uniform distribution

Ask Google?

Ask Stackoverflow?

Ask ChatGPT?

Plenty of answers

Almost all wrong

Some are wrong in interesting ways

Screenshot blogpost dev.to

https://dev.to/code_mystery/random-password-generator-using-javascript-6a

 var chars = "0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 var passwordLength = 12;
 var password = "";
 for (var i = 0; i <= passwordLength; i++) {
   var randomNumber = Math.floor(Math.random() * chars.length);
   password += chars.substring(randomNumber, randomNumber +1);
  }

Math.random()

Math.random() does not provide cryptographically secure random numbers. Do not use them for anything related to security. Use the Web Crypto API instead, and more precisely the window.crypto.getRandomValues() method.

Source: MDN

tl;dr

Math.random() is not secure, we should use window.crypto.getRandomValues()

This can cause real problems

See e.g. https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d

More general

Use a random number generation function that uses the operating system's random number generator

Google:

random password without Math.Random()

Blogpost Where is the bug

https://whereisthebug.com/never-use-math-random-for-passwords/

function generatePassword(length = 16)
{
    let generatedPassword = "";

    const validChars = "0123456789" +
        "abcdefghijklmnopqrstuvwxyz" +
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        ",.-{}+!\"#$%/()=?";

    for (let i = 0; i < length; i++) {
        let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0];
        randomNumber = randomNumber / 0x100000000;
        randomNumber = Math.floor(randomNumber * validChars.length);

        generatedPassword += validChars[randomNumber];
    }

    return generatedPassword;
}
function generatePassword(length = 16)
{
    let generatedPassword = "";

    const validChars = "0123456789" +
        "abcdefghijklmnopqrstuvwxyz" +
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        ",.-{}+!\"#$%/()=?";

    for (let i = 0; i < length; i++) {
        let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0];
        randomNumber = randomNumber / 0x100000000;
        randomNumber = Math.floor(randomNumber * validChars.length);

        generatedPassword += validChars[randomNumber];
    }

    return generatedPassword;
}

That's wrong in an interesting way

        randomNumber = randomNumber / 0x100000000;
        randomNumber = Math.floor(randomNumber * validChars.length);

This converts it to a floating point number

Don't mix Floats and Security/Cryptography

  • Introduces rounding errors (implementation-dependent, difficult to analyze)
  • The way computers store floats makes it difficult to have a uniform distribution

Here's a reasonably good explanation: https://crypto.stackexchange.com/a/31659

Another example with crypto.getRandomValues(), this time from Stack Overflow

var generatePassword = (
  length = 20,
  characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$'
) =>
  Array.from(crypto.getRandomValues(new Uint32Array(length)))
    .map((x) => characters[x % characters.length])
    .join('')

https://stackoverflow.com/a/51540480

var generatePassword = (
  length = 20,
  characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$'
) =>
  Array.from(crypto.getRandomValues(new Uint32Array(length)))
    .map((x) => characters[x % characters.length])
    .join('')

Also wrong in interesting ways: Modulo Bias (small)

Simple Example for Modulo Bias

  • We generate a random byte r (256 possible values: 0-255)
  • We have 62 possible characters (a-z, A-Z, 0-9)
  • We take index r modulo 62 (r%62)
  • 62 is not a multiple of 256
  • r values for a: 0, 62, 124, 186, 248 (5 possibilities)
  • r values for A: 26, 88, 150, 212 (4 possibilities)
  • Not uniform!

It is not hard to get it right!

Three things

  • Use a secure random number generation function
  • No floats
  • No modulo bias

Get byte from secure random number generator

randval = window.crypto.getRandomValues(new Uint8Array(1))[0];

Rejection sampling to prevent Modulo Bias

const pwchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const limit = 256 - (256 % pwchars.length);
[...]
do {
  randval = window.crypto.getRandomValues(new Uint8Array(1))[0];
} while (randval >= limit);

Putting it all together

function simplesecpw() {
  const pwlen = 15;
  const pwchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  const limit = 256 - (256 % pwchars.length);

  let passwd = "";
  let randval;
  for (let i = 0; i < pwlen; i++) {
    do {
      randval = window.crypto.getRandomValues(new Uint8Array(1))[0];
    } while (randval >= limit);
    passwd += pwchars[randval % pwchars.length];
  }
  return passwd;
}

Code is Open Source under a very permissive license (0BSD):

https://github.com/hannob/secpw/

Demo:

https://password.hboeck.de/