zxcvbn-typescript
Version:
realistic password strength estimation, updated and ported to Typescript from Dan Wheeler's zxcvbn
234 lines • 9.79 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.estimate_guesses = exports.most_guessable_match_sequence = void 0;
var bruteforce_guesses_1 = require("./bruteforce_guesses");
var date_guesses_1 = require("./date_guesses");
var dictionary_guesses_1 = require("./dictionary_guesses");
var regex_guesses_1 = require("./regex_guesses");
var repeat_guesses_1 = require("./repeat_guesses");
var sequence_guesses_1 = require("./sequence_guesses");
var spatial_guesses_1 = require("./spatial_guesses");
var support_1 = require("./support");
var MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10000;
// helper: considers whether a length-l sequence ending at match m is better (fewer guesses)
// than previously encountered sequences, updating state if so.
function update(password, optimal, m, l, exclude_additive) {
var k = m.j;
var pi = estimate_guesses(m, password);
if (l > 1) {
// we're considering a length-l sequence ending with match m:
// obtain the product term in the minimization function by multiplying m's guesses
// by the product of the length-(l-1) sequence ending just before m, at m.i - 1.
pi *= optimal.pi[m.i - 1][l - 1];
}
// calculate the minimization func
var g = support_1.factorial(l) * pi;
if (!exclude_additive) {
g += Math.pow(MIN_GUESSES_BEFORE_GROWING_SEQUENCE, l - 1);
}
// update state if new best.
// first see if any competing sequences covering this prefix, with l or fewer matches,
// fare better than this sequence. if so, skip it and return.
for (var competing_l in optimal.g[k]) {
var competing_g = optimal.g[k][competing_l];
if (competing_l > l) {
continue;
}
if (competing_g <= g) {
return;
}
}
// this sequence might be part of the final optimal sequence.
optimal.g[k][l] = g;
optimal.m[k][l] = m;
optimal.pi[k][l] = pi;
}
// helper: step backwards through optimal.m starting at the end,
// constructing the final optimal match sequence.
function unwind(optimal, n) {
var optimal_match_sequence = [];
var k = n - 1;
// find the final best sequence length and score
var l = -1;
var g = Infinity;
for (var candidate_l in optimal.g[k]) {
var candidate_g = optimal.g[k][candidate_l];
if (candidate_g < g) {
l = parseInt(candidate_l);
g = candidate_g;
}
}
while (k >= 0) {
var m = optimal.m[k][l];
optimal_match_sequence.unshift(m);
k = m.i - 1;
l--;
}
return optimal_match_sequence;
}
// helper: evaluate bruteforce matches ending at k.
function bruteforce_update(password, optimal, k, exclude_additive) {
// see if a single bruteforce match spanning the k-prefix is optimal.
var m = make_bruteforce_match(password, 0, k);
update(password, optimal, m, 1, exclude_additive);
for (var i = 1; i <= k; i++) {
// generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k).
// see if adding these new matches to any of the sequences in optimal[i-1]
// leads to new bests.
var m_1 = make_bruteforce_match(password, i, k);
var object = optimal.m[i - 1];
for (var l in object) {
var i_1 = parseInt(l);
var last_m = object[i_1];
if (last_m.pattern === "bruteforce")
continue;
update(password, optimal, m_1, i_1 + 1, exclude_additive);
}
}
}
// helper: make bruteforce match objects spanning i to j, inclusive.
function make_bruteforce_match(password, i, j) {
return {
pattern: "bruteforce",
token: password.slice(i, j + 1),
i: i,
j: j,
};
}
// ------------------------------------------------------------------------------
// search --- most guessable match sequence -------------------------------------
// ------------------------------------------------------------------------------
//
// takes a sequence of overlapping matches, returns the non-overlapping sequence with
// minimum guesses. the following is a O(l_max * (n + m)) dynamic programming algorithm
// for a length-n password with m candidate matches. l_max is the maximum optimal
// sequence length spanning each prefix of the password. In practice it rarely exceeds 5 and the
// search terminates rapidly.
//
// the optimal "minimum guesses" sequence is here defined to be the sequence that
// minimizes the following function:
//
// g = l! * Product(m.guesses for m in sequence) + D^(l - 1)
//
// where l is the length of the sequence.
//
// the factorial term is the number of ways to order l patterns.
//
// the D^(l-1) term is another length penalty, roughly capturing the idea that an
// attacker will try lower-length sequences first before trying length-l sequences.
//
// for example, consider a sequence that is date-repeat-dictionary.
// - an attacker would need to try other date-repeat-dictionary combinations,
// hence the product term.
// - an attacker would need to try repeat-date-dictionary, dictionary-repeat-date,
// ..., hence the factorial term.
// - an attacker would also likely try length-1 (dictionary) and length-2 (dictionary-date)
// sequences before length-3. assuming at minimum D guesses per pattern type,
// D^(l-1) approximates Sum(D^i for i in [1..l-1]
//
// ------------------------------------------------------------------------------
function most_guessable_match_sequence(password, matches, _exclude_additive) {
if (_exclude_additive === void 0) { _exclude_additive = false; }
var guesses, m;
var n = password.length;
// partition matches into sublists according to ending index j
var matches_by_j = new Array(password.length).fill([]);
for (var _i = 0, matches_1 = matches; _i < matches_1.length; _i++) {
m = matches_1[_i];
matches_by_j[m.j].push(m);
}
// small detail: for deterministic output, sort each sublist by i.
for (var _a = 0, matches_by_j_1 = matches_by_j; _a < matches_by_j_1.length; _a++) {
var lst = matches_by_j_1[_a];
lst.sort(function (m1, m2) { return m1.i - m2.i; });
}
var optimal = {
// optimal.m[k][l] holds final match in the best length-l match sequence covering the
// password prefix up to k, inclusive.
// if there is no length-l sequence that scores better (fewer guesses) than
// a shorter match sequence spanning the same prefix, optimal.m[k][l] is undefined.
m: matches_by_j.map(function () { return ({}); }),
// same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence).
// optimal.pi allows for fast (non-looping) updates to the minimization function.
pi: matches_by_j.map(function () { return ({}); }),
// same structure as optimal.m -- holds the overall metric.
g: matches_by_j.map(function () { return ({}); }),
};
for (var k = 0; k < n; k++) {
for (var _b = 0, _c = matches_by_j[k]; _b < _c.length; _b++) {
m = _c[_b];
if (m.i > 0) {
for (var l in optimal.m[m.i - 1]) {
var len = parseInt(l);
update(password, optimal, m, len + 1, _exclude_additive);
}
}
else {
update(password, optimal, m, 1, _exclude_additive);
}
}
bruteforce_update(password, optimal, k, _exclude_additive);
}
var optimal_match_sequence = unwind(optimal, n);
var optimal_l = optimal_match_sequence.length;
// corner: empty password
if (password.length === 0) {
guesses = 1;
}
else {
guesses = optimal.g[n - 1][optimal_l];
}
// final result object
return {
password: password,
guesses: guesses,
guesses_log10: Math.log10(guesses),
sequence: optimal_match_sequence,
score: 0,
};
}
exports.most_guessable_match_sequence = most_guessable_match_sequence;
// ------------------------------------------------------------------------------
// guess estimation -- one function per match pattern ---------------------------
// ------------------------------------------------------------------------------
function estimate_guesses(match, password) {
if (match.guesses) {
return match.guesses;
} // a match's guess estimate doesn't change. cache it.
var min_guesses = 1;
if (match.token.length < password.length) {
min_guesses =
match.token.length === 1
? bruteforce_guesses_1.MIN_SUBMATCH_GUESSES_SINGLE_CHAR
: bruteforce_guesses_1.MIN_SUBMATCH_GUESSES_MULTI_CHAR;
}
var guesses;
switch (match.pattern) {
case "bruteforce":
guesses = bruteforce_guesses_1.bruteforce_guesses(match);
break;
case "date":
guesses = date_guesses_1.date_guesses(match);
break;
case "dictionary":
guesses = dictionary_guesses_1.dictionary_guesses(match);
break;
case "regex":
guesses = regex_guesses_1.regex_guesses(match);
break;
case "repeat":
guesses = repeat_guesses_1.repeat_guesses(match);
break;
case "sequence":
guesses = sequence_guesses_1.sequence_guesses(match);
break;
case "spatial":
guesses = spatial_guesses_1.spatial_guesses(match);
break;
}
match.guesses = Math.max(guesses, min_guesses);
match.guesses_log10 = Math.log10(match.guesses);
return match.guesses;
}
exports.estimate_guesses = estimate_guesses;
//# sourceMappingURL=index.js.map