diff --git a/LICENSE b/LICENSE index 474a87f..6164dfc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Bobby Allen +Copyright (c) 2015-2020 Bobby Allen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index aee0595..2ff5ad0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ This library supports the following kind of complexity settings: * Special character containment * Minimum/maximum character detection * Password age expiry detection -* Detection of previous use (for a configurable amount of previous uses) +* Detection of previous use such as against a password history datastore. +* Ability to add and check against a configurable list of common passwords/words etc. Requirements ------------ @@ -40,7 +41,6 @@ To install the package into your project (assuming you are using the [Composer]( composer require ballen/plexity ``` - Alternatively you can manually add this library to your project using the following steps, simply edit your project's ``composer.json`` file and add the following lines (or update your existing ``require`` section with the library like so): ```json @@ -66,9 +66,10 @@ use Ballen\Plexity\Plexity as PasswordValidator; $password = new PasswordValidator(); -$password->requireSpecialCharacters() // We want the password to contain special characters. - ->requireUpperCase() // Requires the password to contains upper case characters. - ->requireLowerCase() // Requires the password to contains lower case characters. +$password->requireSpecialCharacters() // We want the password to contain (atleast 1) special characters. + //->requireSpecialCharacters(5), // We could also specify a specific number of special characters. + ->requireUpperCase() // Requires the password to contains more than one upper case characters. + ->requireLowerCase(2) // Requires the password to contains atleast 2 lower case characters. ->requireNumericChataters(3); // We want to ensure that the password uses at least 3 numbers! // An example of passing a password history array, if the password exists in here then we'll disallow it! @@ -77,7 +78,8 @@ $password->notIn([ 'Ros3bud', 'mypasSwordh88e8*&|re', ]); - +// You can optionally pass in an implementation of PasswordHistoryInterface like so: +//$password->notIn(new CustomPasswordHistoryDatastore()); // Must implement Ballen\Plexity\Interfaces\PasswordHistoryInterface try { $password->check('my_password_string_here'); @@ -93,7 +95,7 @@ Tests and coverage This library is fully unit tested using [PHPUnit](https://phpunit.de/). -I use [TravisCI](https://travis-ci.org/) for continuous integration, which triggers tests for PHP 5.6, 7.0, 7.1, 7.3 and 7.3 every time a commit is pushed. +I use [TravisCI](https://travis-ci.org/) for continuous integration, which triggers tests for PHP 5.6, 7.0, 7.1, 7.3 and 7.4 every time a commit is pushed. If you wish to run the tests yourself you should run the following: diff --git a/composer.json b/composer.json index 8c874fa..336f34b 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ } ], "require": { - "php": ">=5.4", + "php": ">=5.5", "ballen/collection": "~1.0" }, "require-dev": { @@ -23,5 +23,11 @@ "psr-4": { "Ballen\\Plexity\\": "lib/" } + }, + "autoload-dev": { + "psr-4": { + "Ballen\\Plexity\\Tests\\": "tests/", + "Ballen\\Plexity\\": "lib/" + } } } diff --git a/lib/Interfaces/PasswordHistoryInterface.php b/lib/Interfaces/PasswordHistoryInterface.php new file mode 100644 index 0000000..afab3d3 --- /dev/null +++ b/lib/Interfaces/PasswordHistoryInterface.php @@ -0,0 +1,14 @@ + 0, self::RULE_LENGTH_MIN => 0, self::RULE_LENGTH_MAX => 0, - self::RULE_NOT_IN => [], + self::RULE_NOT_IN => null, ]; /** @@ -157,13 +158,13 @@ public function lengthBetween($minimmum, $maximum) } /** - * Requires that the password/string is not found in the collection. - * @param array The array of passwords/strings to check against. + * Requires that the password/string is not found in a password history collection. + * @param PasswordHistoryInterface|array An array of passwords/strings to check against or an implementation of PasswordHistoryInterface. * @return Plexity */ - public function notIn(array $array) + public function notIn($history) { - $this->rules->put(self::RULE_NOT_IN, $array); + $this->rules->put(self::RULE_NOT_IN, $history); return $this; } diff --git a/lib/Support/Validator.php b/lib/Support/Validator.php index 0c2bb29..da2832a 100644 --- a/lib/Support/Validator.php +++ b/lib/Support/Validator.php @@ -2,6 +2,7 @@ namespace Ballen\Plexity\Support; +use Ballen\Plexity\Interfaces\PasswordHistoryInterface; use Ballen\Plexity\Plexity; use Ballen\Plexity\Exceptions\ValidationException; @@ -21,6 +22,16 @@ class Validator { + /** + * RegEx for uppercase character detection. + */ + const REGEX_UPPER_CASE = "/[A-Z]/"; + + /** + * RegEx for lowercase character detection. + */ + const REGEX_LOWER_CASE = "/[a-z]/"; + /** * The Plexity object (contains the validation configuration) * @var Plexity @@ -32,7 +43,16 @@ class Validator * @var array */ protected $numbers = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 0 ]; /** @@ -41,9 +61,38 @@ class Validator * @var array */ protected $specialCharacters = [ - ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '.', - '/', ':', ';', '<', '=', '>', '?', '@', '[', ']', '\\', '^', '_', '`', - '{', '|', '}', '~', + ' ', + '!', + '"', + '#', + '$', + '%', + '&', + '\'', + '(', + ')', + '*', + '+', + ',', + '.', + '/', + ':', + ';', + '<', + '=', + '>', + '?', + '@', + '[', + ']', + '\\', + '^', + '_', + '`', + '{', + '|', + '}', + '~', ]; /** @@ -148,11 +197,23 @@ public function checkSpecialCharacters() */ public function checkNotIn() { - if (count($this->configuration->rules()->get(Plexity::RULE_NOT_IN)) > 0) { - if (!$this->validateNotIn()) { + + if ($this->configuration->rules()->get(Plexity::RULE_NOT_IN) === null) { + return true; + } + + if ($this->configuration->rules()->get(Plexity::RULE_NOT_IN) instanceof PasswordHistoryInterface) { + if ($this->validateNotInPasswordHistoryImplementation()) { throw new ValidationException('The string exists in the list of disallowed values requirements.'); } } + + if (is_array($this->configuration->rules()->get(Plexity::RULE_NOT_IN)) && count($this->configuration->rules()->get(Plexity::RULE_NOT_IN)) > 0) { + if (!$this->validateNotInArray()) { + throw new ValidationException('The string exists in the list of disallowed values requirements.'); + } + } + } /** @@ -161,7 +222,7 @@ public function checkNotIn() */ private function validateUpperCase() { - $occurences = preg_match_all("/[A-Z]/", $this->configuration->checkString()); + $occurences = preg_match_all(self::REGEX_UPPER_CASE, $this->configuration->checkString()); if ($occurences >= $this->configuration->rules()->get(Plexity::RULE_UPPER)) { return true; @@ -176,9 +237,9 @@ private function validateUpperCase() */ private function validateLowerCase() { - $occurences = preg_match_all("/[a-z]/", $this->configuration->checkString()); + $occurrences = preg_match_all(self::REGEX_LOWER_CASE, $this->configuration->checkString()); - if ($occurences >= $this->configuration->rules()->get(Plexity::RULE_LOWER)) { + if ($occurrences >= $this->configuration->rules()->get(Plexity::RULE_LOWER)) { return true; } @@ -191,7 +252,8 @@ private function validateLowerCase() */ private function validateSpecialCharacters() { - if ($this->countOccurrences($this->specialCharacters, $this->configuration->checkString()) >= $this->configuration->rules()->get(Plexity::RULE_SPECIAL)) { + if ($this->countOccurrences($this->specialCharacters, + $this->configuration->checkString()) >= $this->configuration->rules()->get(Plexity::RULE_SPECIAL)) { return true; } return false; @@ -203,7 +265,8 @@ private function validateSpecialCharacters() */ private function validateNumericCharacters() { - if ($this->countOccurrences($this->numbers, $this->configuration->checkString()) >= $this->configuration->rules()->get(Plexity::RULE_NUMERIC)) { + if ($this->countOccurrences($this->numbers, + $this->configuration->checkString()) >= $this->configuration->rules()->get(Plexity::RULE_NUMERIC)) { return true; } return false; @@ -234,19 +297,29 @@ private function validateLengthMax() } /** - * Validates the not_in requirements. + * Validates the not_in requirements against a simple array. * @return boolean */ - private function validateNotIn() + private function validateNotInArray() { - if (in_array($this->configuration->checkString(), (array)$this->configuration->rules()->get(Plexity::RULE_NOT_IN))) { + if (in_array($this->configuration->checkString(), + (array)$this->configuration->rules()->get(Plexity::RULE_NOT_IN))) { return false; } return true; } /** - * Count the number of occurences of a character or string in a string. + * Validates the not_in requirements against an implementation of PasswordHistoryInterface. + * @return boolean + */ + private function validateNotInPasswordHistoryImplementation() + { + return ($this->configuration->rules()->get(Plexity::RULE_NOT_IN))->checkHistory($this->configuration->checkString()); + } + + /** + * Count the number of occurrences of a character or string in a string. * @param array $needles The character/string to count occurrences of. * @param string $haystack The string to check against. * @return int The number of occurrences. diff --git a/tests/Implementations/MD5PasswordHistoryStore.php b/tests/Implementations/MD5PasswordHistoryStore.php new file mode 100644 index 0000000..9495339 --- /dev/null +++ b/tests/Implementations/MD5PasswordHistoryStore.php @@ -0,0 +1,43 @@ +md5Passwords); + } + + /** + * @inheritDoc + */ + public function checkHistory($password) + { + return $this->hashAndCheckHistory($password); + } +} \ No newline at end of file diff --git a/tests/PlexityTest.php b/tests/PlexityTest.php index cda367f..e713c1e 100644 --- a/tests/PlexityTest.php +++ b/tests/PlexityTest.php @@ -1,6 +1,8 @@ requireUpperCase(); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string failed to meet the upper case requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string failed to meet the upper case requirements.'); $password->check('allthelettersarelowercase'); } @@ -38,7 +41,8 @@ public function testInvalidRequireLowerCase() { $password = new Plexity(); $password->requireLowerCase(); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string failed to meet the lower case requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string failed to meet the lower case requirements.'); $password->check('THISLOWERCASEEXAMPLEWILLFAIL'); } @@ -53,7 +57,8 @@ public function testInvalidLenghtOnMinimum() { $password = new Plexity(); $password->minimumLength(3); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The length does not meet the minimum length requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The length does not meet the minimum length requirements.'); $password->check(''); } @@ -76,7 +81,8 @@ public function testInvalidLengthOnMaximum() { $password = new Plexity(); $password->maximumLength(1); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The length exceeds the maximum length requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The length exceeds the maximum length requirements.'); $password->check('Ab'); } @@ -92,7 +98,8 @@ public function testPasswordInArray() { $password = new Plexity(); $password->notIn(['Example2', 'An3xampl3', 'MyEx4mp!e']); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string exists in the list of disallowed values requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string exists in the list of disallowed values requirements.'); $password->check('An3xampl3'); } @@ -114,7 +121,8 @@ public function testPasswordNotHasNumericCharacters() { $password = new Plexity(); $password->requireNumericChataters(); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string failed to meet the numeric character requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string failed to meet the numeric character requirements.'); $password->check('AnExample'); } @@ -129,7 +137,8 @@ public function testPasswordNotHasAmountOfNumericChars() { $password = new Plexity(); $password->requireNumericChataters(5); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string failed to meet the numeric character requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string failed to meet the numeric character requirements.'); $password->check('An3%a*pl3'); } @@ -144,7 +153,8 @@ public function testPasswordNotHasSpecialCharacters() { $password = new Plexity(); $password->requireSpecialCharacters(); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string failed to meet the special character requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string failed to meet the special character requirements.'); $password->check('An3xampl3'); } @@ -159,7 +169,8 @@ public function testPasswordNotHasAmountOfSpecialChars() { $password = new Plexity(); $password->requireSpecialCharacters(5); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string failed to meet the special character requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string failed to meet the special character requirements.'); $password->check('An3%a*pl3'); } @@ -174,7 +185,8 @@ public function testPasswordIsNotBetweenCharactersMinFail() { $password = new Plexity(); $password->lengthBetween(4, 10); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The length does not meet the minimum length requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The length does not meet the minimum length requirements.'); $password->check('An3'); } @@ -182,7 +194,8 @@ public function testPasswordIsNotBetweenCharactersMaxFail() { $password = new Plexity(); $password->lengthBetween(1, 5); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The length exceeds the maximum length requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The length exceeds the maximum length requirements.'); $password->check('An3akkkk'); } @@ -197,7 +210,8 @@ public function testPasswordContainsANumberOfLowerCharactersFail() { $password = new Plexity(); $password->requireLowerCase(4); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string failed to meet the lower case requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string failed to meet the lower case requirements.'); $password->check('ABCDEfgh'); } @@ -212,7 +226,26 @@ public function testPasswordContainsANumberOfUppercaseCharactersFail() { $password = new Plexity(); $password->requireUpperCase(6); - $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', 'The string failed to meet the upper case requirements.'); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string failed to meet the upper case requirements.'); $password->check('ABCDEfgh'); } + + public function testPasswordDoesExistInPasswordHistoryStore() + { + $password = new Plexity(); + $passwordHistoryStore = new MD5PasswordHistoryStore; + $password->notIn($passwordHistoryStore); + $this->setExpectedException('Ballen\Plexity\Exceptions\ValidationException', + 'The string exists in the list of disallowed values requirements.'); + $password->check('R0seBu9'); + } + + public function testPasswordDoesNotExistInPasswordHistoryStore() + { + $password = new Plexity(); + $passwordHistoryStore = new MD5PasswordHistoryStore; + $password->notIn($passwordHistoryStore); + $this->assertTrue($password->check('Bingo!')); + } }