118 lines
3.9 KiB
Plaintext
118 lines
3.9 KiB
Plaintext
import { sorted, extend } from '../../utils/helper'
|
|
import {
|
|
MatcherBaseClass,
|
|
MatchOptions,
|
|
OptionsGraphEntry,
|
|
SpatialMatch,
|
|
} from '../../types'
|
|
|
|
type SpatialMatchOptions = Pick<MatchOptions, 'password'>
|
|
/*
|
|
* ------------------------------------------------------------------------------
|
|
* spatial match (qwerty/dvorak/keypad and so on) -----------------------------------------
|
|
* ------------------------------------------------------------------------------
|
|
*/
|
|
class MatchSpatial extends MatcherBaseClass {
|
|
SHIFTED_RX = /[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]/
|
|
|
|
match({ password }: SpatialMatchOptions) {
|
|
const matches: SpatialMatch[] = []
|
|
Object.keys(this.options.graphs).forEach((graphName) => {
|
|
const graph = this.options.graphs[graphName]
|
|
extend(matches, this.helper(password, graph, graphName))
|
|
})
|
|
return sorted(matches)
|
|
}
|
|
|
|
checkIfShifted(graphName: string, password: string, index: number) {
|
|
if (
|
|
!graphName.includes('keypad') &&
|
|
// initial character is shifted
|
|
this.SHIFTED_RX.test(password.charAt(index))
|
|
) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// eslint-disable-next-line complexity, max-statements
|
|
helper(password: string, graph: OptionsGraphEntry, graphName: string) {
|
|
let shiftedCount
|
|
const matches: SpatialMatch[] = []
|
|
let i = 0
|
|
const passwordLength = password.length
|
|
while (i < passwordLength - 1) {
|
|
let j = i + 1
|
|
let lastDirection: number | null = null
|
|
let turns = 0
|
|
shiftedCount = this.checkIfShifted(graphName, password, i)
|
|
|
|
while (true) {
|
|
const prevChar = password.charAt(j - 1)
|
|
const adjacents = graph[prevChar] || []
|
|
let found = false
|
|
let curDirection = -1
|
|
// consider growing pattern by one character if j hasn't gone over the edge.
|
|
if (j < passwordLength) {
|
|
const curChar = password.charAt(j)
|
|
const adjacentsLength = adjacents.length
|
|
for (let k = 0; k < adjacentsLength; k += 1) {
|
|
const adjacent = adjacents[k]
|
|
curDirection += 1
|
|
// eslint-disable-next-line max-depth
|
|
if (adjacent) {
|
|
const adjacentIndex = adjacent.indexOf(curChar)
|
|
// eslint-disable-next-line max-depth
|
|
if (adjacentIndex !== -1) {
|
|
found = true
|
|
// eslint-disable-next-line max-depth
|
|
if (adjacentIndex === 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
|
|
}
|
|
const foundDirection = curDirection
|
|
// eslint-disable-next-line max-depth
|
|
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
|
|
// otherwise push the pattern discovered so far, if any...
|
|
} else {
|
|
// don't consider length 1 or 2 chains.
|
|
if (j - i > 2) {
|
|
matches.push({
|
|
pattern: 'spatial',
|
|
i,
|
|
j: j - 1,
|
|
token: password.slice(i, j),
|
|
graph: graphName,
|
|
turns,
|
|
shiftedCount,
|
|
})
|
|
}
|
|
// ...and then start a new search for the rest of the password.
|
|
i = j
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return matches
|
|
}
|
|
}
|
|
|
|
export default MatchSpatial
|