summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/PasswordRulesParser.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/PasswordRulesParser.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/PasswordRulesParser.sys.mjs695
1 files changed, 695 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/PasswordRulesParser.sys.mjs b/toolkit/components/passwordmgr/PasswordRulesParser.sys.mjs
new file mode 100644
index 0000000000..8ce7cba990
--- /dev/null
+++ b/toolkit/components/passwordmgr/PasswordRulesParser.sys.mjs
@@ -0,0 +1,695 @@
+// Sourced from https://github.com/apple/password-manager-resources/blob/5f6da89483e75cdc4165a6fc4756796e0ced7a21/tools/PasswordRulesParser.js
+// Copyright (c) 2019 - 2020 Apple Inc. Licensed under MIT License.
+
+export const PasswordRulesParser = {
+ parsePasswordRules,
+};
+
+const Identifier = {
+ ASCII_PRINTABLE: "ascii-printable",
+ DIGIT: "digit",
+ LOWER: "lower",
+ SPECIAL: "special",
+ UNICODE: "unicode",
+ UPPER: "upper",
+};
+
+const RuleName = {
+ ALLOWED: "allowed",
+ MAX_CONSECUTIVE: "max-consecutive",
+ REQUIRED: "required",
+ MIN_LENGTH: "minlength",
+ MAX_LENGTH: "maxlength",
+};
+
+const CHARACTER_CLASS_START_SENTINEL = "[";
+const CHARACTER_CLASS_END_SENTINEL = "]";
+const PROPERTY_VALUE_SEPARATOR = ",";
+const PROPERTY_SEPARATOR = ";";
+const PROPERTY_VALUE_START_SENTINEL = ":";
+
+const SPACE_CODE_POINT = " ".codePointAt(0);
+
+const SHOULD_NOT_BE_REACHED = "Should not be reached";
+
+class Rule {
+ constructor(name, value) {
+ this._name = name;
+ this.value = value;
+ }
+ get name() {
+ return this._name;
+ }
+ toString() {
+ return JSON.stringify(this);
+ }
+}
+
+class NamedCharacterClass {
+ constructor(name) {
+ console.assert(_isValidRequiredOrAllowedPropertyValueIdentifier(name));
+ this._name = name;
+ }
+ get name() {
+ return this._name.toLowerCase();
+ }
+ toString() {
+ return this._name;
+ }
+ toHTMLString() {
+ return this._name;
+ }
+}
+
+class CustomCharacterClass {
+ constructor(characters) {
+ console.assert(characters instanceof Array);
+ this._characters = characters;
+ }
+ get characters() {
+ return this._characters;
+ }
+ toString() {
+ return `[${this._characters.join("")}]`;
+ }
+ toHTMLString() {
+ return `[${this._characters.join("").replace('"', """)}]`;
+ }
+}
+
+// MARK: Lexer functions
+
+function _isIdentifierCharacter(c) {
+ console.assert(c.length === 1);
+ return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "-";
+}
+
+function _isASCIIDigit(c) {
+ console.assert(c.length === 1);
+ return c >= "0" && c <= "9";
+}
+
+function _isASCIIPrintableCharacter(c) {
+ console.assert(c.length === 1);
+ return c >= " " && c <= "~";
+}
+
+function _isASCIIWhitespace(c) {
+ console.assert(c.length === 1);
+ return c === " " || c === "\f" || c === "\n" || c === "\r" || c === "\t";
+}
+
+// MARK: ASCII printable character bit set and canonicalization functions
+
+function _bitSetIndexForCharacter(c) {
+ console.assert(c.length == 1);
+ return c.codePointAt(0) - SPACE_CODE_POINT;
+}
+
+function _characterAtBitSetIndex(index) {
+ return String.fromCodePoint(index + SPACE_CODE_POINT);
+}
+
+function _markBitsForNamedCharacterClass(bitSet, namedCharacterClass) {
+ console.assert(bitSet instanceof Array);
+ console.assert(namedCharacterClass.name !== Identifier.UNICODE);
+ console.assert(namedCharacterClass.name !== Identifier.ASCII_PRINTABLE);
+ if (namedCharacterClass.name === Identifier.UPPER) {
+ bitSet.fill(
+ true,
+ _bitSetIndexForCharacter("A"),
+ _bitSetIndexForCharacter("Z") + 1
+ );
+ } else if (namedCharacterClass.name === Identifier.LOWER) {
+ bitSet.fill(
+ true,
+ _bitSetIndexForCharacter("a"),
+ _bitSetIndexForCharacter("z") + 1
+ );
+ } else if (namedCharacterClass.name === Identifier.DIGIT) {
+ bitSet.fill(
+ true,
+ _bitSetIndexForCharacter("0"),
+ _bitSetIndexForCharacter("9") + 1
+ );
+ } else if (namedCharacterClass.name === Identifier.SPECIAL) {
+ bitSet.fill(
+ true,
+ _bitSetIndexForCharacter(" "),
+ _bitSetIndexForCharacter("/") + 1
+ );
+ bitSet.fill(
+ true,
+ _bitSetIndexForCharacter(":"),
+ _bitSetIndexForCharacter("@") + 1
+ );
+ bitSet.fill(
+ true,
+ _bitSetIndexForCharacter("["),
+ _bitSetIndexForCharacter("`") + 1
+ );
+ bitSet.fill(
+ true,
+ _bitSetIndexForCharacter("{"),
+ _bitSetIndexForCharacter("~") + 1
+ );
+ } else {
+ console.assert(false, SHOULD_NOT_BE_REACHED, namedCharacterClass);
+ }
+}
+
+function _markBitsForCustomCharacterClass(bitSet, customCharacterClass) {
+ for (let character of customCharacterClass.characters) {
+ bitSet[_bitSetIndexForCharacter(character)] = true;
+ }
+}
+
+function _canonicalizedPropertyValues(
+ propertyValues,
+ keepCustomCharacterClassFormatCompliant
+) {
+ let asciiPrintableBitSet = new Array(
+ "~".codePointAt(0) - " ".codePointAt(0) + 1
+ );
+
+ for (let propertyValue of propertyValues) {
+ if (propertyValue instanceof NamedCharacterClass) {
+ if (propertyValue.name === Identifier.UNICODE) {
+ return [new NamedCharacterClass(Identifier.UNICODE)];
+ }
+
+ if (propertyValue.name === Identifier.ASCII_PRINTABLE) {
+ return [new NamedCharacterClass(Identifier.ASCII_PRINTABLE)];
+ }
+
+ _markBitsForNamedCharacterClass(asciiPrintableBitSet, propertyValue);
+ } else if (propertyValue instanceof CustomCharacterClass) {
+ _markBitsForCustomCharacterClass(asciiPrintableBitSet, propertyValue);
+ }
+ }
+
+ let charactersSeen = [];
+
+ function checkRange(start, end) {
+ let temp = [];
+ for (
+ let i = _bitSetIndexForCharacter(start);
+ i <= _bitSetIndexForCharacter(end);
+ ++i
+ ) {
+ if (asciiPrintableBitSet[i]) {
+ temp.push(_characterAtBitSetIndex(i));
+ }
+ }
+
+ let result =
+ temp.length ===
+ _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1;
+ if (!result) {
+ charactersSeen = charactersSeen.concat(temp);
+ }
+ return result;
+ }
+
+ let hasAllUpper = checkRange("A", "Z");
+ let hasAllLower = checkRange("a", "z");
+ let hasAllDigits = checkRange("0", "9");
+
+ // Check for special characters, accounting for characters that are given special treatment (i.e. '-' and ']')
+ let hasAllSpecial = false;
+ let hasDash = false;
+ let hasRightSquareBracket = false;
+ let temp = [];
+ for (
+ let i = _bitSetIndexForCharacter(" ");
+ i <= _bitSetIndexForCharacter("/");
+ ++i
+ ) {
+ if (!asciiPrintableBitSet[i]) {
+ continue;
+ }
+
+ let character = _characterAtBitSetIndex(i);
+ if (keepCustomCharacterClassFormatCompliant && character === "-") {
+ hasDash = true;
+ } else {
+ temp.push(character);
+ }
+ }
+ for (
+ let i = _bitSetIndexForCharacter(":");
+ i <= _bitSetIndexForCharacter("@");
+ ++i
+ ) {
+ if (asciiPrintableBitSet[i]) {
+ temp.push(_characterAtBitSetIndex(i));
+ }
+ }
+ for (
+ let i = _bitSetIndexForCharacter("[");
+ i <= _bitSetIndexForCharacter("`");
+ ++i
+ ) {
+ if (!asciiPrintableBitSet[i]) {
+ continue;
+ }
+
+ let character = _characterAtBitSetIndex(i);
+ if (keepCustomCharacterClassFormatCompliant && character === "]") {
+ hasRightSquareBracket = true;
+ } else {
+ temp.push(character);
+ }
+ }
+ for (
+ let i = _bitSetIndexForCharacter("{");
+ i <= _bitSetIndexForCharacter("~");
+ ++i
+ ) {
+ if (asciiPrintableBitSet[i]) {
+ temp.push(_characterAtBitSetIndex(i));
+ }
+ }
+
+ if (hasDash) {
+ temp.unshift("-");
+ }
+ if (hasRightSquareBracket) {
+ temp.push("]");
+ }
+
+ let numberOfSpecialCharacters =
+ _bitSetIndexForCharacter("/") -
+ _bitSetIndexForCharacter(" ") +
+ 1 +
+ (_bitSetIndexForCharacter("@") - _bitSetIndexForCharacter(":") + 1) +
+ (_bitSetIndexForCharacter("`") - _bitSetIndexForCharacter("[") + 1) +
+ (_bitSetIndexForCharacter("~") - _bitSetIndexForCharacter("{") + 1);
+ hasAllSpecial = temp.length === numberOfSpecialCharacters;
+ if (!hasAllSpecial) {
+ charactersSeen = charactersSeen.concat(temp);
+ }
+
+ let result = [];
+ if (hasAllUpper && hasAllLower && hasAllDigits && hasAllSpecial) {
+ return [new NamedCharacterClass(Identifier.ASCII_PRINTABLE)];
+ }
+ if (hasAllUpper) {
+ result.push(new NamedCharacterClass(Identifier.UPPER));
+ }
+ if (hasAllLower) {
+ result.push(new NamedCharacterClass(Identifier.LOWER));
+ }
+ if (hasAllDigits) {
+ result.push(new NamedCharacterClass(Identifier.DIGIT));
+ }
+ if (hasAllSpecial) {
+ result.push(new NamedCharacterClass(Identifier.SPECIAL));
+ }
+ if (charactersSeen.length) {
+ result.push(new CustomCharacterClass(charactersSeen));
+ }
+ return result;
+}
+
+// MARK: Parser functions
+
+function _indexOfNonWhitespaceCharacter(input, position = 0) {
+ console.assert(position >= 0);
+ console.assert(position <= input.length);
+
+ let length = input.length;
+ while (position < length && _isASCIIWhitespace(input[position])) {
+ ++position;
+ }
+
+ return position;
+}
+
+function _parseIdentifier(input, position) {
+ console.assert(position >= 0);
+ console.assert(position < input.length);
+ console.assert(_isIdentifierCharacter(input[position]));
+
+ let length = input.length;
+ let seenIdentifiers = [];
+ do {
+ let c = input[position];
+ if (!_isIdentifierCharacter(c)) {
+ break;
+ }
+
+ seenIdentifiers.push(c);
+ ++position;
+ } while (position < length);
+
+ return [seenIdentifiers.join(""), position];
+}
+
+function _isValidRequiredOrAllowedPropertyValueIdentifier(identifier) {
+ return (
+ identifier && Object.values(Identifier).includes(identifier.toLowerCase())
+ );
+}
+
+function _parseCustomCharacterClass(input, position) {
+ console.assert(position >= 0);
+ console.assert(position < input.length);
+ console.assert(input[position] === CHARACTER_CLASS_START_SENTINEL);
+
+ let length = input.length;
+ ++position;
+ if (position >= length) {
+ console.error("Found end-of-line instead of character class character");
+ return [null, position];
+ }
+
+ let initialPosition = position;
+ let result = [];
+ do {
+ let c = input[position];
+ if (!_isASCIIPrintableCharacter(c)) {
+ ++position;
+ continue;
+ }
+
+ if (c === "-" && position - initialPosition > 0) {
+ // FIXME: Should this be an error?
+ console.warn(
+ "Ignoring '-'; a '-' may only appear as the first character in a character class"
+ );
+ ++position;
+ continue;
+ }
+
+ result.push(c);
+ ++position;
+ if (c === CHARACTER_CLASS_END_SENTINEL) {
+ break;
+ }
+ } while (position < length);
+
+ if (
+ (position < length && input[position] !== CHARACTER_CLASS_END_SENTINEL) ||
+ (position == length && input[position - 1] == CHARACTER_CLASS_END_SENTINEL)
+ ) {
+ // Fix up result; we over consumed.
+ result.pop();
+ return [result, position];
+ }
+
+ if (position < length && input[position] == CHARACTER_CLASS_END_SENTINEL) {
+ return [result, position + 1];
+ }
+
+ console.error("Found end-of-line instead of end of character class");
+ return [null, position];
+}
+
+function _parsePasswordRequiredOrAllowedPropertyValue(input, position) {
+ console.assert(position >= 0);
+ console.assert(position < input.length);
+
+ let length = input.length;
+ let propertyValues = [];
+ while (true) {
+ if (_isIdentifierCharacter(input[position])) {
+ let identifierStartPosition = position;
+ var [propertyValue, position] = _parseIdentifier(input, position);
+ if (!_isValidRequiredOrAllowedPropertyValueIdentifier(propertyValue)) {
+ console.error(
+ "Unrecognized property value identifier: " + propertyValue
+ );
+ return [null, identifierStartPosition];
+ }
+ propertyValues.push(new NamedCharacterClass(propertyValue));
+ } else if (input[position] == CHARACTER_CLASS_START_SENTINEL) {
+ var [propertyValue, position] = _parseCustomCharacterClass(
+ input,
+ position
+ );
+ if (propertyValue && propertyValue.length) {
+ propertyValues.push(new CustomCharacterClass(propertyValue));
+ }
+ } else {
+ console.error(
+ "Failed to find start of property value: " + input.substr(position)
+ );
+ return [null, position];
+ }
+
+ position = _indexOfNonWhitespaceCharacter(input, position);
+ if (position >= length || input[position] === PROPERTY_SEPARATOR) {
+ break;
+ }
+
+ if (input[position] === PROPERTY_VALUE_SEPARATOR) {
+ position = _indexOfNonWhitespaceCharacter(input, position + 1);
+ if (position >= length) {
+ console.error(
+ "Found end-of-line instead of start of next property value"
+ );
+ return [null, position];
+ }
+ continue;
+ }
+
+ console.error(
+ "Failed to find start of next property or property value: " +
+ input.substr(position)
+ );
+ return [null, position];
+ }
+ return [propertyValues, position];
+}
+
+function _parsePasswordRule(input, position) {
+ console.assert(position >= 0);
+ console.assert(position < input.length);
+ console.assert(_isIdentifierCharacter(input[position]));
+
+ let length = input.length;
+
+ let mayBeIdentifierStartPosition = position;
+ var [identifier, position] = _parseIdentifier(input, position);
+ if (!Object.values(RuleName).includes(identifier)) {
+ console.error("Unrecognized property name: " + identifier);
+ return [null, mayBeIdentifierStartPosition];
+ }
+
+ if (position >= length) {
+ console.error("Found end-of-line instead of start of property value");
+ return [null, position];
+ }
+
+ if (input[position] !== PROPERTY_VALUE_START_SENTINEL) {
+ console.error(
+ "Failed to find start of property value: " + input.substr(position)
+ );
+ return [null, position];
+ }
+
+ let property = { name: identifier, value: null };
+
+ position = _indexOfNonWhitespaceCharacter(input, position + 1);
+ // Empty value
+ if (position >= length || input[position] === PROPERTY_SEPARATOR) {
+ return [new Rule(property.name, property.value), position];
+ }
+
+ switch (identifier) {
+ case RuleName.ALLOWED:
+ case RuleName.REQUIRED: {
+ var [
+ propertyValue,
+ position,
+ ] = _parsePasswordRequiredOrAllowedPropertyValue(input, position);
+ if (propertyValue) {
+ property.value = propertyValue;
+ }
+ return [new Rule(property.name, property.value), position];
+ }
+ case RuleName.MAX_CONSECUTIVE: {
+ var [propertyValue, position] = _parseMaxConsecutivePropertyValue(
+ input,
+ position
+ );
+ if (propertyValue) {
+ property.value = propertyValue;
+ }
+ return [new Rule(property.name, property.value), position];
+ }
+ case RuleName.MIN_LENGTH:
+ case RuleName.MAX_LENGTH: {
+ var [propertyValue, position] = _parseMinLengthMaxLengthPropertyValue(
+ input,
+ position
+ );
+ if (propertyValue) {
+ property.value = propertyValue;
+ }
+ return [new Rule(property.name, property.value), position];
+ }
+ }
+ console.assert(false, SHOULD_NOT_BE_REACHED);
+}
+
+function _parseMinLengthMaxLengthPropertyValue(input, position) {
+ return _parseInteger(input, position);
+}
+
+function _parseMaxConsecutivePropertyValue(input, position) {
+ return _parseInteger(input, position);
+}
+
+function _parseInteger(input, position) {
+ console.assert(position >= 0);
+ console.assert(position < input.length);
+
+ if (!_isASCIIDigit(input[position])) {
+ console.error(
+ "Failed to parse value of type integer; not a number: " +
+ input.substr(position)
+ );
+ return [null, position];
+ }
+
+ let length = input.length;
+ let initialPosition = position;
+ let result = 0;
+ do {
+ result = 10 * result + parseInt(input[position], 10);
+ ++position;
+ } while (
+ position < length &&
+ input[position] !== PROPERTY_SEPARATOR &&
+ _isASCIIDigit(input[position])
+ );
+
+ if (position >= length || input[position] === PROPERTY_SEPARATOR) {
+ return [result, position];
+ }
+
+ console.error(
+ "Failed to parse value of type integer; not a number: " +
+ input.substr(initialPosition)
+ );
+ return [null, position];
+}
+
+function _parsePasswordRulesInternal(input) {
+ let parsedProperties = [];
+ let length = input.length;
+
+ var position = _indexOfNonWhitespaceCharacter(input);
+ while (position < length) {
+ if (!_isIdentifierCharacter(input[position])) {
+ console.warn(
+ "Failed to find start of property: " + input.substr(position)
+ );
+ return parsedProperties;
+ }
+
+ var [parsedProperty, position] = _parsePasswordRule(input, position);
+ if (parsedProperty && parsedProperty.value) {
+ parsedProperties.push(parsedProperty);
+ }
+
+ position = _indexOfNonWhitespaceCharacter(input, position);
+ if (position >= length) {
+ break;
+ }
+
+ if (input[position] === PROPERTY_SEPARATOR) {
+ position = _indexOfNonWhitespaceCharacter(input, position + 1);
+ if (position >= length) {
+ return parsedProperties;
+ }
+
+ continue;
+ }
+
+ console.error(
+ "Failed to find start of next property: " + input.substr(position)
+ );
+ return null;
+ }
+
+ return parsedProperties;
+}
+
+function parsePasswordRules(input, formatRulesForMinifiedVersion) {
+ let passwordRules = _parsePasswordRulesInternal(input) || [];
+
+ // When formatting rules for minified version, we should keep the formatted rules
+ // as similar to the input as possible. Avoid copying required rules to allowed rules.
+ let suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion;
+
+ let newPasswordRules = [];
+ let newAllowedValues = [];
+ let minimumMaximumConsecutiveCharacters = null;
+ let maximumMinLength = 0;
+ let minimumMaxLength = null;
+
+ for (let rule of passwordRules) {
+ switch (rule.name) {
+ case RuleName.MAX_CONSECUTIVE:
+ minimumMaximumConsecutiveCharacters = minimumMaximumConsecutiveCharacters
+ ? Math.min(rule.value, minimumMaximumConsecutiveCharacters)
+ : rule.value;
+ break;
+
+ case RuleName.MIN_LENGTH:
+ maximumMinLength = Math.max(rule.value, maximumMinLength);
+ break;
+
+ case RuleName.MAX_LENGTH:
+ minimumMaxLength = minimumMaxLength
+ ? Math.min(rule.value, minimumMaxLength)
+ : rule.value;
+ break;
+
+ case RuleName.REQUIRED:
+ rule.value = _canonicalizedPropertyValues(
+ rule.value,
+ formatRulesForMinifiedVersion
+ );
+ newPasswordRules.push(rule);
+ if (!suppressCopyingRequiredToAllowed) {
+ newAllowedValues = newAllowedValues.concat(rule.value);
+ }
+ break;
+
+ case RuleName.ALLOWED:
+ newAllowedValues = newAllowedValues.concat(rule.value);
+ break;
+ }
+ }
+
+ newAllowedValues = _canonicalizedPropertyValues(
+ newAllowedValues,
+ suppressCopyingRequiredToAllowed
+ );
+ if (!suppressCopyingRequiredToAllowed && !newAllowedValues.length) {
+ newAllowedValues = [new NamedCharacterClass(Identifier.ASCII_PRINTABLE)];
+ }
+ if (newAllowedValues.length) {
+ newPasswordRules.push(new Rule(RuleName.ALLOWED, newAllowedValues));
+ }
+
+ if (minimumMaximumConsecutiveCharacters !== null) {
+ newPasswordRules.push(
+ new Rule(RuleName.MAX_CONSECUTIVE, minimumMaximumConsecutiveCharacters)
+ );
+ }
+
+ if (maximumMinLength > 0) {
+ newPasswordRules.push(new Rule(RuleName.MIN_LENGTH, maximumMinLength));
+ }
+
+ if (minimumMaxLength !== null) {
+ newPasswordRules.push(new Rule(RuleName.MAX_LENGTH, minimumMaxLength));
+ }
+
+ return newPasswordRules;
+}