File "DictionaryMatch.php"
Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/better-wp-security/vendor-prod/bjeavons/zxcvbn-php/src/Matchers/DictionaryMatch.php
File size: 8.14 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* @license MIT
*
* Modified using Strauss.
* @see https://github.com/BrianHenryIE/strauss
*/
declare(strict_types=1);
namespace iThemesSecurity\Strauss\ZxcvbnPhp\Matchers;
use JetBrains\PhpStorm\ArrayShape;
use iThemesSecurity\Strauss\ZxcvbnPhp\Matcher;
use iThemesSecurity\Strauss\ZxcvbnPhp\Math\Binomial;
class DictionaryMatch extends BaseMatch
{
public $pattern = 'dictionary';
/** @var string The name of the dictionary that the token was found in. */
public $dictionaryName;
/** @var int The rank of the token in the dictionary. */
public $rank;
/** @var string The word that was matched from the dictionary. */
public $matchedWord;
/** @var bool Whether or not the matched word was reversed in the token. */
public $reversed = false;
/** @var bool Whether or not the token contained l33t substitutions. */
public $l33t = false;
/** @var array A cache of the frequency_lists json file */
protected static $rankedDictionaries = [];
protected const START_UPPER = "/^[A-Z][^A-Z]+$/u";
protected const END_UPPER = "/^[^A-Z]+[A-Z]$/u";
protected const ALL_UPPER = "/^[^a-z]+$/u";
protected const ALL_LOWER = "/^[^A-Z]+$/u";
/**
* Match occurrences of dictionary words in password.
*
* @param string $password
* @param array $userInputs
* @param array $rankedDictionaries
* @return DictionaryMatch[]
*/
public static function match(string $password, array $userInputs = [], array $rankedDictionaries = []): array
{
$matches = [];
if ($rankedDictionaries) {
$dicts = $rankedDictionaries;
} else {
$dicts = static::getRankedDictionaries();
}
if (!empty($userInputs)) {
$dicts['user_inputs'] = [];
foreach ($userInputs as $rank => $input) {
$input_lower = mb_strtolower($input);
$dicts['user_inputs'][$input_lower] = $rank + 1; // rank starts at 1, not 0
}
}
foreach ($dicts as $name => $dict) {
$results = static::dictionaryMatch($password, $dict);
foreach ($results as $result) {
$result['dictionary_name'] = $name;
$matches[] = new static($password, $result['begin'], $result['end'], $result['token'], $result);
}
}
Matcher::usortStable($matches, [Matcher::class, 'compareMatches']);
return $matches;
}
/**
* @param string $password
* @param int $begin
* @param int $end
* @param string $token
* @param array $params An array with keys: [dictionary_name, matched_word, rank].
*/
public function __construct(string $password, int $begin, int $end, string $token, array $params = [])
{
parent::__construct($password, $begin, $end, $token);
if (!empty($params)) {
$this->dictionaryName = $params['dictionary_name'] ?? '';
$this->matchedWord = $params['matched_word'] ?? '';
$this->rank = $params['rank'] ?? 0;
}
}
/**
* @param bool $isSoleMatch
* @return array
*/
#[ArrayShape(['warning' => 'string', 'suggestions' => 'string[]'])]
public function getFeedback(bool $isSoleMatch): array
{
$startUpper = '/^[A-Z][^A-Z]+$/u';
$allUpper = '/^[^a-z]+$/u';
$feedback = [
'warning' => $this->getFeedbackWarning($isSoleMatch),
'suggestions' => []
];
if (preg_match($startUpper, $this->token)) {
$feedback['suggestions'][] = "Capitalization doesn't help very much";
} elseif (preg_match($allUpper, $this->token) && mb_strtolower($this->token) != $this->token) {
$feedback['suggestions'][] = "All-uppercase is almost as easy to guess as all-lowercase";
}
return $feedback;
}
public function getFeedbackWarning(bool $isSoleMatch): string
{
switch ($this->dictionaryName) {
case 'passwords':
if ($isSoleMatch && !$this->l33t && !$this->reversed) {
if ($this->rank <= 10) {
return 'This is a top-10 common password';
} elseif ($this->rank <= 100) {
return 'This is a top-100 common password';
} else {
return 'This is a very common password';
}
} elseif ($this->getGuessesLog10() <= 4) {
return 'This is similar to a commonly used password';
}
break;
case 'english_wikipedia':
if ($isSoleMatch) {
return 'A word by itself is easy to guess';
}
break;
case 'surnames':
case 'male_names':
case 'female_names':
if ($isSoleMatch) {
return 'Names and surnames by themselves are easy to guess';
} else {
return 'Common names and surnames are easy to guess';
}
}
return '';
}
/**
* Attempts to find the provided password (as well as all possible substrings) in a dictionary.
*
* @param string $password
* @param array $dict
* @return array
*/
protected static function dictionaryMatch(string $password, array $dict): array
{
$result = [];
$length = mb_strlen($password);
$pw_lower = mb_strtolower($password);
foreach (range(0, $length - 1) as $i) {
foreach (range($i, $length - 1) as $j) {
$word = mb_substr($pw_lower, $i, $j - $i + 1);
if (isset($dict[$word])) {
$result[] = [
'begin' => $i,
'end' => $j,
'token' => mb_substr($password, $i, $j - $i + 1),
'matched_word' => $word,
'rank' => $dict[$word],
];
}
}
}
return $result;
}
/**
* Load ranked frequency dictionaries.
*
* @return array
*/
protected static function getRankedDictionaries(): array
{
if (empty(self::$rankedDictionaries)) {
$json = file_get_contents(dirname(__FILE__) . '/frequency_lists.json');
$data = json_decode($json, true);
$rankedLists = [];
foreach ($data as $name => $words) {
$rankedLists[$name] = array_combine($words, range(1, count($words)));
}
self::$rankedDictionaries = $rankedLists;
}
return self::$rankedDictionaries;
}
protected function getRawGuesses(): float
{
$guesses = $this->rank;
$guesses *= $this->getUppercaseVariations();
return $guesses;
}
protected function getUppercaseVariations(): float
{
$word = $this->token;
if (preg_match(self::ALL_LOWER, $word) || mb_strtolower($word) === $word) {
return 1;
}
// a capitalized word is the most common capitalization scheme,
// so it only doubles the search space (uncapitalized + capitalized).
// allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe.
foreach (array(self::START_UPPER, self::END_UPPER, self::ALL_UPPER) as $regex) {
if (preg_match($regex, $word)) {
return 2;
}
}
// otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters
// with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD),
// the number of ways to lowercase U+L letters with L lowercase letters or less.
$uppercase = count(array_filter(preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY), 'ctype_upper'));
$lowercase = count(array_filter(preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY), 'ctype_lower'));
$variations = 0;
for ($i = 1; $i <= min($uppercase, $lowercase); $i++) {
$variations += Binomial::binom($uppercase + $lowercase, $i);
}
return $variations;
}
}