<?php namespace ZxcvbnPhp\Matchers; class SequenceMatch extends BaseMatch { public const MAX_DELTA = 5; public $pattern = 'sequence'; /** @var string The name of the detected sequence. */ public $sequenceName; /** @var int The number of characters in the complete sequence space. */ public $sequenceSpace; /** @var bool True if the sequence is ascending, and false if it is descending. */ public $ascending; /** * Match sequences of three or more characters. * * @param string $password * @param array $userInputs * @return SequenceMatch[] */ public static function match($password, array $userInputs = []) { $matches = []; $passwordLength = mb_strlen($password); if ($passwordLength === 1) { return []; } $begin = 0; $lastDelta = null; for ($index = 1; $index < $passwordLength; $index++) { $delta = mb_ord(mb_substr($password, $index, 1)) - mb_ord(mb_substr($password, $index - 1, 1)); if ($lastDelta === null) { $lastDelta = $delta; } if ($lastDelta === $delta) { continue; } static::findSequenceMatch($password, $begin, $index - 1, $lastDelta, $matches); $begin = $index - 1; $lastDelta = $delta; } static::findSequenceMatch($password, $begin, $passwordLength - 1, $lastDelta, $matches); return $matches; } public static function findSequenceMatch($password, $begin, $end, $delta, &$matches) { if ($end - $begin > 1 || abs($delta) === 1) { if (abs($delta) > 0 && abs($delta) <= self::MAX_DELTA) { $token = mb_substr($password, $begin, $end - $begin + 1); if (preg_match('/^[a-z]+$/u', $token)) { $sequenceName = 'lower'; $sequenceSpace = 26; } elseif (preg_match('/^[A-Z]+$/u', $token)) { $sequenceName = 'upper'; $sequenceSpace = 26; } elseif (preg_match('/^\d+$/u', $token)) { $sequenceName = 'digits'; $sequenceSpace = 10; } else { $sequenceName = 'unicode'; $sequenceSpace = 26; } $matches[] = new static($password, $begin, $end, $token, [ 'sequenceName' => $sequenceName, 'sequenceSpace' => $sequenceSpace, 'ascending' => $delta > 0, ]); return; } } } public function getFeedback($isSoleMatch) { return [ 'warning' => "Sequences like abc or 6543 are easy to guess", 'suggestions' => [ 'Avoid sequences' ] ]; } /** * @param string $password * @param int $begin * @param int $end * @param string $token * @param array $params An array with keys: [sequenceName, sequenceSpace, ascending]. */ public function __construct($password, $begin, $end, $token, $params = []) { parent::__construct($password, $begin, $end, $token); if (!empty($params)) { $this->sequenceName = isset($params['sequenceName']) ? $params['sequenceName'] : null; $this->sequenceSpace = isset($params['sequenceSpace']) ? $params['sequenceSpace'] : null; $this->ascending = isset($params['ascending']) ? $params['ascending'] : null; } } protected function getRawGuesses() { $firstCharacter = mb_substr($this->token, 0, 1); $guesses = 0; if (in_array($firstCharacter, array('a', 'A', 'z', 'Z', '0', '1', '9'), true)) { $guesses += 4; // lower guesses for obvious starting points } elseif (ctype_digit($firstCharacter)) { $guesses += 10; // digits } else { // could give a higher base for uppercase, // assigning 26 to both upper and lower sequences is more conservative $guesses += 26; } if (!$this->ascending) { // need to try a descending sequence in addition to every ascending sequence -> // 2x guesses $guesses *= 2; } return $guesses * mb_strlen($this->token); } }