File "SpatialMatch.php"
Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/better-wp-security/vendor-prod/bjeavons/zxcvbn-php/src/Matchers/SpatialMatch.php
File size: 9.32 KB
MIME-type: text/x-php
Charset: utf-8
<?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\Math\Binomial;
class SpatialMatch extends BaseMatch
{
public const SHIFTED_CHARACTERS = '~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?';
// Preset properties since adjacency graph is constant for qwerty keyboard and keypad.
public const KEYBOARD_STARTING_POSITION = 94;
public const KEYPAD_STARTING_POSITION = 15;
public const KEYBOARD_AVERAGE_DEGREES = 4.5957446809; // 432 / 94
public const KEYPAD_AVERAGE_DEGREES = 5.0666666667; // 76 / 15
public $pattern = 'spatial';
/** @var int The number of characters the shift key was held for in the token. */
public $shiftedCount;
/** @var int The number of turns on the keyboard required to complete the token. */
public $turns;
/** @var string The keyboard layout that the token is a spatial match on. */
public $graph;
/** @var array A cache of the adjacency_graphs json file */
protected static $adjacencyGraphs = [];
/**
* Match spatial patterns based on keyboard layouts (e.g. qwerty, dvorak, keypad).
*
* @param string $password
* @param array $userInputs
* @param array $graphs
* @return SpatialMatch[]
*/
public static function match(string $password, array $userInputs = [], array $graphs = []): array
{
$matches = [];
if (!$graphs) {
$graphs = static::getAdjacencyGraphs();
}
foreach ($graphs as $name => $graph) {
$results = static::graphMatch($password, $graph, $name);
foreach ($results as $result) {
$result['graph'] = $name;
$matches[] = new static($password, $result['begin'], $result['end'], $result['token'], $result);
}
}
Matcher::usortStable($matches, [Matcher::class, 'compareMatches']);
return $matches;
}
#[ArrayShape(['warning' => 'string', 'suggestions' => 'string[]'])]
public function getFeedback(bool $isSoleMatch): array
{
$warning = $this->turns == 1
? 'Straight rows of keys are easy to guess'
: 'Short keyboard patterns are easy to guess';
return [
'warning' => $warning,
'suggestions' => [
'Use a longer keyboard pattern with more turns'
]
];
}
/**
* @param string $password
* @param int $begin
* @param int $end
* @param string $token
* @param array $params An array with keys: [graph (required), shifted_count, turns].
*/
public function __construct(string $password, int $begin, int $end, string $token, array $params = [])
{
parent::__construct($password, $begin, $end, $token);
$this->graph = $params['graph'];
if (!empty($params)) {
$this->shiftedCount = $params['shifted_count'] ?? null;
$this->turns = $params['turns'] ?? null;
}
}
/**
* Match spatial patterns in a adjacency graph.
* @param string $password
* @param array $graph
* @param string $graphName
* @return array
*/
protected static function graphMatch(string $password, array $graph, string $graphName): array
{
$result = [];
$i = 0;
$passwordLength = mb_strlen($password);
while ($i < $passwordLength - 1) {
$j = $i + 1;
$lastDirection = null;
$turns = 0;
$shiftedCount = 0;
// Check if the initial character is shifted
if ($graphName === 'qwerty' || $graphName === 'dvorak') {
if (mb_strpos(self::SHIFTED_CHARACTERS, mb_substr($password, $i, 1)) !== false) {
$shiftedCount++;
}
}
while (true) {
$prevChar = mb_substr($password, $j - 1, 1);
$found = false;
$curDirection = -1;
$adjacents = $graph[$prevChar] ?? [];
// Consider growing pattern by one character if j hasn't gone over the edge.
if ($j < $passwordLength) {
$curChar = mb_substr($password, $j, 1);
foreach ($adjacents as $adj) {
$curDirection += 1;
if ($adj === null) {
continue;
}
$curCharPos = static::indexOf($adj, $curChar);
if ($curCharPos !== -1) {
$found = true;
$foundDirection = $curDirection;
if ($curCharPos === 1) {
// index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
// for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
$shiftedCount += 1;
}
if ($lastDirection !== $foundDirection) {
// adding a turn is correct even in the initial case when last_direction is null:
// every spatial pattern starts with a turn.
$turns += 1;
$lastDirection = $foundDirection;
}
break;
}
}
}
// if the current pattern continued, extend j and try to grow again
if ($found) {
$j += 1;
} else {
// otherwise push the pattern discovered so far, if any...
// Ignore length 1 or 2 chains.
if ($j - $i > 2) {
$result[] = [
'begin' => $i,
'end' => $j - 1,
'token' => mb_substr($password, $i, $j - $i),
'turns' => $turns,
'shifted_count' => $shiftedCount
];
}
// ...and then start a new search for the rest of the password.
$i = $j;
break;
}
}
}
return $result;
}
/**
* Get the index of a string a character first
*
* @param string $string
* @param string $char
*
* @return int
*/
protected static function indexOf(string $string, string $char): int
{
$pos = mb_strpos($string, $char);
return ($pos === false ? -1 : $pos);
}
/**
* Load adjacency graphs.
*
* @return array
*/
public static function getAdjacencyGraphs(): array
{
if (empty(self::$adjacencyGraphs)) {
$json = file_get_contents(dirname(__FILE__) . '/adjacency_graphs.json');
$data = json_decode($json, true);
// This seems pointless, but the data file is not guaranteed to be in any particular order.
// We want to be in the exact order below so as to match most closely with upstream, because when a match
// can be found in multiple graphs (such as 789), the one that's listed first is that one that will be picked.
$data = [
'qwerty' => $data['qwerty'],
'dvorak' => $data['dvorak'],
'keypad' => $data['keypad'],
'mac_keypad' => $data['mac_keypad'],
];
self::$adjacencyGraphs = $data;
}
return self::$adjacencyGraphs;
}
protected function getRawGuesses(): float
{
if ($this->graph === 'qwerty' || $this->graph === 'dvorak') {
$startingPosition = self::KEYBOARD_STARTING_POSITION;
$averageDegree = self::KEYBOARD_AVERAGE_DEGREES;
} else {
$startingPosition = self::KEYPAD_STARTING_POSITION;
$averageDegree = self::KEYPAD_AVERAGE_DEGREES;
}
$guesses = 0;
$length = mb_strlen($this->token);
$turns = $this->turns;
// estimate the number of possible patterns w/ length L or less with t turns or less.
for ($i = 2; $i <= $length; $i++) {
$possibleTurns = min($turns, $i - 1);
for ($j = 1; $j <= $possibleTurns; $j++) {
$guesses += Binomial::binom($i - 1, $j - 1) * $startingPosition * pow($averageDegree, $j);
}
}
// add extra guesses for shifted keys. (% instead of 5, A instead of a.)
// math is similar to extra guesses of l33t substitutions in dictionary matches.
if ($this->shiftedCount > 0) {
$shifted = $this->shiftedCount;
$unshifted = $length - $shifted;
if ($unshifted === 0) {
$guesses *= 2;
} else {
$variations = 0;
for ($i = 1; $i <= min($shifted, $unshifted); $i++) {
$variations += Binomial::binom($shifted + $unshifted, $i);
}
$guesses *= $variations;
}
}
return $guesses;
}
}