Skip to content

Commit

Permalink
Merge branch 'AlexLisenkov-add-custom-matcher'
Browse files Browse the repository at this point in the history
  • Loading branch information
bjeavons committed Apr 29, 2020
2 parents 7153931 + f2f91f3 commit dde9679
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 41 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"symfony/polyfill-mbstring": ">=1.3.1"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"phpunit/phpunit": "^7.0",
"php-coveralls/php-coveralls": "*",
"squizlabs/php_codesniffer": "3.*"
},
Expand Down
7 changes: 3 additions & 4 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="test/config/bootstrap.php">

<testsuites>
Expand All @@ -22,9 +21,9 @@
</filter>

<logging>
<log type="coverage-html" target="build/coverage" charset="UTF-8"/>
<log type="coverage-clover" target="build/logs/clover.xml" charset="UTF-8"/>
<log type="junit" target="build/logs/junit.xml" logIncompleteSkipped="false"/>
<log type="coverage-html" target="build/coverage"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
<log type="junit" target="build/logs/junit.xml"/>
</logging>

</phpunit>
4 changes: 2 additions & 2 deletions src/Feedback.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Feedback
public function getFeedback($score, array $sequence)
{
// starting feedback
if (count($sequence) == 0) {
if (count($sequence) === 0) {
return [
'warning' => '',
'suggestions' => [
Expand All @@ -46,7 +46,7 @@ public function getFeedback($score, array $sequence)
}
}

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

array_unshift($feedback['suggestions'], $extraFeedback);
Expand Down
48 changes: 34 additions & 14 deletions src/Matcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,23 @@
namespace ZxcvbnPhp;

use ZxcvbnPhp\Matchers\Match;
use ZxcvbnPhp\Matchers\MatchInterface;

class Matcher
{
private const DEFAULT_MATCHERS = [
Matchers\DateMatch::class,
Matchers\DictionaryMatch::class,
Matchers\ReverseDictionaryMatch::class,
Matchers\L33tMatch::class,
Matchers\RepeatMatch::class,
Matchers\SequenceMatch::class,
Matchers\SpatialMatch::class,
Matchers\YearMatch::class,
];

private $additionalMatchers = [];

/**
* Get matches for a password.
*
Expand All @@ -24,14 +38,27 @@ public function getMatches($password, array $userInputs = [])
foreach ($this->getMatchers() as $matcher) {
$matched = $matcher::match($password, $userInputs);
if (is_array($matched) && !empty($matched)) {
$matches = array_merge($matches, $matched);
$matches[] = $matched;
}
}

$matches = array_merge([], ...$matches);
self::usortStable($matches, [$this, 'compareMatches']);

return $matches;
}

public function addMatcher(string $className)
{
if (!is_a($className, MatchInterface::class, true)) {
throw new \InvalidArgumentException(sprintf('Matcher class must implement %s', MatchInterface::class));
}

$this->additionalMatchers[$className] = $className;

return $this;
}

/**
* A stable implementation of usort().
*
Expand All @@ -46,14 +73,14 @@ public function getMatches($password, array $userInputs = [])
* @param callable $value_compare_func
* @return bool
*/
public static function usortStable(array &$array, $value_compare_func)
public static function usortStable(array &$array, callable $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]);
$result = $value_compare_func($a[1], $b[1]);
return $result == 0 ? $a[0] - $b[0] : $result;
});
foreach ($array as &$item) {
Expand All @@ -78,16 +105,9 @@ public static function compareMatches(Match $a, Match $b)
*/
protected function getMatchers()
{
// @todo change to dynamic
return [
Matchers\DateMatch::class,
Matchers\DictionaryMatch::class,
Matchers\ReverseDictionaryMatch::class,
Matchers\L33tMatch::class,
Matchers\RepeatMatch::class,
Matchers\SequenceMatch::class,
Matchers\SpatialMatch::class,
Matchers\YearMatch::class,
];
return array_merge(
self::DEFAULT_MATCHERS,
array_values($this->additionalMatchers)
);
}
}
12 changes: 7 additions & 5 deletions src/Matchers/DateMatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class DateMatch extends Match
public const MAX_YEAR = 2050;

public const MIN_YEAR_SPACE = 20;

public $pattern = 'date';

private static $DATE_SPLITS = [
Expand Down Expand Up @@ -369,13 +369,15 @@ protected static function twoToFourDigitYear($year)
{
if ($year > 99) {
return $year;
} elseif ($year > 50) {
}

if ($year > 50) {
// 87 -> 1987
return $year + 1900;
} else {
// 15 -> 2015
return $year + 2000;
}

// 15 -> 2015
return $year + 2000;
}

/**
Expand Down
48 changes: 33 additions & 15 deletions src/TimeEstimator.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,26 @@ protected function guessesToScore($guesses)
if ($guesses < 1e3 + $DELTA) {
# risky password: "too guessable"
return 0;
} elseif ($guesses < 1e6 + $DELTA) {
}

if ($guesses < 1e6 + $DELTA) {
# modest protection from throttled online attacks: "very guessable"
return 1;
} elseif ($guesses < 1e8 + $DELTA) {
}

if ($guesses < 1e8 + $DELTA) {
# modest protection from unthrottled online attacks: "somewhat guessable"
return 2;
} elseif ($guesses < 1e10 + $DELTA) {
}

if ($guesses < 1e10 + $DELTA) {
# modest protection from offline attacks: "safely unguessable"
# assuming a salted, slow hash function like bcrypt, scrypt, PBKDF2, argon, etc
return 3;
} else {
# strong protection from offline attacks under same scenario: "very unguessable"
return 4;
}

# strong protection from offline attacks under same scenario: "very unguessable"
return 4;
}

protected function displayTime($seconds)
Expand All @@ -66,33 +72,45 @@ protected function displayTime($seconds)

if ($seconds < 1) {
return [null, 'less than a second'];
} elseif ($seconds < $minute) {
}

if ($seconds < $minute) {
$base = round($seconds);
return [$base, "$base second"];
} elseif ($seconds < $hour) {
}

if ($seconds < $hour) {
$base = round($seconds / $minute);
return [$base, "$base minute"];
} elseif ($seconds < $day) {
}

if ($seconds < $day) {
$base = round($seconds / $hour);
return [$base, "$base hour"];
} elseif ($seconds < $month) {
}

if ($seconds < $month) {
$base = round($seconds / $day);
return [$base, "$base day"];
} elseif ($seconds < $year) {
}

if ($seconds < $year) {
$base = round($seconds / $month);
return [$base, "$base month"];
} elseif ($seconds < $century) {
}

if ($seconds < $century) {
$base = round($seconds / $year);
return [$base, "$base year"];
} else {
return [null, 'centuries'];
}

return [null, 'centuries'];
};

list($display_num, $display_str) = $callback($seconds);

if ($display_num > 1) {
$display_str .= "s";
$display_str .= 's';
}

return $display_str;
Expand Down
7 changes: 7 additions & 0 deletions src/Zxcvbn.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ public function __construct()
$this->feedback = new \ZxcvbnPhp\Feedback();
}

public function addMatcher(string $className)
{
$this->matcher->addMatcher($className);

return $this;
}

/**
* Calculate password strength via non-overlapping minimum entropy patterns.
*
Expand Down
19 changes: 19 additions & 0 deletions test/MatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPUnit\Framework\TestCase;
use ZxcvbnPhp\Matcher;
use ZxcvbnPhp\Matchers\Bruteforce;
use ZxcvbnPhp\Matchers\DictionaryMatch;

/**
Expand Down Expand Up @@ -63,4 +64,22 @@ public function testUserDefinedWords()
$this->assertInstanceOf(DictionaryMatch::class, $matches[0], "user input match is correct class");
$this->assertEquals('wQbg', $matches[0]->token, "user input match has correct token");
}

public function testAddMatcherWillThrowException()
{
$this->expectException(\InvalidArgumentException::class);

$matcher = new Matcher();
$matcher->addMatcher('invalid className');

$this->expectNotToPerformAssertions();
}

public function testAddMatcherWillReturnSelf()
{
$matcher = new Matcher();
$result = $matcher->addMatcher(Bruteforce::class);

$this->assertSame($matcher, $result);
}
}
17 changes: 17 additions & 0 deletions test/ZxcvbnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ZxcvbnPhp\Test;

use PHPUnit\Framework\TestCase;
use ZxcvbnPhp\Matchers\Bruteforce;
use ZxcvbnPhp\Matchers\DictionaryMatch;
use ZxcvbnPhp\Matchers\Match;
use ZxcvbnPhp\Zxcvbn;
Expand Down Expand Up @@ -130,4 +131,20 @@ public function testMultibyteUserDefinedWords()
$this->assertInstanceOf(DictionaryMatch::class, $result['sequence'][0], "user input match is correct class");
$this->assertEquals('المفاتيح', $result['sequence'][0]->token, "user input match has correct token");
}

public function testAddMatcherWillThrowException()
{
$this->expectException(\InvalidArgumentException::class);

$this->zxcvbn->addMatcher('invalid className');

$this->expectNotToPerformAssertions();
}

public function testAddMatcherWillReturnSelf()
{
$result = $this->zxcvbn->addMatcher(Bruteforce::class);

$this->assertSame($this->zxcvbn, $result);
}
}

0 comments on commit dde9679

Please sign in to comment.