forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			229 lines
		
	
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			229 lines
		
	
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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
 | |
|  * https://cs.chromium.org/chromium/src/components/password_manager/core/browser/generation/password_generator.cc?l=93&rcl=a896a3ac4ea731b5ab3d2ab5bd76a139885d5c4f
 | |
|  * 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.
 | |
|    * @see https://cs.chromium.org/chromium/src/components/password_manager/core/browser/generation/password_generator.cc?l=93&rcl=a896a3ac4ea731b5ab3d2ab5bd76a139885d5c4f
 | |
|    */
 | |
|   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.
 | |
|    * @see https://cs.chromium.org/chromium/src/base/rand_util.cc?l=58&rcl=648a59893e4ed5303b5c381b03ce0c75e4165617
 | |
|    */
 | |
|   _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).
 | |
|     // See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias
 | |
|     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
 | |
|     // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_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;
 | |
|   },
 | |
| };
 | 
