Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FXIOS-9964 - Integrate password generator inside password manager script #22150

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion firefox-ios/Client/Assets/CC_Script/CC_Python_Update.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"toolkit/modules/third_party/fathom/fathom.mjs",
"toolkit/components/passwordmgr/shared/LoginFormFactory.sys.mjs",
"toolkit/components/passwordmgr/shared/NewPasswordModel.sys.mjs",
"toolkit/components/passwordmgr/shared/PasswordGenerator.sys.mjs ",
"toolkit/components/passwordmgr/shared/PasswordGenerator.sys.mjs",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space typo caused this file not be pulled 😅

"toolkit/components/passwordmgr/shared/PasswordRulesParser.sys.mjs",
"toolkit/components/passwordmgr/LoginManager.shared.sys.mjs",
]
Expand Down
1 change: 1 addition & 0 deletions firefox-ios/Client/Assets/CC_Script/Overrides.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const ModuleOverrides = {
"ContentDOMReference.sys.mjs": "Helpers.ios.mjs",
"FormAutofill.sys.mjs": "FormAutofill.ios.sys.mjs",
"EntryFile.sys.mjs": "FormAutofillChild.ios.sys.mjs",
"LoginHelper.sys.mjs": "LoginManager.shared.sys.mjs",
};

// We need this because not all webpack libraries used in iOS are ES Modules
Expand Down
229 changes: 229 additions & 0 deletions firefox-ios/Client/Assets/CC_Script/PasswordGenerator.sys.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/* 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;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

"use strict";

import { Logic } from "Assets/CC_Script/LoginManager.shared.mjs";
import "Assets/CC_Script/Helpers.ios.mjs";
import { Logic } from "Assets/CC_Script/LoginManager.shared.sys.mjs";
import { PasswordGenerator } from "resource://gre/modules/PasswordGenerator.sys.mjs";

// Ensure this module only gets included once. This is
// required for user scripts injected into all frames.
Expand Down Expand Up @@ -444,6 +446,33 @@ window.__firefox__.includeOnce("LoginsHelper", function() {
},
}

const generatePassword = (rules) => {
let mapOfRules = null;

// If the rules are not provided, we will use the default rules
// The rules are provided by swift depending on the domain
if(rules) {
issammani marked this conversation as resolved.
Show resolved Hide resolved
const domainRules = PasswordRulesParser.parsePasswordRules(
rules
);
mapOfRules = transformRulesToMap(domainRules);
}

const generatedPassword = PasswordGenerator.generatePassword({
inputMaxLength: LoginManagerContent.activeField.maxLength,
rules: mapOfRules,
});

return generatedPassword;
};

const fillGeneratedPassword = (password) => {
LoginManagerContent.yieldFocusBackToField();
LoginManagerContent.activeField.setUserInput(password);
Logic.fillConfirmFieldWithGeneratedPassword(
LoginManagerContent.activeField
);
};

function yieldFocusBackToField() {
LoginManagerContent.activeField?.blur();
Expand All @@ -463,11 +492,21 @@ window.__firefox__.includeOnce("LoginsHelper", function() {
}

const [username, password] = LoginManagerContent._getFormFields(form, false);
if (password) {
LoginManagerContent.activeField = event.target;
const field = event.target;
const formHasNewPassword =
password && Logic.isProbablyANewPasswordField(password);
const isPasswordField = field === password;

LoginManagerContent.activeField = field;
if (formHasNewPassword && isPasswordField) {
webkit.messageHandlers.loginsManagerMessageHandler.postMessage({
type: "generatePassword",
});
} else if (!formHasNewPassword && password) {
webkit.messageHandlers.loginsManagerMessageHandler.postMessage({
type: "fieldType",
fieldType: event.target === username ? FocusFieldType.username : FocusFieldType.password,
fieldType:
field === username ? FocusFieldType.username : FocusFieldType.password,
});
}
}
Expand Down Expand Up @@ -516,6 +555,8 @@ window.__firefox__.includeOnce("LoginsHelper", function() {
}
};
this.yieldFocusBackToField = yieldFocusBackToField;
this.generatePassword = generatePassword;
this.fillGeneratedPassword = fillGeneratedPassword;
}

Object.defineProperty(window.__firefox__, "logins", {
Expand Down