Skip to content
This repository has been archived by the owner on Feb 10, 2021. It is now read-only.

Commit

Permalink
Merge 7f7c701 into 50a4280
Browse files Browse the repository at this point in the history
  • Loading branch information
clamburger committed Jul 20, 2018
2 parents 50a4280 + 7f7c701 commit f6ddc95
Show file tree
Hide file tree
Showing 32 changed files with 1,202 additions and 357 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -11,6 +11,7 @@ matrix:
- php: hhvm
allow_failures:
- php: hhvm
- php: 5.5

before_script:
- composer update --prefer-dist
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -12,7 +12,8 @@
}
],
"require": {
"php": "^5.5 || ^7.0"
"php": "^5.5 || ^7.0",
"symfony/polyfill-mbstring": ">=1.3.1"
},
"require-dev": {
"phpunit/phpunit": "< 6.0",
Expand Down
8 changes: 4 additions & 4 deletions match-upstream.md
Expand Up @@ -7,21 +7,21 @@ Public API: the main public API of zxcvbn-php does not need to change with this
Planned Changes:
* Zxcvbn: I updated `passwordStrength()` to more closely mimic upstream's `main.coffee`. I arguably went too far, and might walk that back a bit.
* Feedback: added.
* [ ] Need to flesh out based on upstream. Should be pretty straightforward.
* [ ] Write tests for the feedback.
* [x] Need to flesh out based on upstream. Should be pretty straightforward.
* [x] Write tests for the feedback.
* Time estimator: added.
* [x] Just need to flesh out `displayTime()` based on upstream. Should be straightforward.
* [x] Write tests for the time estimator.
* Matchers: added.
* [x] The majority of the matchers have now been ported.
* [x] Port the tests for the matchers.
* [ ] RepeatMatch: `base_guesses` and `base_matches` are still missing, but this will require the `Scorer` to be up and running before we can implement them.
* [x] RepeatMatch: `base_guesses` and `base_matches` are still missing, but this will require the `Scorer` to be up and running before we can implement them.
* `Scorer`: This is vastly different between upstream and this port. Upstream's algorithm is complicated and hard to follow. I think this will be the hardest thing to bring up to parity. *Some* of this may be similar to `Searcher::getMinimumEntropy()` but I really can't tell.
* Some of the other language ports e.g. https://github.com/rianhunter/zxcvbn-cpp/blob/zxcvbn-cpp/native-src/zxcvbn/scoring.cpp may also be useful references when porting.
* :question: `ScorerInterface`: In upstream, `scoring.most_guessable_match_sequence` returns a hash with password/guesses/guesses_log10/sequence. Our current `ScorerInterface` has methods for `getScore()` and `getMetrics()`. Our interface is clearly "cleaner", but it might make more sense to just mirror upstream. :neutral_face:
* [x] Port or rewrite the scorer - this includes returning `guesses`, `guesses_log10` and `score`.
* [x] Write tests for the scorer.
* [ ] `Searcher`: Once we're done using it as a reference when porting `Scorer`, it should probably be deleted.
* [x] `Searcher`: Once we're done using it as a reference when porting `Scorer`, it should probably be deleted.
* [x] Data files: We have 3 files at `src/Matchers/*.json` which at least approximately correspond to their data files. Their `data/` directory has been copied verbatim, and the `data-scripts/*.py` scripts which were generating coffeescript have been modified to output JSON instead. Some of upstream's `data-scripts` are used to build `data/*.txt` files based on wikipedia/wiktionary/etc exports. Those haven't been copied over; instead, if the upstream data files change, we should recopy the data files.
* [x] `src/Matchers/adjacency_graphs.json` This is identical to upstream.
* [x] `src/Matchers/frequency_lists.json` This had different datasets, but has now been updated.
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Expand Up @@ -16,7 +16,7 @@
</testsuites>

<filter>
<whitelist>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
Expand Down
27 changes: 11 additions & 16 deletions src/Feedback.php
Expand Up @@ -2,6 +2,8 @@

namespace ZxcvbnPhp;

use ZxcvbnPhp\Matchers\Match;

/**
* Feedback - gives some user guidance based on the strength
* of a password
Expand All @@ -10,7 +12,12 @@
*/
class Feedback
{
public function getFeedback($score, $sequence)
/**
* @param int $score
* @param Match[] $sequence
* @return array
*/
public function getFeedback($score, array $sequence)
{
// starting feedback
if (count($sequence) == 0) {
Expand All @@ -34,27 +41,15 @@ public function getFeedback($score, $sequence)
// tie feedback to the longest match for longer sequences
$longestMatch = $sequence[0];
foreach (array_slice($sequence, 1) as $match) {
if (strlen($match->token) > strlen($longestMatch->token)) {
if (mb_strlen($match->token) > mb_strlen($longestMatch->token)) {
$longestMatch = $match;
}
}

$feedback = $longestMatch->getFeedback(count($sequence) == 1);
$extraFeedback = 'Add another word or two. Uncommon words are better.';

if ($feedback) {
return [
'warning' => $feedback['warning'] ?: '', // this seems unnecessary...
'suggestions' => array_merge(
[$extraFeedback],
$feedback['suggestions']
)
];
}

return [
'warning' => '',
'suggestions' => [$extraFeedback]
];
array_unshift($feedback['suggestions'], $extraFeedback);
return $feedback;
}
}
50 changes: 39 additions & 11 deletions src/Matcher.php
Expand Up @@ -31,20 +31,48 @@ public function getMatches($password, array $userInputs = [])
$matches = array_merge($matches, $matched);
}
}

self::usortStable($matches, [$this, 'compareMatches']);
return $matches;
}

/**
* @param Match $a
* @param Match $b
* A stable implementation of usort().
*
* Whether or not the sort() function in JavaScript is stable or not is implementation-defined.
* This means it's impossible for us to match all browsers exactly, but since most browsers implement sort() using
* a stable sorting algorithm, we'll get the highest rate of accuracy by using a stable sort in our code as well.
*
* This function taken from https://github.com/vanderlee/PHP-stable-sort-functions
* Copyright © 2015-2018 Martijn van der Lee (http://martijn.vanderlee.com). MIT License applies.
*
* @param array $array
* @param callable $value_compare_func
* @return bool
*/
public static function sortMatches($a, $b)
public static function usortStable(array &$array, $value_compare_func)
{
$index = 0;
foreach ($array as &$item) {
$item = array($index++, $item);
}
$result = usort($array, function ($a, $b) use ($value_compare_func) {
$result = call_user_func($value_compare_func, $a[1], $b[1]);
return $result == 0 ? $a[0] - $b[0] : $result;
});
foreach ($array as &$item) {
$item = $item[1];
}
return $result;
}

public static function compareMatches(Match $a, Match $b)
{
if ($a->begin != $b->begin) {
return $a->begin - $b->begin;
} else {
return $a->end - $b->end;
$beginDiff = $a->begin - $b->begin;
if ($beginDiff) {
return $beginDiff;
}
return $a->end - $b->end;
}

/**
Expand All @@ -57,14 +85,14 @@ protected function getMatchers()
{
// @todo change to dynamic
return [
'ZxcvbnPhp\Matchers\DateMatch',
'ZxcvbnPhp\Matchers\DictionaryMatch',
'ZxcvbnPhp\Matchers\ReverseDictionaryMatch',
'ZxcvbnPhp\Matchers\L33tMatch',
'ZxcvbnPhp\Matchers\SpatialMatch',
'ZxcvbnPhp\Matchers\RepeatMatch',
'ZxcvbnPhp\Matchers\SequenceMatch',
'ZxcvbnPhp\Matchers\SpatialMatch',
'ZxcvbnPhp\Matchers\YearMatch',
'ZxcvbnPhp\Matchers\DictionaryMatch',
'ZxcvbnPhp\Matchers\ReverseDictionaryMatch',
'ZxcvbnPhp\Matchers\DateMatch',
];
}
}
22 changes: 4 additions & 18 deletions src/Matchers/Bruteforce.php
Expand Up @@ -25,7 +25,7 @@ class Bruteforce extends Match
public static function match($password, array $userInputs = [])
{
// Matches entire string.
$match = new static($password, 0, strlen($password) - 1, $password);
$match = new static($password, 0, mb_strlen($password) - 1, $password);
return [$match];
}

Expand All @@ -39,30 +39,16 @@ public function getFeedback($isSoleMatch)
];
}

/**
* @param $password
* @param $begin
* @param $end
* @param $token
* @param $cardinality
*/
public function __construct($password, $begin, $end, $token, $cardinality = null)
{
parent::__construct($password, $begin, $end, $token);
// Cardinality can be injected to support full password cardinality instead of token.
$this->cardinality = $cardinality;
}

public function getGuesses()
public function getRawGuesses()
{
$guesses = pow(self::BRUTEFORCE_CARDINALITY, strlen($this->token));
$guesses = pow(self::BRUTEFORCE_CARDINALITY, mb_strlen($this->token));
if ($guesses === INF) {
return defined('PHP_FLOAT_MAX') ? PHP_FLOAT_MAX : 1e308;
}

// small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
// submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
if (strlen($this->token) === 1) {
if (mb_strlen($this->token) === 1) {
$minGuesses = Scorer::MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1;
} else {
$minGuesses = Scorer::MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1;
Expand Down
25 changes: 14 additions & 11 deletions src/Matchers/DateMatch.php
Expand Up @@ -2,6 +2,8 @@

namespace ZxcvbnPhp\Matchers;

use ZxcvbnPhp\Matcher;

class DateMatch extends Match
{

Expand Down Expand Up @@ -42,7 +44,7 @@ class DateMatch extends Match
],
];

const DATE_NO_SEPARATOR = '/^\d{4,8}$/';
const DATE_NO_SEPARATOR = '/^\d{4,8}$/u';

/**
* (\d{1,4}) # day, month, year
Expand All @@ -51,7 +53,7 @@ class DateMatch extends Match
* \2 # same separator
* (\d{1,4}) # day, month, year
*/
const DATE_WITH_SEPARATOR = '/^(\d{1,4})([\s\/\\\\_.-])(\d{1,2})\2(\d{1,4})$/';
const DATE_WITH_SEPARATOR = '/^(\d{1,4})([\s\/\\\\_.-])(\d{1,2})\2(\d{1,4})$/u';

/** @var int The day portion of the date in the token. */
public $day;
Expand Down Expand Up @@ -100,6 +102,7 @@ public static function match($password, array $userInputs = [])
foreach ($dates as $date) {
$matches[] = new static($password, $date['begin'], $date['end'], $date['token'], $date);
}
Matcher::usortStable($matches, [Matcher::class, 'compareMatches']);
return $matches;
}

Expand Down Expand Up @@ -138,12 +141,12 @@ public function __construct($password, $begin, $end, $token, array $params)
protected static function datesWithSeparators($password)
{
$matches = [];
$length = strlen($password);
$length = mb_strlen($password);

// dates with separators are between length 6 '1/1/91' and 10 '11/11/1991'
for ($begin = 0; $begin < $length - 5; $begin++) {
for ($end = $begin + 5; $end - $begin < 10 && $end < $length; $end++) {
$token = substr($password, $begin, $end - $begin + 1);
$token = mb_substr($password, $begin, $end - $begin + 1);

if (!preg_match(static::DATE_WITH_SEPARATOR, $token, $captures)) {
continue;
Expand Down Expand Up @@ -183,24 +186,24 @@ protected static function datesWithSeparators($password)
protected static function datesWithoutSeparators($password)
{
$matches = [];
$length = strlen($password);
$length = mb_strlen($password);

// dates without separators are between length 4 '1191' and 8 '11111991'
for ($begin = 0; $begin < $length - 3; $begin++) {
for ($end = $begin + 3; $end - $begin < 8 && $end < $length; $end++) {
$token = substr($password, $begin, $end - $begin + 1);
$token = mb_substr($password, $begin, $end - $begin + 1);

if (!preg_match(static::DATE_NO_SEPARATOR, $token)) {
continue;
}

$candidates = [];

$possibleSplits = static::$DATE_SPLITS[strlen($token)];
$possibleSplits = static::$DATE_SPLITS[mb_strlen($token)];
foreach ($possibleSplits as $splitPositions) {
$day = substr($token, 0, $splitPositions[0]);
$month = substr($token, $splitPositions[0], $splitPositions[1] - $splitPositions[0]);
$year = substr($token, $splitPositions[1]);
$day = mb_substr($token, 0, $splitPositions[0]);
$month = mb_substr($token, $splitPositions[0], $splitPositions[1] - $splitPositions[0]);
$year = mb_substr($token, $splitPositions[1]);

$date = static::checkDate([$day, $month, $year]);
if ($date !== false) {
Expand Down Expand Up @@ -402,7 +405,7 @@ protected static function removeRedundantMatches($matches)
});
}

public function getGuesses()
protected function getRawGuesses()
{
// base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years
$yearSpace = max(abs($this->year - static::getReferenceYear()), static::MIN_YEAR_SPACE);
Expand Down

0 comments on commit f6ddc95

Please sign in to comment.