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; } }