JavaScript Password Generator Sometimes Not Including Character Selections?
Hello Stack Overflow!
This is my first time posting on the site so please bare with me and my question. My class was tasked with individually creating a password generator using JavaScript. Thankfully I had gotten most of the application operating correctly, but I've gotten stuck on a problem.
Example: The user chooses to have 8 characters in their password and chooses to include special, lowercase, and uppercase characters. When the password is generated sometimes it won't include all of the character selections. (Sometimes it'll generate a password with both special and uppercase characters, but not have a single lowercase character).
I've been finished with this assignment for a minute now, but my goal is to understand what I can do to fix this problem and complete this app anyway. I was thinking of potentially removing the passwordOptions object and turning each option into an array of their own, what are your thoughts?
Thank you so much for any suggestions! :D
// passwordOptions contains all necessary string data needed to generate the password
const passwordOptions = {
num: "1234567890",
specialChar: "!@#$%&'()*+,^-./:;<=>?[]_`{~}|",
lowerCase: "abcdefghijklmnopqrstuvwxyz",
upperCase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
};
document.getElementById('generate').addEventListener('click', function() {
alert(generatePassword());
});
// Executes when button is clicked
let generatePassword = function() {
// initial state for password information
let passInfo = "";
// ask user for the length of their password
let characterAmount = window.prompt("Enter the amount of characters you want for your password. NOTE: Must be between 8-128 characters");
// If the character length doesn't match requirements, alert the user
if (characterAmount >= 8 && characterAmount < 129) {
// ask if user wants to include integers
let getInteger = window.confirm("Would you like to include NUMBERS?");
// if user wants to include numbers
if (getInteger) {
// add numerical characters to password data
passInfo = passInfo + passwordOptions.num;
};
// ask if user wants to include special characters
let getSpecialCharacters = window.confirm("Would you like to include SPECIAL characters?");
// if user wants to include special characters
if (getSpecialCharacters) {
// add special characters to password data
passInfo = passInfo + passwordOptions.specialChar;
};
// ask if user wants to include lowercase characters
let getLowerCase = window.confirm("Would you like to include LOWERCASE characters?");
// if user wants to include lowercase characters
if (getLowerCase) {
// add lowercase characters to password data
passInfo = passInfo + passwordOptions.lowerCase;
};
// ask if user wants to include uppercase characters
let getUpperCase = window.confirm("Would you like to include UPPERCASE characters?");
// if user wants to include uppercase characters
if (getUpperCase) {
// add uppercase characters to password data
passInfo = passInfo + passwordOptions.upperCase;
};
// ensure user chooses at least one option
if (getInteger !=true && getSpecialCharacters !=true && getLowerCase !=true && getUpperCase !=true) {
// notify user needs to select at least one option
window.alert("You need to select at least one option, please try again!");
// return user back to their questions
return generatePassword();
};
// randomPassword is an empty string that the for loop will pass information in
let randomPassword = "";
// for loop grabs characterAmount to use
for (let i = 0; i < characterAmount; i++) {
//passInfo connects to charAt that uses both Math.floor and random to take the length of passInfo and randomize the results
randomPassword += passInfo[Math.floor(Math.random() * passInfo.length)];
};
// return password results
return randomPassword;
}
// if user's response is invalid
else {
// alert user
window.alert("You need to provide a valid length!");
// return user back to their questions
/* Removed for testing purposes to break the endless loop. */
// return generatePassword();
}
};
<button id="generate">Run</button>
The best way I can imagine is to pick one character from each selected category, then select the remaining characters randomly, finally, shuffle the selected characters.
You can do it like this:
// passwordOptions contains all necessary string data needed to generate the password
const passwordOptions = {
num: "1234567890",
specialChar: "!@#$%&'()*+,^-./:;<=>?[]_`{~}|",
lowerCase: "abcdefghijklmnopqrstuvwxyz",
upperCase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
};
document.getElementById('generate').addEventListener('click', function() {
alert(generatePassword());
});
// Executes when button is clicked
let generatePassword = function() {
// initial state for password information
let passInfo = "";
// list of chosen characters
const passChars = [];
// ask user for the length of their password
let characterAmount = window.prompt("Enter the amount of characters you want for your password. NOTE: Must be between 8-128 characters");
// If the character length doesn't match requirements, alert the user
if (characterAmount >= 8 && characterAmount <= 128) {
// ask if user wants to include integers
const getInteger = window.confirm("Would you like to include NUMBERS?");
// if user wants to include numbers
if (getInteger) {
// add numerical characters to password data
passInfo += passwordOptions.num;
// add a number to the list of chosen characters
passChars.push(getRandomChar(passwordOptions.num));
};
// ask if user wants to include special characters
const getSpecialCharacters = window.confirm("Would you like to include SPECIAL characters?");
// if user wants to include special characters
if (getSpecialCharacters) {
// add special characters to password data
passInfo += passwordOptions.specialChar;
// add a special character to the list of chosen characters
passChars.push(getRandomChar(passwordOptions.specialChar));
};
// ask if user wants to include lowercase characters
const getLowerCase = window.confirm("Would you like to include LOWERCASE characters?");
// if user wants to include lowercase characters
if (getLowerCase) {
// add lowercase characters to password data
passInfo += passwordOptions.lowerCase;
// add a lowercase character to the list of chosen characters
passChars.push(getRandomChar(passwordOptions.lowerCase));
};
// ask if user wants to include uppercase characters
const getUpperCase = window.confirm("Would you like to include UPPERCASE characters?");
// if user wants to include uppercase characters
if (getUpperCase) {
// add uppercase characters to password data
passInfo += passwordOptions.upperCase;
// add an uppercase character to the list of chosen characters
passChars.push(getRandomChar(passwordOptions.upperCase));
};
// ensure user chooses at least one option -- passInfo will be empty if they don't
if (!passInfo) {
// notify user needs to select at least one option
window.alert("You need to select at least one option, please try again!");
// return user back to their questions
return generatePassword();
};
// while there aren't enough characters
while(passChars.length < characterAmount) {
// choose a random char from charInfo
passChars.push(getRandomChar(passInfo));
};
// shuffle the list of characters using Fisher-Yates algorithm
// see https://stackoverflow.com/a/2450976/8376184
for(let i = passChars.length - 1; i > 0; i--){
const swapIndex = Math.floor(Math.random() * (i + 1));
const temp = passChars[i];
passChars[i] = passChars[swapIndex];
passChars[swapIndex] = temp;
};
// return the password character list concatenated to a string
return passChars.join("");
}
// if user's response is invalid
else {
// alert user
window.alert("You need to provide a valid length!");
// return user back to their questions
/* Removed for testing purposes to break the endless loop. */
// return generatePassword();
}
};
function getRandomChar(fromString){
return fromString[Math.floor(Math.random() * fromString.length)];
}
<button id="generate">Run</button>
But, as password generators should be cryptographically random, you should use crypto.getRandomValues()
instead of Math.random()
. You can use this algorithm to convert it to a range of values:
// Generate a random integer r with equal chance in 0 <= r < max.
function randRange(max) {
const requestBytes = Math.ceil(Math.log2(max) / 8);
if (!requestBytes) { // No randomness required
return 0;
};
const maxNum = Math.pow(256, requestBytes);
const ar = new Uint8Array(requestBytes);
while (true) {
window.crypto.getRandomValues(ar);
let val = 0;
for (let i = 0; i < requestBytes;i++) {
val = (val << 8) + ar[i];
};
if (val < maxNum - maxNum % max) {
return val % max;
};
};
};
You can combine this with the code above like this:
// passwordOptions contains all necessary string data needed to generate the password
const passwordOptions = {
num: "1234567890",
specialChar: "!@#$%&'()*+,^-./:;<=>?[]_`{~}|",
lowerCase: "abcdefghijklmnopqrstuvwxyz",
upperCase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
};
document.getElementById('generate').addEventListener('click', function() {
alert(generatePassword());
});
// Executes when button is clicked
let generatePassword = function() {
// initial state for password information
let passInfo = "";
// list of chosen characters
const passChars = [];
// ask user for the length of their password
let characterAmount = window.prompt("Enter the amount of characters you want for your password. NOTE: Must be between 8-128 characters");
// If the character length doesn't match requirements, alert the user
if (characterAmount >= 8 && characterAmount <= 128) {
// ask if user wants to include integers
const getInteger = window.confirm("Would you like to include NUMBERS?");
// if user wants to include numbers
if (getInteger) {
// add numerical characters to password data
passInfo += passwordOptions.num;
// add a number to the list of chosen characters
passChars.push(getRandomChar(passwordOptions.num));
};
// ask if user wants to include special characters
const getSpecialCharacters = window.confirm("Would you like to include SPECIAL characters?");
// if user wants to include special characters
if (getSpecialCharacters) {
// add special characters to password data
passInfo += passwordOptions.specialChar;
// add a special character to the list of chosen characters
passChars.push(getRandomChar(passwordOptions.specialChar));
};
// ask if user wants to include lowercase characters
const getLowerCase = window.confirm("Would you like to include LOWERCASE characters?");
// if user wants to include lowercase characters
if (getLowerCase) {
// add lowercase characters to password data
passInfo += passwordOptions.lowerCase;
// add a lowercase character to the list of chosen characters
passChars.push(getRandomChar(passwordOptions.lowerCase));
};
// ask if user wants to include uppercase characters
const getUpperCase = window.confirm("Would you like to include UPPERCASE characters?");
// if user wants to include uppercase characters
if (getUpperCase) {
// add uppercase characters to password data
passInfo += passwordOptions.upperCase;
// add an uppercase character to the list of chosen characters
passChars.push(getRandomChar(passwordOptions.upperCase));
};
// ensure user chooses at least one option -- passInfo will be empty if they don't
if (!passInfo) {
// notify user needs to select at least one option
window.alert("You need to select at least one option, please try again!");
// return user back to their questions
return generatePassword();
};
// while there aren't enough characters
while(passChars.length < characterAmount) {
// choose a random char from charInfo
passChars.push(getRandomChar(passInfo));
};
// shuffle the list of characters using Fisher-Yates algorithm
// see https://stackoverflow.com/a/2450976/8376184
for(let i = passChars.length - 1; i > 0; i--){
const swapIndex = randRange(i + 1);
const temp = passChars[i];
passChars[i] = passChars[swapIndex];
passChars[swapIndex] = temp;
};
// return the password character list concatenated to a string
return passChars.join("");
}
// if user's response is invalid
else {
// alert user
window.alert("You need to provide a valid length!");
// return user back to their questions
/* Removed for testing purposes to break the endless loop. */
// return generatePassword();
}
};
function getRandomChar(fromString){
return fromString[randRange(fromString.length)];
};
// Generate a random integer r with equal chance in 0 <= r < max.
function randRange(max) {
const requestBytes = Math.ceil(Math.log2(max) / 8);
if (!requestBytes) { // No randomness required
return 0;
};
const maxNum = Math.pow(256, requestBytes);
const ar = new Uint8Array(requestBytes);
while (true) {
window.crypto.getRandomValues(ar);
let val = 0;
for (let i = 0; i < requestBytes;i++) {
val = (val << 8) + ar[i];
};
if (val < maxNum - maxNum % max) {
return val % max;
};
};
};
<button id="generate">Run</button>
Rather than doing it on the fly, separate the questions from the actual function which generates the password.
Then simply count the enabled options and divide that number by the password length, then that will be how many chars from each set you use, you can also then use that number to repeat each set to average pad the total chars needed for the password
generatePassword(32, {
numbers: true,
special: true,
lowerCase: true,
upperCase: true
})
aDq.6@9l%Hx=OgS'(3WZNI?372siy12$
function generatePassword(len, options) {
const chars = {
num: "1234567890",
specialChar: "!@#$%&'()*+,^-./:;<=>?[]_`{~}|",
lowerCase: "abcdefghijklmnopqrstuvwxyz",
upperCase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
custom: options.custom || undefined
};
const shuffleStr = str => str.split('').sort(() => 0.5 - Math.random()).join('')
const factor = Math.ceil(len / Object.values(options).reduce((a, b) => b ? a + 1 : a, 0))
let str = ''
if (options.numbers) str += shuffleStr(chars.num.repeat(factor)).substr(0, factor)
if (options.special) str += shuffleStr(chars.specialChar.repeat(factor)).substr(0, factor)
if (options.lowerCase) str += shuffleStr(chars.lowerCase.repeat(factor)).substr(0, factor)
if (options.upperCase) str += shuffleStr(chars.upperCase.repeat(factor)).substr(0, factor)
if (options.custom) str += shuffleStr(chars.custom.repeat(factor)).substr(0, factor)
return shuffleStr(str).substr(0, len)
}
console.log(generatePassword(32, {
numbers: true,
special: true,
lowerCase: true,
upperCase: true
}))
console.log(generatePassword(32, {
numbers: true,
special: false,
lowerCase: false,
upperCase: false
}))
console.log(generatePassword(32, {
numbers: false,
special: true,
lowerCase: false,
upperCase: false
}))
console.log(generatePassword(32, {
numbers: false,
special: false,
lowerCase: true,
upperCase: false
}))
console.log(generatePassword(32, {
numbers: false,
special: false,
lowerCase: false,
upperCase: true
}))
console.log(generatePassword(32, {
custom: 'abc1'
}))