171 lines
4.6 KiB
PHP
171 lines
4.6 KiB
PHP
<?php
|
|
/**
|
|
* Wolfram|Alpha password strength calculation
|
|
*
|
|
* @see https://gist.github.com/1514997 (original source this class is based on)
|
|
* @see http://www.wolframalpha.com/input/?i=password+strength+for+god
|
|
* @see https://github.com/rchouinard/phpass
|
|
* @license MIT
|
|
*/
|
|
class PasswordStrength {
|
|
|
|
const STRENGTH_VERY_WEAK = 1;
|
|
const STRENGTH_WEAK = 2;
|
|
const STRENGTH_FAIR = 3;
|
|
const STRENGTH_STRONG = 4;
|
|
const STRENGTH_VERY_STRONG = 5;
|
|
|
|
public function classifyScore($score) {
|
|
if ($score < 50) return self::STRENGTH_VERY_WEAK;
|
|
if ($score < 60) return self::STRENGTH_WEAK;
|
|
if ($score < 75) return self::STRENGTH_FAIR;
|
|
if ($score < 90) return self::STRENGTH_STRONG;
|
|
return self::STRENGTH_VERY_STRONG;
|
|
}
|
|
|
|
public function classify($pw) {
|
|
return $this->classifyScore($this->calculate($pw));
|
|
}
|
|
|
|
/**
|
|
* Calculate score for a password
|
|
*
|
|
* @param string $pw the password to work on
|
|
* @return int score
|
|
*/
|
|
public function calculate($pw) {
|
|
$length = strlen($pw);
|
|
$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 = $pw[$i];
|
|
$code = ord($ch);
|
|
/* [0-9] */ if ($code >= 48 && $code <= 57) { $nNum++; $locNum[] = $i; }
|
|
/* [A-Z] */ elseif ($code >= 65 && $code <= 90) { $nUpper++; $locUpper[] = $i; }
|
|
/* [a-z] */ elseif ($code >= 97 && $code <= 122) { $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, $pw, $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($pw)) as $seq) {
|
|
if (count($seq) > 2) {
|
|
$score -= (count($seq) - 2) * 2;
|
|
}
|
|
}
|
|
// sequential numbers
|
|
foreach ($this->findSequence($locNum, mb_strtolower($pw)) as $seq) {
|
|
if (count($seq) > 2) {
|
|
$score -= (count($seq) - 2) * 2;
|
|
}
|
|
}
|
|
}
|
|
return $score;
|
|
}
|
|
/**
|
|
* Find all sequential chars in string $src
|
|
*
|
|
* Only chars in $charLocs are considered. $charLocs is a list of numbers.
|
|
* For example if $charLocs is [0,2,3], then only $src[2:3] is a possible
|
|
* substring with sequential chars.
|
|
*
|
|
* @param array $charLocs
|
|
* @param string $src
|
|
* @return array [[c,c,c,c], [a,a,a], ...]
|
|
*/
|
|
private function findSequence($charLocs, $src) {
|
|
$sequences = array();
|
|
$sequence = array();
|
|
for ($i = 0; $i < count($charLocs)-1; ++$i) {
|
|
$here = $charLocs[$i];
|
|
$next = $charLocs[$i+1];
|
|
$charHere = $src[$charLocs[$i]];
|
|
$charNext = $src[$charLocs[$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;
|
|
}
|
|
} |