<?php /** * @license MIT * * Modified using Strauss. * @see https://github.com/BrianHenryIE/strauss */ declare(strict_types=1); namespace iThemesSecurity\Strauss\ZxcvbnPhp\Matchers; use JetBrains\PhpStorm\ArrayShape; use iThemesSecurity\Strauss\ZxcvbnPhp\Matcher; use iThemesSecurity\Strauss\ZxcvbnPhp\Scorer; class RepeatMatch extends BaseMatch { public const GREEDY_MATCH = '/(.+)\1+/u'; public const LAZY_MATCH = '/(.+?)\1+/u'; public const ANCHORED_LAZY_MATCH = '/^(.+?)\1+$/u'; public $pattern = 'repeat'; /** @var MatchInterface[] An array of matches for the repeated section itself. */ public $baseMatches = []; /** @var int The number of guesses required for the repeated section itself. */ public $baseGuesses; /** @var int The number of times the repeated section is repeated. */ public $repeatCount; /** @var string The string that was repeated in the token. */ public $repeatedChar; /** * Match 3 or more repeated characters. * * @param string $password * @param array $userInputs * @return RepeatMatch[] */ public static function match(string $password, array $userInputs = []): array { $matches = []; $lastIndex = 0; while ($lastIndex < mb_strlen($password)) { $greedyMatches = self::findAll($password, self::GREEDY_MATCH, $lastIndex); $lazyMatches = self::findAll($password, self::LAZY_MATCH, $lastIndex); if (empty($greedyMatches)) { break; } if (mb_strlen($greedyMatches[0][0]['token']) > mb_strlen($lazyMatches[0][0]['token'])) { $match = $greedyMatches[0]; preg_match(self::ANCHORED_LAZY_MATCH, $match[0]['token'], $anchoredMatch); $repeatedChar = $anchoredMatch[1]; } else { $match = $lazyMatches[0]; $repeatedChar = $match[1]['token']; } $scorer = new Scorer(); $matcher = new Matcher(); $baseAnalysis = $scorer->getMostGuessableMatchSequence($repeatedChar, $matcher->getMatches($repeatedChar)); $baseMatches = $baseAnalysis['sequence']; $baseGuesses = $baseAnalysis['guesses']; $repeatCount = mb_strlen($match[0]['token']) / mb_strlen($repeatedChar); $matches[] = new static( $password, $match[0]['begin'], $match[0]['end'], $match[0]['token'], [ 'repeated_char' => $repeatedChar, 'base_guesses' => $baseGuesses, 'base_matches' => $baseMatches, 'repeat_count' => $repeatCount, ] ); $lastIndex = $match[0]['end'] + 1; } return $matches; } #[ArrayShape(['warning' => 'string', 'suggestions' => 'string[]'])] public function getFeedback(bool $isSoleMatch): array { $warning = mb_strlen($this->repeatedChar) == 1 ? 'Repeats like "aaa" are easy to guess' : 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"'; return [ 'warning' => $warning, 'suggestions' => [ 'Avoid repeated words and characters', ], ]; } /** * @param string $password * @param int $begin * @param int $end * @param string $token * @param array $params An array with keys: [repeated_char, base_guesses, base_matches, repeat_count]. */ public function __construct(string $password, int $begin, int $end, string $token, array $params = []) { parent::__construct($password, $begin, $end, $token); if (!empty($params)) { $this->repeatedChar = $params['repeated_char'] ?? ''; $this->baseGuesses = $params['base_guesses'] ?? 0; $this->baseMatches = $params['base_matches'] ?? []; $this->repeatCount = $params['repeat_count'] ?? 0; } } protected function getRawGuesses(): float { return $this->baseGuesses * $this->repeatCount; } }