summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js')
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js523
1 files changed, 523 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js b/toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js
new file mode 100644
index 0000000000..88520769cf
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js
@@ -0,0 +1,523 @@
+/**
+ * Test PasswordRulesManager.generatePassword()
+ */
+
+"use strict";
+const { PasswordGenerator } = ChromeUtils.importESModule(
+ "resource://gre/modules/PasswordGenerator.sys.mjs"
+);
+const { PasswordRulesManagerParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/PasswordRulesManager.sys.mjs"
+);
+const { PasswordRulesParser } = ChromeUtils.importESModule(
+ "resource://gre/modules/PasswordRulesParser.sys.mjs"
+);
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+const IMPROVED_RULES_COLLECTION = "password-rules";
+
+function getRulesForRecord(records, baseOrigin) {
+ let rules;
+ for (let record of records) {
+ if (record.Domain === baseOrigin) {
+ rules = record["password-rules"];
+ break;
+ }
+ }
+ return rules;
+}
+
+add_task(async function test_verify_password_rules() {
+ const testCases = [
+ { maxlength: 12 },
+ { minlength: 4, maxlength: 32 },
+ { required: ["lower"] },
+ { required: ["upper"] },
+ { required: ["digit"] },
+ { required: ["special"] },
+ { required: ["*", "$", "@", "_", "B", "Q"] },
+ { required: ["lower", "upper", "special"] },
+ { "max-consecutive": 2 },
+ {
+ minlength: 8,
+ required: [
+ "digit",
+ [
+ "-",
+ " ",
+ "!",
+ '"',
+ "#",
+ "$",
+ "&",
+ "'",
+ "(",
+ ")",
+ "*",
+ "+",
+ ",",
+ ".",
+ ":",
+ ";",
+ "<",
+ "=",
+ ">",
+ "?",
+ "@",
+ "[",
+ "^",
+ "_",
+ "`",
+ "{",
+ "|",
+ "}",
+ "~",
+ "]",
+ ],
+ ],
+ },
+ { minlength: 8, maxlength: 16, required: ["lower", "upper", "digit"] },
+ {
+ minlength: 8,
+ maxlength: 20,
+ required: ["lower", "upper", "digit"],
+ "max-consecutive": 2,
+ },
+ ];
+
+ for (let test of testCases) {
+ let mapOfRules = new Map();
+ let rules = ``;
+ for (let testRules in test) {
+ mapOfRules.set(testRules, test[testRules]);
+ if (test[testRules] === "required" && test[testRules].includes("*$")) {
+ rules += `${testRules}: ${test[testRules].join("")}`;
+ } else {
+ rules += `${testRules}: ${test[testRules]};`;
+ }
+ }
+ let generatedPassword = PasswordGenerator.generatePassword({
+ rules: mapOfRules,
+ });
+ verifyPassword(rules, generatedPassword);
+ }
+});
+
+/**
+ * Note: We do not test the "allowed" property in these tests.
+ * This is because a password can still be valid even if there is not a character from
+ * the "allowed" list.
+ * If a character, or character class, is required, then it should be marked as such.
+ * */
+
+add_task(async function test_generatePassword_many_rules() {
+ // Force password generation to be enabled.
+ Services.prefs.setBoolPref("signon.generation.available", true);
+ Services.prefs.setBoolPref("signon.generation.enabled", true);
+ Services.prefs.setBoolPref("signon.improvedPasswordRules.enabled", true);
+ // TEST_ORIGIN emulates the browsingContext.currentWindowGlobal.documentURI variable in LoginManagerParent
+ // and so it should always be a correctly formed URI when working with
+ // the PasswordRulesParser and PasswordRulesManager modules
+ const TEST_ORIGIN = Services.io.newURI("https://example.com");
+
+ // TEST_BASE_ORIGIN is how each domain is stored in RemoteSettings, and so
+ // we need this in order to parse out the particular password rules we're verifying
+ const TEST_BASE_ORIGIN = "example.com";
+ await LoginTestUtils.remoteSettings.setupImprovedPasswordRules();
+ const records = await RemoteSettings(IMPROVED_RULES_COLLECTION).get();
+
+ let rules = getRulesForRecord(records, TEST_BASE_ORIGIN);
+
+ rules = PasswordRulesParser.parsePasswordRules(rules);
+ Assert.ok(rules.length, "Rules should exist after parsing");
+
+ let PRMP = new PasswordRulesManagerParent();
+ Assert.ok(PRMP.generatePassword, "PRMP.generatePassword exists");
+
+ let generatedPassword = await PRMP.generatePassword(TEST_ORIGIN);
+ Assert.ok(generatedPassword, "A password was generated");
+
+ verifyPassword(rules, generatedPassword);
+
+ await LoginTestUtils.remoteSettings.cleanImprovedPasswordRules();
+});
+
+add_task(async function test_generatePassword_all_characters_allowed() {
+ // Force password generation to be enabled.
+ Services.prefs.setBoolPref("signon.generation.available", true);
+ Services.prefs.setBoolPref("signon.generation.enabled", true);
+ Services.prefs.setBoolPref("signon.improvedPasswordRules.enabled", true);
+ // TEST_ORIGIN emulates the browsingContext.currentWindowGlobal.documentURI variable in LoginManagerParent
+ // and so it should always be a correctly formed URI when working with
+ // the PasswordRulesParser and PasswordRulesManager modules
+ const TEST_ORIGIN = Services.io.newURI("https://example.com");
+
+ // TEST_BASE_ORIGIN is how each domain is stored in RemoteSettings, and so
+ // we need this in order to parse out the particular password rules we're verifying
+ const TEST_BASE_ORIGIN = "example.com";
+ const TEST_RULES = "minlength: 6; maxlength: 12;";
+ await LoginTestUtils.remoteSettings.setupImprovedPasswordRules(
+ TEST_BASE_ORIGIN,
+ TEST_RULES
+ );
+ const records = await RemoteSettings(IMPROVED_RULES_COLLECTION).get();
+
+ let rules = getRulesForRecord(records, TEST_BASE_ORIGIN);
+
+ rules = PasswordRulesParser.parsePasswordRules(rules);
+ Assert.ok(rules.length, "Rules should exist after parsing");
+
+ let PRMP = new PasswordRulesManagerParent();
+ Assert.ok(PRMP.generatePassword, "PRMP.generatePassword exists");
+
+ let generatedPassword = await PRMP.generatePassword(TEST_ORIGIN);
+ Assert.ok(generatedPassword, "A password was generated");
+
+ verifyPassword(rules, generatedPassword);
+
+ await LoginTestUtils.remoteSettings.cleanImprovedPasswordRules();
+});
+
+add_task(async function test_generatePassword_required_special_character() {
+ // Force password generation to be enabled.
+ Services.prefs.setBoolPref("signon.generation.available", true);
+ Services.prefs.setBoolPref("signon.generation.enabled", true);
+ Services.prefs.setBoolPref("signon.improvedPasswordRules.enabled", true);
+ // TEST_ORIGIN emulates the browsingContext.currentWindowGlobal.documentURI variable in LoginManagerParent
+ // and so it should always be a correctly formed URI when working with
+ // the PasswordRulesParser and PasswordRulesManager modules
+ const TEST_ORIGIN = Services.io.newURI("https://example.com");
+
+ // TEST_BASE_ORIGIN is how each domain is stored in RemoteSettings, and so
+ // we need this in order to parse out the particular password rules we're verifying
+ const TEST_BASE_ORIGIN = "example.com";
+ const TEST_RULES = "required: special";
+ await LoginTestUtils.remoteSettings.setupImprovedPasswordRules(
+ TEST_BASE_ORIGIN,
+ TEST_RULES
+ );
+ const records = await RemoteSettings(IMPROVED_RULES_COLLECTION).get();
+
+ let rules = getRulesForRecord(records, TEST_BASE_ORIGIN);
+
+ rules = PasswordRulesParser.parsePasswordRules(rules);
+ Assert.ok(rules.length, "Rules should exist after parsing");
+
+ let PRMP = new PasswordRulesManagerParent();
+ Assert.ok(PRMP.generatePassword, "PRMP.generatePassword exists");
+
+ let generatedPassword = await PRMP.generatePassword(TEST_ORIGIN);
+ Assert.ok(generatedPassword, "A password was generated");
+
+ verifyPassword(TEST_RULES, generatedPassword);
+});
+
+add_task(
+ async function test_generatePassword_with_arbitrary_required_characters() {
+ // Force password generation to be enabled.
+ Services.prefs.setBoolPref("signon.generation.available", true);
+ Services.prefs.setBoolPref("signon.generation.enabled", true);
+ Services.prefs.setBoolPref("signon.improvedPasswordRules.enabled", true);
+ // TEST_ORIGIN emulates the browsingContext.currentWindowGlobal.documentURI variable in LoginManagerParent
+ // and so it should always be a correctly formed URI when working with
+ // the PasswordRulesParser and PasswordRulesManager modules
+ const TEST_ORIGIN = Services.io.newURI("https://example.com");
+
+ // TEST_BASE_ORIGIN is how each domain is stored in RemoteSettings, and so
+ // we need this in order to parse out the particular password rules we're verifying
+ const TEST_BASE_ORIGIN = "example.com";
+ const REQUIRED_ARBITRARY_CHARACTERS = "!#$@*()_+=";
+ // We use an extremely long password to ensure there are no invalid characters generated in the password.
+ // This ensures we exhaust all of "allRequiredCharacters" in PasswordGenerator.jsm.
+ // Otherwise, there's a small chance a "," may have been added to "allRequiredCharacters"
+ // which will generate an invalid password in this case.
+ const TEST_RULES = `required: [${REQUIRED_ARBITRARY_CHARACTERS}], upper, lower; maxlength: 255; minlength: 255;`;
+ await LoginTestUtils.remoteSettings.setupImprovedPasswordRules(
+ TEST_BASE_ORIGIN,
+ TEST_RULES
+ );
+ const records = await RemoteSettings(IMPROVED_RULES_COLLECTION).get();
+
+ let rules = getRulesForRecord(records, TEST_BASE_ORIGIN);
+
+ rules = PasswordRulesParser.parsePasswordRules(rules);
+ Assert.ok(rules.length, "Rules should exist after parsing");
+
+ let PRMP = new PasswordRulesManagerParent();
+ Assert.ok(PRMP.generatePassword, "PRMP.generatePassword exists");
+
+ let generatedPassword = await PRMP.generatePassword(TEST_ORIGIN);
+ generatedPassword = await PRMP.generatePassword(TEST_ORIGIN);
+ Assert.ok(generatedPassword, "A password was generated");
+
+ verifyPassword(TEST_RULES, generatedPassword);
+
+ let specialCharacters = PasswordGenerator._getSpecialCharacters();
+ let digits = PasswordGenerator._getDigits();
+ // Additional verification for this password case since
+ // we want to ensure no extra special characters and no digits are generated.
+ let disallowedSpecialCharacters = "";
+ for (let char of specialCharacters) {
+ if (!REQUIRED_ARBITRARY_CHARACTERS.includes(char)) {
+ disallowedSpecialCharacters += char;
+ }
+ }
+ for (let char of disallowedSpecialCharacters) {
+ Assert.ok(
+ !generatedPassword.includes(char),
+ "Password must not contain any disallowed special characters: " + char
+ );
+ }
+ for (let char of digits) {
+ Assert.ok(
+ !generatedPassword.includes(char),
+ "Password must not contain any digits: " + char
+ );
+ }
+ }
+);
+
+// Checks the "www4.prepaid.bankofamerica.com" case to ensure the rules are found
+add_task(async function test_generatePassword_subdomain_rule() {
+ const testCases = [
+ {
+ uri: "https://www4.test.example.com",
+ rulesDomain: "example.com",
+ shouldApplyPWRule: true,
+ },
+ {
+ uri: "https://test.example.com",
+ rulesDomain: "example.com",
+ shouldApplyPWRule: true,
+ },
+ {
+ uri: "https://example.com",
+ rulesDomain: "example.com",
+ shouldApplyPWRule: true,
+ },
+ {
+ uri: "https://www4.test.example.com",
+ rulesDomain: "test.example.com",
+ shouldApplyPWRule: true,
+ },
+ {
+ uri: "https://test.example.com",
+ rulesDomain: "test.example.com",
+ shouldApplyPWRule: true,
+ },
+ {
+ uri: "https://example.com",
+ rulesDomain: "test.example.com",
+ shouldApplyPWRule: false,
+ },
+ {
+ uri: "https://evil.com",
+ rulesDomain: "example.com",
+ shouldApplyPWRule: false,
+ },
+ {
+ uri: "https://evil.example.com",
+ rulesDomain: "test.example.com",
+ shouldApplyPWRule: false,
+ },
+ {
+ uri: "https://test.example.com.cn",
+ rulesDomain: "test.example.com",
+ shouldApplyPWRule: false,
+ },
+ {
+ uri: "https://eviltest.example.com",
+ rulesDomain: "test.example.com",
+ shouldApplyPWRule: false,
+ },
+ ];
+ const TEST_RULES = "required: special; maxlength: 12;";
+
+ for (let test of testCases) {
+ await LoginTestUtils.remoteSettings.setupImprovedPasswordRules(
+ test.rulesDomain,
+ TEST_RULES
+ );
+ const TEST_ORIGIN = Services.io.newURI(test.uri);
+ const TEST_BASE_ORIGIN = test.rulesDomain;
+ const records = await RemoteSettings(IMPROVED_RULES_COLLECTION).get();
+
+ let rules = getRulesForRecord(records, TEST_BASE_ORIGIN);
+
+ rules = PasswordRulesParser.parsePasswordRules(rules);
+ Assert.ok(rules.length, "Rules should exist after parsing");
+
+ let PRMP = new PasswordRulesManagerParent();
+
+ let generatedPassword = await PRMP.generatePassword(TEST_ORIGIN);
+ Assert.ok(
+ generatedPassword,
+ "A password was generated for URI: " + test.uri
+ );
+
+ // If a rule should be applied, we verify the password has all the required classes in the generated password.
+ if (test.shouldApplyPWRule) {
+ verifyPassword(rules, generatedPassword);
+ }
+ }
+});
+
+add_task(async function test_improved_password_rules_telemetry() {
+ // Force password generation to be enabled.
+ Services.prefs.setBoolPref("signon.generation.available", true);
+ Services.prefs.setBoolPref("signon.generation.enabled", true);
+ Services.prefs.setBoolPref("signon.improvedPasswordRules.enabled", true);
+
+ const IMPROVED_PASSWORD_GENERATION_HISTOGRAM =
+ "PWMGR_NUM_IMPROVED_GENERATED_PASSWORDS";
+
+ // Clear out the previous pings from this test
+ let snapshot = TelemetryTestUtils.getAndClearHistogram(
+ IMPROVED_PASSWORD_GENERATION_HISTOGRAM
+ );
+
+ // TEST_ORIGIN emulates the browsingContext.currentWindowGlobal.documentURI variable in LoginManagerParent
+ // and so it should always be a correctly formed URI when working with
+ // the PasswordRulesParser and PasswordRulesManager modules
+ let TEST_ORIGIN = Services.io.newURI("https://example.com");
+ await LoginTestUtils.remoteSettings.setupImprovedPasswordRules();
+
+ let PRMP = new PasswordRulesManagerParent();
+
+ // Generate a password with custom rules,
+ // so we should send a ping to the custom rules bucket (position 1).
+ let generatedPassword = await PRMP.generatePassword(TEST_ORIGIN);
+ Assert.ok(generatedPassword, "A password was generated");
+
+ TelemetryTestUtils.assertHistogram(snapshot, 1, 1);
+
+ TEST_ORIGIN = Services.io.newURI("https://otherexample.com");
+ // Generate a password with default rules,
+ // so we should send a ping to the default rules bucket (position 0).
+ snapshot = TelemetryTestUtils.getAndClearHistogram(
+ IMPROVED_PASSWORD_GENERATION_HISTOGRAM
+ );
+ generatedPassword = await PRMP.generatePassword(TEST_ORIGIN);
+ Assert.ok(generatedPassword, "A password was generated");
+
+ TelemetryTestUtils.assertHistogram(snapshot, 0, 1);
+});
+
+function checkCharacters(password, _characters) {
+ let containsCharacters = false;
+ let testString = _characters.join("");
+ for (let character of password) {
+ containsCharacters = testString.includes(character);
+ if (containsCharacters) {
+ return containsCharacters;
+ }
+ }
+ return containsCharacters;
+}
+
+function checkConsecutiveCharacters(generatePassword, value) {
+ let findMaximumRepeating = str => {
+ let max = 0;
+ for (let start = 0, end = 1; end < str.length; ) {
+ if (str[end] === str[start]) {
+ if (max < end - start + 1) {
+ max = end - start + 1;
+ if (max > value) {
+ return max;
+ }
+ }
+ end++;
+ } else {
+ start = end++;
+ }
+ }
+ return max;
+ };
+ let consecutiveCharacters = findMaximumRepeating(generatePassword);
+ if (consecutiveCharacters <= value) {
+ return true;
+ }
+ return false;
+}
+
+function verifyPassword(rules, generatedPassword) {
+ const UPPER_CASE_ALPHA = PasswordGenerator._getUpperCaseCharacters();
+ const LOWER_CASE_ALPHA = PasswordGenerator._getLowerCaseCharacters();
+ const DIGITS = PasswordGenerator._getDigits();
+ const SPECIAL_CHARACTERS = PasswordGenerator._getSpecialCharacters();
+ for (let rule of rules) {
+ let { _name, value } = rule;
+ if (_name === "required") {
+ for (let required of value) {
+ if (required._name === "upper") {
+ let _checkUppercase = new RegExp(`[${UPPER_CASE_ALPHA}]`);
+ Assert.ok(
+ generatedPassword.match(_checkUppercase),
+ "Password must include upper case letter"
+ );
+ } else if (required._name === "lower") {
+ let _checkLowercase = new RegExp(`[${LOWER_CASE_ALPHA}]`);
+ Assert.ok(
+ generatedPassword.match(_checkLowercase),
+ "Password must include lower case letter"
+ );
+ } else if (required._name === "digit") {
+ let _checkDigits = new RegExp(`[${DIGITS}]`);
+ Assert.ok(
+ generatedPassword.match(_checkDigits),
+ "Password must include digits"
+ );
+ generatedPassword.match(_checkDigits);
+ } else if (required._name === "special") {
+ // We need to escape our special characters since some of them
+ // have special meaning in regex.
+ let escapedSpecialCharacters = SPECIAL_CHARACTERS.replace(
+ /[.*\-+?^${}()|[\]\\]/g,
+ "\\$&"
+ );
+ let _checkSpecial = new RegExp(`[${escapedSpecialCharacters}]`);
+ Assert.ok(
+ generatedPassword.match(_checkSpecial),
+ "Password must include special character"
+ );
+ } else {
+ // Nested destructing of the value object in the characters case
+ let [{ _characters }] = value;
+
+ // We can't use regex to do a quick check here since the
+ // required characters could be characters that need to be escaped
+ // in order for the regex to work properly ([]"^...etc)
+ Assert.ok(
+ checkCharacters(generatedPassword, _characters),
+ `Password must contain one of the following characters: ${_characters}`
+ );
+ }
+ }
+ } else if (_name === "minlength") {
+ Assert.ok(
+ generatedPassword.length >= value,
+ `Password should have a minimum length of ${value}`
+ );
+ } else if (_name === "maxlength") {
+ Assert.ok(
+ generatedPassword.length <= value,
+ `Password should have a maximum length of ${value}`
+ );
+ } else if (_name === "max-consecutive") {
+ Assert.ok(
+ checkConsecutiveCharacters(generatedPassword, value),
+ `Password must not contain more than ${value} consecutive characters`
+ );
+ }
+ }
+}