Skip to content

Commit

Permalink
Word converters in guessable data rules
Browse files Browse the repository at this point in the history
  • Loading branch information
Stadly committed Dec 6, 2018
1 parent c360bef commit 961f541
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
- Leetspeak character converter.
- Word converter creating all combinations of upper case and lower case letters in words.
- Possible to use word converters in dictionaries. Useful for converting leetspeak to normal characters before checking the word list.
- Possible to use word converters in guessable data rules. Useful for converting leetspeak to normal characters before comparing to the guessable data.

### Changed
- Case converters return a traversable with strings instead of a single string.
Expand Down
55 changes: 52 additions & 3 deletions src/Rule/GuessableData.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
use DateTimeInterface;
use InvalidArgumentException;
use RuntimeException;
use Traversable;
use Stadly\PasswordPolice\Password;
use Stadly\PasswordPolice\Policy;
use Stadly\PasswordPolice\WordConverter\WordConverterInterface;
use Stadly\PasswordPolice\WordList\WordListInterface;

final class GuessableData implements RuleInterface
Expand Down Expand Up @@ -54,6 +56,19 @@ final class GuessableData implements RuleInterface
', ',
];

/**
* @var WordConverterInterface[] Word converters.
*/
private $wordConverters;

/**
* @param WordConverterInterface... $wordConverters Word converters.
*/
public function __construct(WordConverterInterface... $wordConverters)
{
$this->wordConverters = $wordConverters;
}

/**
* Check whether a password is in compliance with the rule.
*
Expand Down Expand Up @@ -89,15 +104,49 @@ public function enforce($password): void
private function getGuessableData($password)
{
if ($password instanceof Password) {
foreach ($password->getGuessableData() as $data) {
if ($this->contains((string)$password, $data)) {
return $data;
foreach ($this->getWordsToCheck((string)$password) as $word) {
foreach ($password->getGuessableData() as $data) {
if ($this->contains($word, $data)) {
return $data;
}
}
}
}
return null;
}

/**
* @param string $word Word to check.
* @return Traversable<string> Variants of the word to check.
*/
private function getWordsToCheck(string $word): Traversable
{
$checked = [];
foreach ($this->getConvertedWords($word) as $wordToCheck) {
if (isset($checked[$wordToCheck])) {
continue;
}

$checked[$wordToCheck] = true;
yield $wordToCheck;
}
}

/**
* @param string $word Word to convert.
* @return Traversable<string> Converted words. May contain duplicates.
*/
private function getConvertedWords(string $word): Traversable
{
yield $word;

foreach ($this->wordConverters as $wordConverter) {
foreach ($wordConverter->convert($word) as $converted) {
yield $converted;
}
}
}

/**
* @param string $password Password to check.
* @param string|DateTimeInterface $data Data to check.
Expand Down
101 changes: 101 additions & 0 deletions tests/Rule/GuessableDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use DateTime;
use PHPUnit\Framework\TestCase;
use Stadly\PasswordPolice\Password;
use Stadly\PasswordPolice\WordConverter\WordConverterInterface;

/**
* @coversDefaultClass \Stadly\PasswordPolice\Rule\GuessableData
Expand All @@ -15,6 +16,18 @@
*/
final class GuessableDataTest extends TestCase
{
/**
* @covers ::__construct
*/
public function testCanConstructRule(): void
{
$rule = new GuessableData();

// Force generation of code coverage
$ruleConstruct = new GuessableData();
self::assertEquals($rule, $ruleConstruct);
}

/**
* @covers ::test
*/
Expand Down Expand Up @@ -69,6 +82,94 @@ public function testPasswordCanNotContainDate(): void
self::assertTrue($rule->test($password));
}

/**
* @covers ::test
*/
public function testStringIsRecognizedAfterSingleWordConverter(): void
{
$wordConverter = $this->createMock(WordConverterInterface::class);
$wordConverter->method('convert')->willReturnCallback(
function ($word) {
yield str_replace(['4', ''], ['a', 'e'], $word);
}
);

$rule = new GuessableData($wordConverter);

self::assertFalse($rule->test(new Password('pine4ppl€jack', ['apple'])));
self::assertTrue($rule->test(new Password('pine4pp1€jack', ['apple'])));
}

/**
* @covers ::test
*/
public function testDateIsRecognizedAfterSingleWordConverter(): void
{
$wordConverter = $this->createMock(WordConverterInterface::class);
$wordConverter->method('convert')->willReturnCallback(
function ($word) {
yield str_replace(['I', 'B'], ['1', '8'], $word);
}
);

$rule = new GuessableData($wordConverter);

self::assertFalse($rule->test(new Password('foo2B/II/1Bbar', [new DateTime('2018-11-28')])));
self::assertTrue($rule->test(new Password('fooZB/I!/1Bbar', [new DateTime('2018-11-28')])));
}

/**
* @covers ::test
*/
public function testStringIsRecognizedAfterMultipleWordConverters(): void
{
$wordConverter1 = $this->createMock(WordConverterInterface::class);
$wordConverter1->method('convert')->willReturnCallback(
function ($word) {
yield str_replace(['4'], ['a'], $word);
}
);

$wordConverter2 = $this->createMock(WordConverterInterface::class);
$wordConverter2->method('convert')->willReturnCallback(
function ($word) {
yield str_replace([''], ['e'], $word);
}
);

$rule = new GuessableData($wordConverter1, $wordConverter2);

self::assertTrue($rule->test(new Password('pine4ppl€jack', ['apple'])));
self::assertFalse($rule->test(new Password('pineappl€jack', ['apple'])));
self::assertFalse($rule->test(new Password('pine4pplejack', ['apple'])));
}

/**
* @covers ::test
*/
public function testDateIsRecognizedAfterMultipleWordConverters(): void
{
$wordConverter1 = $this->createMock(WordConverterInterface::class);
$wordConverter1->method('convert')->willReturnCallback(
function ($word) {
yield str_replace(['I'], ['1'], $word);
}
);

$wordConverter2 = $this->createMock(WordConverterInterface::class);
$wordConverter2->method('convert')->willReturnCallback(
function ($word) {
yield str_replace(['B'], ['8'], $word);
}
);

$rule = new GuessableData($wordConverter1, $wordConverter2);

self::assertTrue($rule->test(new Password('foo2B/I!/1Bbar', [new DateTime('2018-11-28')])));
self::assertFalse($rule->test(new Password('foo28/II/18bar', [new DateTime('2018-11-28')])));
self::assertFalse($rule->test(new Password('foo2B/11/1Bbar', [new DateTime('2018-11-28')])));
}

/**
* @covers ::enforce
*/
Expand Down

0 comments on commit 961f541

Please sign in to comment.