65 lines
1.8 KiB
Plaintext
65 lines
1.8 KiB
Plaintext
import utils from '../../../../scoring/utils'
|
|
import { PasswordChanges } from '../matching/unmunger/getCleanPasswords'
|
|
|
|
export interface L33tOptions {
|
|
l33t: string
|
|
subs: PasswordChanges[]
|
|
token: string
|
|
}
|
|
|
|
export interface GetCountsOptions {
|
|
token: string
|
|
sub: PasswordChanges
|
|
}
|
|
|
|
const countSubstring = (string: string, substring: string) => {
|
|
let count = 0
|
|
let pos = string.indexOf(substring)
|
|
while (pos >= 0) {
|
|
count += 1
|
|
pos = string.indexOf(substring, pos + substring.length)
|
|
}
|
|
return count
|
|
}
|
|
|
|
const getCounts = ({ sub, token }: GetCountsOptions) => {
|
|
// lower-case match.token before calculating: capitalization shouldn't affect l33t calc.
|
|
const tokenLower = token.toLowerCase()
|
|
// num of subbed chars
|
|
const subbedCount = countSubstring(tokenLower, sub.substitution)
|
|
|
|
// num of unsubbed chars
|
|
const unsubbedCount = countSubstring(tokenLower, sub.letter)
|
|
return {
|
|
subbedCount,
|
|
unsubbedCount,
|
|
}
|
|
}
|
|
|
|
export default ({ l33t, subs, token }: L33tOptions) => {
|
|
if (!l33t) {
|
|
return 1
|
|
}
|
|
let variations = 1
|
|
subs.forEach((sub) => {
|
|
const { subbedCount, unsubbedCount } = getCounts({ sub, token })
|
|
|
|
if (subbedCount === 0 || unsubbedCount === 0) {
|
|
// for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
|
|
// treat that as doubling the space (attacker needs to try fully subbed chars in addition to
|
|
// unsubbed.)
|
|
variations *= 2
|
|
} else {
|
|
// this case is similar to capitalization:
|
|
// with aa44a, U = 3, S = 2, attacker needs to try unsubbed + one sub + two subs
|
|
const p = Math.min(unsubbedCount, subbedCount)
|
|
let possibilities = 0
|
|
for (let i = 1; i <= p; i += 1) {
|
|
possibilities += utils.nCk(unsubbedCount + subbedCount, i)
|
|
}
|
|
variations *= possibilities
|
|
}
|
|
})
|
|
return variations
|
|
}
|