DEV Community

Cover image for Public Solving: Generating secure password
Chris Bongers
Chris Bongers

Posted on • Originally published at daily-dev-tips.com

Public Solving: Generating secure password

Santa's head elf is one of those old-school dudes who creates passwords from the top of his head instead of using a password manager.

The board of elves has asked us to create a password generator to help the head elf come up with unique and secure passwords.

And they came to the right place!

You can find the complete puzzle here.

Thinking about the problem

Before we can dive into the solution, let's see what we have to work with.

There are two parameters the function should take:

  1. Length: The length of the password
  2. Options: Certain options the password should include, see below:

The options are as follows:

  • lowercase: Lowercase letters (a-z)
  • uppercase: Uppercase letters (A-Z)
  • numbers: Numbers (0-9)
  • specialCharacters: Special characters (only !@#$%^&*())

Knowing this, we should be able to help the elves.

Two side notes are essential and will help us:

  • We should throw an error if no options are passed
  • When the length of the option is longer than the length we should also throw an error

Creating a JavaScript password generator

Alright, let's get right into it.

The first thing I did, check the two errors we should throw.
Since the options are an object, and we want to check the length, I've converted it with Object.keys.
This will convert it into an array.

const optionKeys = Object.keys(options);
if (!optionKeys.length) throw Error('NOT_ENOUGH_OPTIONS');
if (length < optionKeys.length) throw Error('PASSWORD_TOO_SHORT');
Enter fullscreen mode Exit fullscreen mode

That will make sure the errors are thrown when needed.

Then I've decided to create a new object with the option values.

const optionValues = {
  lowercase: 'abcdefghijklmnopqrstuvwxyz',
  numbers: '0123456789',
  specialCharacters: '!@#$%^&*()',
  get uppercase() {
    return this.lowercase.toUpperCase();
  },
};
Enter fullscreen mode Exit fullscreen mode

You can see all the alphabetic characters defined in the lowercase property, all numbers, and the special characters.
For the uppercase version, I've decided to use a function to leverage our existing input.

Note: The keys here match the option keys we should support.

Since we want to mix a random password with the options, I want to loop over each option and get a random number from that option.

We can check the main password length with a basic while loop like so.

let password = '';
while (password.length < length) {
    // password += 'SOMETHING';
}
Enter fullscreen mode Exit fullscreen mode

This will loop until the length of the password is long enough.

As mentioned above, I want to use an equal number of options for each password.
So I've decided to use a for...of loop. I've chosen this particular loop because we can break out of it.

We need to break out of it because we could push too many letters.

For example, we want to generate a 3 character password with 2 options.
The while loop will fire 2 times, and the options will loop 2 times as well, meaning we get a 4 character password.

while (password.length < length) {
    for (let option of optionKeys) {
      if (password.length >= length) break;
      // Add a character
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, I break the loop if we hit the length inside the for loop.

Now we just need to grab a random character for the current looped option and add it to the password.

password += optionValues[option][Math.floor(Math.random() * optionValues[option].length)];
Enter fullscreen mode Exit fullscreen mode

Some things to note:

  • optionValues[option] refers to our option value object, and picks the current option
  • Math.floor(Math.random() * optionValues[option].length) picks a random item from the current option array

With this in place we finished our function, so it looks like this:

export const generatePassword = (length, options = {}) => {
  const optionKeys = Object.keys(options);
  if (!optionKeys.length) throw Error('NOT_ENOUGH_OPTIONS');
  if (length < optionKeys.length) throw Error('PASSWORD_TOO_SHORT');
  let password = '';
  while (password.length < length) {
    for (let option of optionKeys) {
      if (password.length >= length) break;
      password += optionValues[option][Math.floor(Math.random() * optionValues[option].length)];
    }
  }
  return password;
};
Enter fullscreen mode Exit fullscreen mode

One last thing, the test should turn green.

Passing all coding tests

And yes, we did it!

I'm always looking forward to hearing what you would have done differently and why.

Thank you for reading, and let's connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

Oldest comments (4)

Collapse
 
lexlohr profile image
Alex Lohr

First of all: don't jeopardize the security of Santa and his elves by using Math.random() to generate passwords. Use crypto.getRandomValues(typedArray) instead. Also, do not linearly cycle the options, as this reduces the randomness again; instead just make sure that each option is used once and afterwards, select the option randomly. Then, there's another small issue in your first check, as options could also contain { numbers: false }.

const optionValues = {
  lowercase: 'abcdefghijklmnopqrstuvwxyz',
  uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
  numbers: '0123456789',
  specialCharacters: '!@#$%^&*()'
}

export const generatePassword = (length, options) => {
  const selectedOptionsCount =
    Object.values(options).filter(Boolean).length
  if (length < selectedOptionsCount) {
      throw new Error(
        'length is too short to include selected options'
      )
  } else if (selectedOptionsCount === 0) {
      throw new Error('please select at least one option')
  }
  const rand = new Uint16Array(length)
  crypto.getRandomValues(rand)
  const optionKeys = Object.keys(options)
    .filter((key) => options[key])
  return [...new Array(length)]
    .map((_, i) => {
      // make sure to include at least one of each selected option
      // and randomize the rest
      const currentOption = i < selectedOptionsCount
        ? optionValues[optionKeys[i]]
        : optionValues[optionKeys[rand[i] % optionKeys.length]]
      return currentOption[(rand[i] >> 2) % currentOption.length]
    })
    // shuffle for extra randomness
    .sort(() => 0.5 - Math.random())
    .join('')
}
Enter fullscreen mode Exit fullscreen mode

Now we can rest ye merry, gentlemen, as Santa's secrets are very secure.

Collapse
 
dailydevtips1 profile image
Chris Bongers

Nice one!
What's the bad thing about using Math.random?

Do like your super randomness function, smart to use the remained for that.
I think I hit a use-case when trying that where a option would not be added.

So thanks for this solution, great idea to make sure at least the options are set once and then re-shuffle for extra randomness.

Collapse
 
lexlohr profile image
Alex Lohr

Math.random is not random enough, it's just a seeded pseudo-random number generator. So if an attacker knows your code, they can reduce a lot of the possible combinations they would need to try. The same goes for cycling the options, just even worse so, because instead of 72 possible characters for each positions, there would now be only 26 or even 10 left.

Thread Thread
 
dailydevtips1 profile image
Chris Bongers

They should really rename that function 😂
Yeah I've taken in account it would not be the most "random" version because of the looping method always being the same.

Thanks Alex 👏