Skip to content

Instantly share code, notes, and snippets.

@tflori
Created January 4, 2019 14:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tflori/f0b2ff3767e1f4b3d9221ad33a4dc31f to your computer and use it in GitHub Desktop.
Save tflori/f0b2ff3767e1f4b3d9221ad33a4dc31f to your computer and use it in GitHub Desktop.
Password strength validation for verja
<?php
namespace App\Validator;
use Verja\Error;
use Verja\Validator;
class PasswordStrength extends Validator
{
/** @var int */
protected $minimalScore;
/**
* PasswordStrength constructor.
*
* @param int $minimalScore
*/
public function __construct(int $minimalScore = 50)
{
$this->minimalScore = $minimalScore;
}
/**
* Validate $value
*
* @param mixed $value
* @param array $context
* @return bool
*/
public function validate($value, array $context = []): bool
{
$score = $this->calculate($value);
if ($score >= $this->minimalScore) {
return true;
}
$this->error = new Error(
'PASSWORD_TO_WEAK',
$value,
sprintf('password strength score should be at least %d - reached %d', $this->minimalScore, $score),
[
'minimalScore' => $this->minimalScore,
'score' => $score
]
);
return false;
}
/**
* Calculate score for a password
*
* @param string $password
* @return int
*/
public function calculate($password)
{
$length = strlen($password);
$score = $length * 4;
$nUpper = 0;
$nLower = 0;
$nNum = 0;
$nSymbol = 0;
$locUpper = array();
$locLower = array();
$locNum = array();
$locSymbol = array();
$charDict = array();
// count character classes
for ($i = 0; $i < $length; ++$i) {
$ch = $password[$i];
$code = ord($ch);
if ($code >= 48 && $code <= 57) { // 0-9
$nNum++;
$locNum[] = $i;
} elseif ($code >= 65 && $code <= 90) { // A-Z
$nUpper++;
$locUpper[] = $i;
} elseif ($code >= 97 && $code <= 122) { // a-z
$nLower++;
$locLower[] = $i;
} else {
$nSymbol++;
$locSymbol[] = $i;
}
if (!isset($charDict[$ch])) {
$charDict[$ch] = 1;
} else {
$charDict[$ch]++;
}
}
// reward upper/lower characters if pw is not made up of only either one
if ($nUpper !== $length && $nLower !== $length) {
if ($nUpper !== 0) {
$score += ($length - $nUpper) * 2;
}
if ($nLower !== 0) {
$score += ($length - $nLower) * 2;
}
}
// reward numbers if pw is not made up of only numbers
if ($nNum !== $length) {
$score += $nNum * 4;
}
// reward symbols
$score += $nSymbol * 6;
// middle number or symbol
foreach (array($locNum, $locSymbol) as $list) {
$reward = 0;
foreach ($list as $i) {
$reward += ($i !== 0 && $i !== $length -1) ? 1 : 0;
}
$score += $reward * 2;
}
// chars only
if ($nUpper + $nLower === $length) {
$score -= $length;
}
// numbers only
if ($nNum === $length) {
$score -= $length;
}
// repeating chars
$repeats = 0;
foreach ($charDict as $count) {
if ($count > 1) {
$repeats += $count - 1;
}
}
if ($repeats > 0) {
$score -= (int) (floor($repeats / ($length-$repeats)) + 1);
}
if ($length > 2) {
// consecutive letters and numbers
foreach (array('/[a-z]{2,}/', '/[A-Z]{2,}/', '/[0-9]{2,}/') as $re) {
preg_match_all($re, $password, $matches, PREG_SET_ORDER);
if (!empty($matches)) {
foreach ($matches as $match) {
$score -= (strlen($match[0]) - 1) * 2;
}
}
}
// sequential letters
$locLetters = array_merge($locUpper, $locLower);
sort($locLetters);
foreach ($this->findSequence($locLetters, mb_strtolower($password)) as $seq) {
if (count($seq) > 2) {
$score -= (count($seq) - 2) * 2;
}
}
// sequential numbers
foreach ($this->findSequence($locNum, mb_strtolower($password)) as $seq) {
if (count($seq) > 2) {
$score -= (count($seq) - 2) * 2;
}
}
}
return $score;
}
/**
* Find all sequential chars in string $src
*
* Only chars in $charLocations are considered. $charLocations is a list of numbers.
* For example if $charLocations is [0,2,3], then only $src[2:3] is a possible
* substring with sequential chars.
*
* @param array $charLocations
* @param string $src
* @return array [[c,c,c,c], [a,a,a], ...]
*/
private function findSequence($charLocations, $src)
{
$sequences = array();
$sequence = array();
for ($i = 0; $i < count($charLocations) - 1; ++$i) {
$here = $charLocations[$i];
$next = $charLocations[$i + 1];
$charHere = $src[$charLocations[$i]];
$charNext = $src[$charLocations[$i + 1]];
$distance = $next - $here;
$charDistance = ord($charNext) - ord($charHere);
if ($distance === 1 && $charDistance === 1) {
// We find a pair of sequential chars!
if (empty($sequence)) {
$sequence = array($charHere, $charNext);
} else {
$sequence[] = $charNext;
}
} elseif (!empty($sequence)) {
$sequences[] = $sequence;
$sequence = array();
}
}
if (!empty($sequence)) {
$sequences[] = $sequence;
}
return $sequences;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment