Bug 1548393 - Generate a digestable password, free of ambiguous characters. r=mhoye,sfoster
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Mon, 13 May 2019 20:55:46 +0000
changeset 532512 96563508f9fed49ddc64469d05c9d3d7be46d547
parent 532511 b1c78bd9fdc280ce52f84e105321a79a89e2177a
child 532533 e0a622476b7756daebac7ccb7a9bbeb5dfe3cac0
child 532539 5f723f79b70e9f2fa2fb8ec563686e79267cba8f
push id11268
push usercsabou@mozilla.com
push dateTue, 14 May 2019 15:24:22 +0000
treeherdermozilla-beta@5fb7fcd568d6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmhoye, sfoster
bugs1548393
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1548393 - Generate a digestable password, free of ambiguous characters. r=mhoye,sfoster Differential Revision: https://phabricator.services.mozilla.com/D30249
toolkit/components/passwordmgr/PasswordGenerator.jsm
toolkit/components/passwordmgr/moz.build
toolkit/components/passwordmgr/test/unit/test_PasswordGenerator.js
toolkit/components/passwordmgr/test/unit/xpcshell.ini
toolkit/content/license.html
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/PasswordGenerator.jsm
@@ -0,0 +1,113 @@
+/* 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.
+ */
+
+var 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,
+];
+
+const 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("");
+  },
+};
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -47,16 +47,17 @@ EXTRA_JS_MODULES += [
 if CONFIG['OS_TARGET'] == 'Android':
     EXTRA_JS_MODULES += [
         'storage-mozStorage.js',
     ]
 else:
     EXTRA_JS_MODULES += [
         'LoginImport.jsm',
         'LoginStore.jsm',
+        'PasswordGenerator.jsm',
         'storage-json.js',
     ]
 
 if CONFIG['OS_TARGET'] == 'WINNT':
     EXTRA_JS_MODULES += [
         'OSCrypto_win.js',
     ]
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_PasswordGenerator.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const {PasswordGenerator} = ChromeUtils.import("resource://gre/modules/PasswordGenerator.jsm");
+
+add_task(async function test_shuffleString() {
+  let original = "1234567890";
+  let shuffled = PasswordGenerator._shuffleString(original);
+  notEqual(original, shuffled, "String should have been shuffled");
+});
+
+add_task(async function test_randomUInt8Index() {
+  throws(() => PasswordGenerator._randomUInt8Index(256), /uint8/, "Should throw for larger than uint8");
+  ok(Number.isSafeInteger(PasswordGenerator._randomUInt8Index(255)), "Check integer returned");
+});
+
+add_task(async function test_generatePassword_classes() {
+  let password = PasswordGenerator.generatePassword(/* REQUIRED_CHARACTER_CLASSES */ 3);
+  info(password);
+  equal(password.length, 3, "Check length is correct");
+  ok(password.match(/[a-km-np-z]/), "Minimal password should include at least one lowercase character");
+  ok(password.match(/[A-HJ-NP-Z]/), "Minimal password should include at least one uppercase character");
+  ok(password.match(/[2-9]/), "Minimal password should include at least one digit");
+  ok(password.match(/^[a-km-np-zA-HJ-NP-Z2-9]+$/), "All characters should be in the expected set");
+});
+
+add_task(async function test_generatePassword_length() {
+  let password = PasswordGenerator.generatePassword(5);
+  info(password);
+  equal(password.length, 5, "Check length is correct");
+
+  throws(() => PasswordGenerator.generatePassword(2), /too short/i, "Throws if too short");
+  throws(() => PasswordGenerator.generatePassword(Math.pow(2, 8)), /too long/i, "Throws if too long");
+  ok(password.match(/^[a-km-np-zA-HJ-NP-Z2-9]+$/), "All characters should be in the expected set");
+});
+
+add_task(async function test_generatePassword_defaultLength() {
+  let password = PasswordGenerator.generatePassword();
+  info(password);
+  equal(password.length, 15, "Check default length is correct");
+  ok(password.match(/^[a-km-np-zA-HJ-NP-Z2-9]{15}$/), "All characters should be in the expected set");
+});
+
--- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -35,13 +35,15 @@ skip-if = os == "android"
 skip-if = os == "android" # Bug 1171687: Needs fixing on Android
 [test_logins_metainfo.js]
 [test_logins_search.js]
 [test_maybeImportLogin.js]
 skip-if = os == "android" # Only used by migrator, which isn't on Android
 [test_notifications.js]
 [test_OSCrypto_win.js]
 skip-if = os != "win"
+[test_PasswordGenerator.js]
+skip-if = os == "android" # Not packaged/used on Android
 [test_recipes_add.js]
 [test_recipes_content.js]
 [test_search_schemeUpgrades.js]
 [test_storage.js]
 [test_telemetry.js]
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -3019,16 +3019,17 @@ WITH THE USE OR PERFORMANCE OF THIS SOFT
     <p>This license applies to parts of the code in:</p>
     <ul>
         <li><code>browser/extensions/formautofill/content/heuristicsRegexp.js</code></li>
         <li><code>browser/extensions/formautofill/FormAutofillHeuristics.jsm</code></li>
         <li><code>browser/extensions/formautofill/FormAutofillNameUtils.jsm</code></li>
         <li><code>editor/libeditor/EditorEventListener.cpp</code></li>
         <li><code>mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StrictModeContext.java</code></li>
         <li><code>security/sandbox/</code></li>
+        <li><code>toolkit/components/passwordmgr/PasswordGenerator.jsm</code></li>
         <li><code>widget/cocoa/GfxInfo.mm</code></li>
         <li><code>widget/windows/nsWindow.cpp</code></li>
     </ul>
     <p>and also some files in these directories:</p>
     <ul>
         <li><code>dom/media/webspeech/recognition/</code></li>
         <li><code>dom/plugins/</code></li>
         <li><code>gfx/ots/</code></li>
@@ -3036,17 +3037,17 @@ WITH THE USE OR PERFORMANCE OF THIS SOFT
         <li><code>ipc/chromium/</code></li>
         <li><code>media/openmax_dl/</code></li>
         <li><code>toolkit/components/downloads/chromium/</code></li>
         <li><code>toolkit/components/url-classifier/chromium/</code></li>
         <li><code>tools/profiler/</code></li>
     </ul>
 
 <pre>
-Copyright (c) 2006-2016 The Chromium Authors. All rights reserved.
+Copyright (c) 2006-2018 The Chromium Authors. All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met:
 
    * Redistributions of source code must retain the above copyright
 notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above