coffeenode-zxcvbn
Version:
realistic password strength estimation
391 lines (362 loc) • 10.7 kB
JavaScript
// Generated by CoffeeScript 1.6.3
var ALL_LOWER, ALL_UPPER, END_UPPER, NUM_ATTACKERS, NUM_DAYS, NUM_MONTHS, NUM_YEARS, SECONDS_PER_GUESS, SINGLE_GUESS, START_UPPER, calc_bruteforce_cardinality, calc_entropy, crack_time_to_score, date_entropy, dictionary_entropy, digits_entropy, display_time, entropy_to_crack_time, extra_l33t_entropy, extra_uppercase_entropy, lg, minimum_entropy_match_sequence, nCk, repeat_entropy, round_to_x_digits, sequence_entropy, spatial_entropy, year_entropy;
nCk = function(n, k) {
var d, r, _i;
if (k > n) {
return 0;
}
if (k === 0) {
return 1;
}
r = 1;
for (d = _i = 1; 1 <= k ? _i <= k : _i >= k; d = 1 <= k ? ++_i : --_i) {
r *= n;
r /= d;
n -= 1;
}
return r;
};
lg = function(n) {
return Math.log(n) / Math.log(2);
};
minimum_entropy_match_sequence = function(password, matches) {
var backpointers, bruteforce_cardinality, candidate_entropy, crack_time, i, j, k, make_bruteforce_match, match, match_sequence, match_sequence_copy, min_entropy, up_to_k, _i, _j, _k, _len, _len1, _ref, _ref1, _ref2;
bruteforce_cardinality = calc_bruteforce_cardinality(password);
up_to_k = [];
backpointers = [];
for (k = _i = 0, _ref = password.length; 0 <= _ref ? _i < _ref : _i > _ref; k = 0 <= _ref ? ++_i : --_i) {
up_to_k[k] = (up_to_k[k - 1] || 0) + lg(bruteforce_cardinality);
backpointers[k] = null;
for (_j = 0, _len = matches.length; _j < _len; _j++) {
match = matches[_j];
if (!(match.j === k)) {
continue;
}
_ref1 = [match.i, match.j], i = _ref1[0], j = _ref1[1];
candidate_entropy = (up_to_k[i - 1] || 0) + calc_entropy(match);
if (candidate_entropy < up_to_k[j]) {
up_to_k[j] = candidate_entropy;
backpointers[j] = match;
}
}
}
match_sequence = [];
k = password.length - 1;
while (k >= 0) {
match = backpointers[k];
if (match) {
match_sequence.push(match);
k = match.i - 1;
} else {
k -= 1;
}
}
match_sequence.reverse();
make_bruteforce_match = function(i, j) {
return {
pattern: 'bruteforce',
i: i,
j: j,
token: password.slice(i, +j + 1 || 9e9),
entropy: lg(Math.pow(bruteforce_cardinality, j - i + 1)),
cardinality: bruteforce_cardinality
};
};
k = 0;
match_sequence_copy = [];
for (_k = 0, _len1 = match_sequence.length; _k < _len1; _k++) {
match = match_sequence[_k];
_ref2 = [match.i, match.j], i = _ref2[0], j = _ref2[1];
if (i - k > 0) {
match_sequence_copy.push(make_bruteforce_match(k, i - 1));
}
k = j + 1;
match_sequence_copy.push(match);
}
if (k < password.length) {
match_sequence_copy.push(make_bruteforce_match(k, password.length - 1));
}
match_sequence = match_sequence_copy;
min_entropy = up_to_k[password.length - 1] || 0;
crack_time = entropy_to_crack_time(min_entropy);
return {
password: password,
entropy: round_to_x_digits(min_entropy, 3),
match_sequence: match_sequence,
crack_time: round_to_x_digits(crack_time, 3),
crack_time_display: display_time(crack_time),
score: crack_time_to_score(crack_time)
};
};
round_to_x_digits = function(n, x) {
return Math.round(n * Math.pow(10, x)) / Math.pow(10, x);
};
SINGLE_GUESS = .010;
NUM_ATTACKERS = 100;
SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS;
entropy_to_crack_time = function(entropy) {
return .5 * Math.pow(2, entropy) * SECONDS_PER_GUESS;
};
crack_time_to_score = function(seconds) {
if (seconds < Math.pow(10, 2)) {
return 0;
}
if (seconds < Math.pow(10, 4)) {
return 1;
}
if (seconds < Math.pow(10, 6)) {
return 2;
}
if (seconds < Math.pow(10, 8)) {
return 3;
}
return 4;
};
calc_entropy = function(match) {
var entropy_func;
if (match.entropy != null) {
return match.entropy;
}
entropy_func = (function() {
switch (match.pattern) {
case 'repeat':
return repeat_entropy;
case 'sequence':
return sequence_entropy;
case 'digits':
return digits_entropy;
case 'year':
return year_entropy;
case 'date':
return date_entropy;
case 'spatial':
return spatial_entropy;
case 'dictionary':
return dictionary_entropy;
}
})();
return match.entropy = entropy_func(match);
};
repeat_entropy = function(match) {
var cardinality;
cardinality = calc_bruteforce_cardinality(match.token);
return lg(cardinality * match.token.length);
};
sequence_entropy = function(match) {
var base_entropy, first_chr;
first_chr = match.token.charAt(0);
if (first_chr === 'a' || first_chr === '1') {
base_entropy = 1;
} else {
if (first_chr.match(/\d/)) {
base_entropy = lg(10);
} else if (first_chr.match(/[a-z]/)) {
base_entropy = lg(26);
} else {
base_entropy = lg(26) + 1;
}
}
if (!match.ascending) {
base_entropy += 1;
}
return base_entropy + lg(match.token.length);
};
digits_entropy = function(match) {
return lg(Math.pow(10, match.token.length));
};
NUM_YEARS = 119;
NUM_MONTHS = 12;
NUM_DAYS = 31;
year_entropy = function(match) {
return lg(NUM_YEARS);
};
date_entropy = function(match) {
var entropy;
if (match.year < 100) {
entropy = lg(NUM_DAYS * NUM_MONTHS * 100);
} else {
entropy = lg(NUM_DAYS * NUM_MONTHS * NUM_YEARS);
}
if (match.separator) {
entropy += 2;
}
return entropy;
};
spatial_entropy = function(match) {
var L, S, U, d, entropy, i, j, possibilities, possible_turns, s, t, _i, _j, _k, _ref, _ref1;
if ((_ref = match.graph) === 'qwerty' || _ref === 'dvorak') {
s = KEYBOARD_STARTING_POSITIONS;
d = KEYBOARD_AVERAGE_DEGREE;
} else {
s = KEYPAD_STARTING_POSITIONS;
d = KEYPAD_AVERAGE_DEGREE;
}
possibilities = 0;
L = match.token.length;
t = match.turns;
for (i = _i = 2; 2 <= L ? _i <= L : _i >= L; i = 2 <= L ? ++_i : --_i) {
possible_turns = Math.min(t, i - 1);
for (j = _j = 1; 1 <= possible_turns ? _j <= possible_turns : _j >= possible_turns; j = 1 <= possible_turns ? ++_j : --_j) {
possibilities += nCk(i - 1, j - 1) * s * Math.pow(d, j);
}
}
entropy = lg(possibilities);
if (match.shifted_count) {
S = match.shifted_count;
U = match.token.length - match.shifted_count;
possibilities = 0;
for (i = _k = 0, _ref1 = Math.min(S, U); 0 <= _ref1 ? _k <= _ref1 : _k >= _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
possibilities += nCk(S + U, i);
}
entropy += lg(possibilities);
}
return entropy;
};
dictionary_entropy = function(match) {
match.base_entropy = lg(match.rank);
match.uppercase_entropy = extra_uppercase_entropy(match);
match.l33t_entropy = extra_l33t_entropy(match);
return match.base_entropy + match.uppercase_entropy + match.l33t_entropy;
};
START_UPPER = /^[A-Z][^A-Z]+$/;
END_UPPER = /^[^A-Z]+[A-Z]$/;
ALL_UPPER = /^[^a-z]+$/;
ALL_LOWER = /^[^A-Z]+$/;
extra_uppercase_entropy = function(match) {
var L, U, chr, i, possibilities, regex, word, _i, _j, _len, _ref, _ref1;
word = match.token;
if (word.match(ALL_LOWER)) {
return 0;
}
_ref = [START_UPPER, END_UPPER, ALL_UPPER];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
regex = _ref[_i];
if (word.match(regex)) {
return 1;
}
}
U = ((function() {
var _j, _len1, _ref1, _results;
_ref1 = word.split('');
_results = [];
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
chr = _ref1[_j];
if (chr.match(/[A-Z]/)) {
_results.push(chr);
}
}
return _results;
})()).length;
L = ((function() {
var _j, _len1, _ref1, _results;
_ref1 = word.split('');
_results = [];
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
chr = _ref1[_j];
if (chr.match(/[a-z]/)) {
_results.push(chr);
}
}
return _results;
})()).length;
possibilities = 0;
for (i = _j = 0, _ref1 = Math.min(U, L); 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) {
possibilities += nCk(U + L, i);
}
return lg(possibilities);
};
extra_l33t_entropy = function(match) {
var S, U, chr, i, possibilities, subbed, unsubbed, _i, _ref, _ref1;
if (!match.l33t) {
return 0;
}
possibilities = 0;
_ref = match.sub;
for (subbed in _ref) {
unsubbed = _ref[subbed];
S = ((function() {
var _i, _len, _ref1, _results;
_ref1 = match.token.split('');
_results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
chr = _ref1[_i];
if (chr === subbed) {
_results.push(chr);
}
}
return _results;
})()).length;
U = ((function() {
var _i, _len, _ref1, _results;
_ref1 = match.token.split('');
_results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
chr = _ref1[_i];
if (chr === unsubbed) {
_results.push(chr);
}
}
return _results;
})()).length;
for (i = _i = 0, _ref1 = Math.min(U, S); 0 <= _ref1 ? _i <= _ref1 : _i >= _ref1; i = 0 <= _ref1 ? ++_i : --_i) {
possibilities += nCk(U + S, i);
}
}
return lg(possibilities) || 1;
};
calc_bruteforce_cardinality = function(password) {
var c, chr, digits, lower, ord, symbols, upper, _i, _len, _ref, _ref1;
_ref = [false, false, false, false], lower = _ref[0], upper = _ref[1], digits = _ref[2], symbols = _ref[3];
_ref1 = password.split('');
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
chr = _ref1[_i];
ord = chr.charCodeAt(0);
if ((0x30 <= ord && ord <= 0x39)) {
digits = true;
} else if ((0x41 <= ord && ord <= 0x5a)) {
upper = true;
} else if ((0x61 <= ord && ord <= 0x7a)) {
lower = true;
} else {
symbols = true;
}
}
c = 0;
if (digits) {
c += 10;
}
if (upper) {
c += 26;
}
if (lower) {
c += 26;
}
if (symbols) {
c += 33;
}
return c;
};
display_time = function(seconds) {
var century, day, hour, minute, month, year;
minute = 60;
hour = minute * 60;
day = hour * 24;
month = day * 31;
year = month * 12;
century = year * 100;
if (seconds < minute) {
return 'instant';
} else if (seconds < hour) {
return "" + (1 + Math.ceil(seconds / minute)) + " minutes";
} else if (seconds < day) {
return "" + (1 + Math.ceil(seconds / hour)) + " hours";
} else if (seconds < month) {
return "" + (1 + Math.ceil(seconds / day)) + " days";
} else if (seconds < year) {
return "" + (1 + Math.ceil(seconds / month)) + " months";
} else if (seconds < century) {
return "" + (1 + Math.ceil(seconds / year)) + " years";
} else {
return 'centuries';
}
};