Added DictonaryMatch

This commit is contained in:
Netkas 2021-09-16 19:16:25 -04:00
parent 5f8817dd44
commit d4a939830d
3 changed files with 305 additions and 0 deletions

View File

@ -0,0 +1,290 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace Zxcvbn\Classes\Matchers;
use Zxcvbn\Abstracts\BaseMatch;
use Zxcvbn\Classes\Matcher;
use Zxcvbn\Classes\Utilities;
class DictionaryMatch extends BaseMatch
{
public $pattern = 'dictionary';
/**
* The name of the dictionary that the token was found in.
* @var string
*/
public $dictionaryName;
/**
* The rank of the token in the dictionary.
* @var int
*/
public $rank;
/**
* The word that was matched from the dictionary.
* @var string
*/
public $matchedWord;
/**
* Whether the matched word was reversed in the token.
* @var bool
*/
public $reversed = false;
/**
* Whether the token contained l33t substitutions.
* @var bool
*/
public $l33t = false;
/**
* A cache of the frequency_lists json file
* @var array
*/
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);
}
}
Utilities::usort($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($password, $begin, $end, $token, array $params = [])
{
parent::__construct($password, $begin, $end, $token);
if (!empty($params)) {
$this->dictionaryName = $params['dictionary_name'] ?? null;
$this->matchedWord = $params['matched_word'] ?? null;
$this->rank = $params['rank'] ?? null;
}
}
/**
* @param $isSoleMatch
* @return array
*/
public function getFeedback($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;
}
/**
* @param $isSoleMatch
* @return string
*/
public function getFeedbackWarning($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;
}
/**
* @return integer
*/
protected function getRawGuesses(): ?int
{
$guesses = $this->rank;
$guesses *= $this->getUppercaseVariations();
return $guesses;
}
/**
* @return integer
*/
protected function getUppercaseVariations(): int
{
$word = $this->token;
if (preg_match(self::ALL_LOWER, $word) || mb_strtolower($word) === $word)
{
return 1;
}
foreach (array(self::START_UPPER, self::END_UPPER, self::ALL_UPPER) as $regex)
{
if (preg_match($regex, $word))
{
return 2;
}
}
$uppercase = count(array_filter(preg_split('//u', $word, null, PREG_SPLIT_NO_EMPTY), 'ctype_upper'));
$lowercase = count(array_filter(preg_split('//u', $word, null, PREG_SPLIT_NO_EMPTY), 'ctype_lower'));
$variations = 0;
for ($i = 1; $i <= min($uppercase, $lowercase); $i++)
{
$variations += static::binom($uppercase + $lowercase, $i);
}
return $variations;
}
}

View File

@ -33,4 +33,15 @@
return $result;
}
/**
* Gets the file data path
*
* @param string $name
* @return string
*/
public static function getDataFilePath(string $name): string
{
return __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . $name;
}
}

View File

@ -40,6 +40,10 @@
"required": true,
"file": "Classes/Matchers/DateMatch.php"
},
{
"required": true,
"file": "Classes/Matchers/DictionaryMatch.php"
},
{
"required": true,
"file": "Classes/Scorer.php"