Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This file is a port of a subset of Chromium's implementation from
* which is Copyright 2018 The Chromium Authors. All rights reserved.
*/
const DEFAULT_PASSWORD_LENGTH = 15;
const MAX_UINT8 = Math.pow(2, 8) - 1;
const MAX_UINT32 = Math.pow(2, 32) - 1;
// Some characters are removed due to visual similarity:
const LOWER_CASE_ALPHA = "abcdefghijkmnpqrstuvwxyz"; // no 'l' or 'o'
const UPPER_CASE_ALPHA = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // no 'I' or 'O'
const DIGITS = "23456789"; // no '1' or '0'
const SPECIAL_CHARACTERS = "-~!@#$%^&*_+=)}:;\"'>,.?]";
const REQUIRED_CHARACTER_CLASSES = [
LOWER_CASE_ALPHA,
UPPER_CASE_ALPHA,
DIGITS,
SPECIAL_CHARACTERS,
];
// Consts for different password rules
const REQUIRED = "required";
const MAX_LENGTH = "maxlength";
const MIN_LENGTH = "minlength";
const MAX_CONSECUTIVE = "max-consecutive";
const UPPER = "upper";
const LOWER = "lower";
const DIGIT = "digit";
const SPECIAL = "special";
// Default password rules
const DEFAULT_RULES = new Map();
DEFAULT_RULES.set(MIN_LENGTH, REQUIRED_CHARACTER_CLASSES.length);
DEFAULT_RULES.set(MAX_LENGTH, MAX_UINT8);
DEFAULT_RULES.set(REQUIRED, [UPPER, LOWER, DIGIT, SPECIAL]);
export const PasswordGenerator = {
/**
* @param {Object} options
* @param {number} options.length - length of the generated password if there are no rules that override the length
* @param {Map} options.rules - map of password rules
* @returns {string} password that was generated
* @throws Error if `length` is invalid
* @copyright 2018 The Chromium Authors. All rights reserved.
*/
generatePassword({
length = DEFAULT_PASSWORD_LENGTH,
rules = DEFAULT_RULES,
inputMaxLength,
}) {
rules = new Map([...DEFAULT_RULES, ...rules]);
if (rules.get(MIN_LENGTH) > length) {
length = rules.get(MIN_LENGTH);
}
if (rules.get(MAX_LENGTH) < length) {
length = rules.get(MAX_LENGTH);
}
if (inputMaxLength > 0 && inputMaxLength < length) {
length = inputMaxLength;
}
let password = "";
let requiredClasses = [];
let allRequiredCharacters = "";
// Generate one character of each required class and/or required character list from the rules
this._addRequiredClassesAndCharacters(rules, requiredClasses);
// Generate one of each required class
for (const charClassString of requiredClasses) {
password +=
charClassString[this._randomUInt8Index(charClassString.length)];
if (Array.isArray(charClassString)) {
// Convert array into single string so that commas aren't
// concatenated with each character in the arbitrary character array.
allRequiredCharacters += charClassString.join("");
} else {
allRequiredCharacters += charClassString;
}
}
// Now fill the rest of the password with random characters.
while (password.length < length) {
password +=
allRequiredCharacters[
this._randomUInt8Index(allRequiredCharacters.length)
];
}
// So far the password contains the minimally required characters at the
// the beginning. Therefore, we create a random permutation.
password = this._shuffleString(password);
// Make sure the password passes the "max-consecutive" rule, if the rule exists
if (rules.has(MAX_CONSECUTIVE)) {
// Ensures that a password isn't shuffled an infinite number of times.
const DEFAULT_NUMBER_OF_SHUFFLES = 15;
let shuffleCount = 0;
let consecutiveFlag = this._checkConsecutiveCharacters(
password,
rules.get(MAX_CONSECUTIVE)
);
while (!consecutiveFlag) {
password = this._shuffleString(password);
consecutiveFlag = this._checkConsecutiveCharacters(
password,
rules.get(MAX_CONSECUTIVE)
);
++shuffleCount;
if (shuffleCount === DEFAULT_NUMBER_OF_SHUFFLES) {
consecutiveFlag = true;
}
}
}
return password;
},
/**
* Adds special characters and/or other required characters to the requiredCharacters array.
* @param {Map} rules
* @param {string[]} requiredClasses
*/
_addRequiredClassesAndCharacters(rules, requiredClasses) {
for (const charClass of rules.get(REQUIRED)) {
if (charClass === UPPER) {
requiredClasses.push(UPPER_CASE_ALPHA);
} else if (charClass === LOWER) {
requiredClasses.push(LOWER_CASE_ALPHA);
} else if (charClass === DIGIT) {
requiredClasses.push(DIGITS);
} else if (charClass === SPECIAL) {
requiredClasses.push(SPECIAL_CHARACTERS);
} else {
requiredClasses.push(charClass);
}
}
},
/**
* @param range to generate the number in
* @returns a random number in range [0, range).
* @copyright 2018 The Chromium Authors. All rights reserved.
*/
_randomUInt8Index(range) {
if (range > MAX_UINT8) {
throw new Error("`range` cannot fit into uint8");
}
// We must discard random results above this number, as they would
// make the random generator non-uniform (consider e.g. if
// MAX_UINT64 was 7 and |range| was 5, then a result of 1 would be twice
// as likely as a result of 3 or 4).
const MAX_ACCEPTABLE_VALUE = Math.floor(MAX_UINT8 / range) * range - 1;
const randomValueArr = new Uint8Array(1);
do {
crypto.getRandomValues(randomValueArr);
} while (randomValueArr[0] > MAX_ACCEPTABLE_VALUE);
return randomValueArr[0] % range;
},
/**
* Shuffle the order of characters in a string.
* @param {string} str to shuffle
* @returns {string} shuffled string
*/
_shuffleString(str) {
let arr = Array.from(str);
// Generate all the random numbers that will be needed.
const randomValues = new Uint32Array(arr.length - 1);
crypto.getRandomValues(randomValues);
// Fisher-Yates Shuffle
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor((randomValues[i - 1] / MAX_UINT32) * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr.join("");
},
/**
* Determine the number of consecutive characters in a string.
* This is primarily used to validate the "max-consecutive" rule
* of a generated password.
* @param {string} generatedPassword
* @param {number} value the number of consecutive characters allowed
* @return {boolean} `true` if the generatePassword has less than the value argument number of characters, `false` otherwise
*/
_checkConsecutiveCharacters(generatedPassword, value) {
let max = 0;
for (let start = 0, end = 1; end < generatedPassword.length; ) {
if (generatedPassword[end] === generatedPassword[start]) {
if (max < end - start + 1) {
max = end - start + 1;
if (max > value) {
return false;
}
}
end++;
} else {
start = end++;
}
}
return true;
},
_getUpperCaseCharacters() {
return UPPER_CASE_ALPHA;
},
_getLowerCaseCharacters() {
return LOWER_CASE_ALPHA;
},
_getDigits() {
return DIGITS;
},
_getSpecialCharacters() {
return SPECIAL_CHARACTERS;
},
};