Bug 1686071 - Add PasswordRulesParser module. r=dimi

Differential Revision: https://phabricator.services.mozilla.com/D114110
This commit is contained in:
Tim Giles 2021-06-09 13:54:09 +00:00
parent 72e5df1cbf
commit 0180e3b96d
4 changed files with 730 additions and 0 deletions

View File

@ -0,0 +1,699 @@
// Sourced from https://github.com/apple/password-manager-resources/blob/5f6da89483e75cdc4165a6fc4756796e0ced7a21/tools/PasswordRulesParser.js
// Copyright (c) 2019 - 2020 Apple Inc. Licensed under MIT License.
"use strict";
const EXPORTED_SYMBOLS = ["PasswordRulesParser"];
this.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;
}

View File

@ -45,6 +45,7 @@ EXTRA_JS_MODULES += [
"NewPasswordModel.jsm",
"OSCrypto.jsm",
"PasswordGenerator.jsm",
"PasswordRulesParser.jsm",
"storage-json.js",
]

View File

@ -69,6 +69,7 @@
<li><a href="about:license#apache-llvm">Apache License 2.0 with LLVM exception</a></li>
<li><a href="about:license#apple">Apple License</a></li>
<li><a href="about:license#apple-mozilla">Apple/Mozilla NPRuntime License</a></li>
<li><a href="about:license#apple-password-rules-parser">Apple Password Rules Parser License</a></li>
<li><a href="about:license#arm">ARM License</a></li>
<li><a href="about:license#babel">Babel License</a></li>
<li><a href="about:license#babylon">Babylon License</a></li>
@ -4900,6 +4901,34 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</pre>
<hr>
<h1><a id="apple-password-rules-parser"></a>Apple Password Rules Parser License</h1>
<p>This license applies to the file
<code>toolkit/component/passwordmgr/PasswordRulesParser.jsm</code>.</p>
<pre>
Copyright 2020 Apple Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</pre>
<hr>

View File

@ -166,6 +166,7 @@ third_party/
toolkit/components/certviewer/content/vendor/
toolkit/components/jsoncpp/
toolkit/components/normandy/vendor/
toolkit/components/passwordmgr/PasswordRulesParser.jsm
toolkit/components/protobuf/
toolkit/components/url-classifier/chromium/
toolkit/components/utils/mozjexl.js