/** * 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` ); } } }