summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/PasswordGenerator.jsm
blob: cdf087ac2e00688e130063627f29c5074ecb1d0d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/* 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/. */

"use strict";

/**
 * 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 EXPORTED_SYMBOLS = ["PasswordGenerator"];

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);

XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);

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 ALL_CHARACTERS = LOWER_CASE_ALPHA + UPPER_CASE_ALPHA + DIGITS;

const REQUIRED_CHARACTER_CLASSES = [LOWER_CASE_ALPHA, UPPER_CASE_ALPHA, DIGITS];

this.PasswordGenerator = {
  /**
   * @param {Number} length of the password to generate
   * @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) {
    if (length < REQUIRED_CHARACTER_CLASSES.length) {
      throw new Error("requested password length is too short");
    }

    if (length > MAX_UINT8) {
      throw new Error("requested password length is too long");
    }
    let password = "";

    // Generate one of each required class
    for (const charClassString of REQUIRED_CHARACTER_CLASSES) {
      password +=
        charClassString[this._randomUInt8Index(charClassString.length)];
    }

    // Now fill the rest of the password with random characters.
    while (password.length < length) {
      password += ALL_CHARACTERS[this._randomUInt8Index(ALL_CHARACTERS.length)];
    }

    // So far the password contains the minimally required characters at the
    // the beginning. Therefore, we create a random permutation.
    password = this._shuffleString(password);

    return password;
  },

  /**
   * @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("");
  },
};