Skip to content

Commit

Permalink
The notIn() method now accepts an implementation of PasswordHistoryIn…
Browse files Browse the repository at this point in the history
…terface in addition to the current string[]. Bumped Composer package dependencies too and minor README updates to demonstrate newer functionality.
  • Loading branch information
allebb committed Feb 8, 2020
1 parent 6136862 commit b4b3bcb
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 42 deletions.
2 changes: 1 addition & 1 deletion 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
Expand Down
16 changes: 9 additions & 7 deletions README.md
Expand Up @@ -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
------------
Expand All @@ -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
Expand All @@ -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!
Expand All @@ -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');
Expand All @@ -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:

Expand Down
8 changes: 7 additions & 1 deletion composer.json
Expand Up @@ -13,7 +13,7 @@
}
],
"require": {
"php": ">=5.4",
"php": ">=5.5",
"ballen/collection": "~1.0"
},
"require-dev": {
Expand All @@ -23,5 +23,11 @@
"psr-4": {
"Ballen\\Plexity\\": "lib/"
}
},
"autoload-dev": {
"psr-4": {
"Ballen\\Plexity\\Tests\\": "tests/",
"Ballen\\Plexity\\": "lib/"
}
}
}
14 changes: 14 additions & 0 deletions lib/Interfaces/PasswordHistoryInterface.php
@@ -0,0 +1,14 @@
<?php


namespace Ballen\Plexity\Interfaces;

interface PasswordHistoryInterface
{
/**
* Checks that the password does not exist in a password history implementation.
* @param string $password The password to check the history against.
* @return boolean Returns true if the password exists in the history.
*/
public function checkHistory($password);
}
11 changes: 6 additions & 5 deletions lib/Plexity.php
Expand Up @@ -3,6 +3,7 @@
namespace Ballen\Plexity;

use Ballen\Collection\Collection;
use Ballen\Plexity\Interfaces\PasswordHistoryInterface;
use Ballen\Plexity\Support\Validator;

/**
Expand Down Expand Up @@ -52,7 +53,7 @@ class Plexity
self::RULE_NUMERIC => 0,
self::RULE_LENGTH_MIN => 0,
self::RULE_LENGTH_MAX => 0,
self::RULE_NOT_IN => [],
self::RULE_NOT_IN => null,
];

/**
Expand Down Expand Up @@ -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;
}

Expand Down
103 changes: 88 additions & 15 deletions lib/Support/Validator.php
Expand Up @@ -2,6 +2,7 @@

namespace Ballen\Plexity\Support;

use Ballen\Plexity\Interfaces\PasswordHistoryInterface;
use Ballen\Plexity\Plexity;
use Ballen\Plexity\Exceptions\ValidationException;

Expand All @@ -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
Expand All @@ -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
];

/**
Expand All @@ -41,9 +61,38 @@ class Validator
* @var array
*/
protected $specialCharacters = [
' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '.',
'/', ':', ';', '<', '=', '>', '?', '@', '[', ']', '\\', '^', '_', '`',
'{', '|', '}', '~',
' ',
'!',
'"',
'#',
'$',
'%',
'&',
'\'',
'(',
')',
'*',
'+',
',',
'.',
'/',
':',
';',
'<',
'=',
'>',
'?',
'@',
'[',
']',
'\\',
'^',
'_',
'`',
'{',
'|',
'}',
'~',
];

/**
Expand Down Expand Up @@ -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.');
}
}

}

/**
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions tests/Implementations/MD5PasswordHistoryStore.php
@@ -0,0 +1,43 @@
<?php


namespace Ballen\Plexity\Tests\Implementations;

use Ballen\Plexity\Interfaces\PasswordHistoryInterface;

/**
* Class MD5PasswordHistoryStore
* @package Ballen\Plexity\Tests\Implementations
*/
class MD5PasswordHistoryStore implements PasswordHistoryInterface
{

/**
* An array of plain and simple MD5 password hashes.
* (I HIGHLY RECOMMEND YOU USE STRONG ENCRYPTION IN YOUR REAL WORLD APPLICATIONS THOUGH - THIS IS JUST A VERY SIMPLE IMPLEMENTATION EXAMPLE!!!!!)
* @var array
*/
private $md5Passwords = [
'dc647eb65e6711e155375218212b3964', # 'Password'
'3ce189cedebda85cc8454c8339091e39', # 'R0seBu9'
];

/**
* Converts the plain password into an MD5 hash before checking against our hashed history array.
* @param $plainPassword
* @return bool
*/
private function hashAndCheckHistory($plainPassword)
{
$hashedPassword = md5($plainPassword);
return in_array($hashedPassword, $this->md5Passwords);
}

/**
* @inheritDoc
*/
public function checkHistory($password)
{
return $this->hashAndCheckHistory($password);
}
}

0 comments on commit b4b3bcb

Please sign in to comment.