this.zxcvbnts = this.zxcvbnts || {}; this.zxcvbnts.core = (function (exports) { 'use strict'; const empty = obj => Object.keys(obj).length === 0; const extend = (listToExtend, list) => // eslint-disable-next-line prefer-spread listToExtend.push.apply(listToExtend, list); const translate = (string, chrMap) => { const tempArray = string.split(''); return tempArray.map(char => chrMap[char] || char).join(''); }; // mod implementation that works for negative numbers const sorted = matches => matches.sort((m1, m2) => m1.i - m2.i || m1.j - m2.j); const buildRankedDictionary = orderedList => { const result = {}; let counter = 1; // rank starts at 1, not 0 orderedList.forEach(word => { result[word] = counter; counter += 1; }); return result; }; var dateSplits = { 4: [// for length-4 strings, eg 1191 or 9111, two ways to split: [1, 2], [2, 3] // 91 1 1 ], 5: [[1, 3], [2, 3], // [2, 3], // 91 1 11 <- duplicate previous one [2, 4] // 91 11 1 <- New and must be added as bug fix ], 6: [[1, 2], [2, 4], [4, 5] // 1991 1 1 ], // 1111991 7: [[1, 3], [2, 3], [4, 5], [4, 6] // 1991 11 1 ], 8: [[2, 4], [4, 6] // 1991 11 11 ] }; const DATE_MAX_YEAR = 2050; const DATE_MIN_YEAR = 1000; const DATE_SPLITS = dateSplits; const BRUTEFORCE_CARDINALITY = 10; const MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10000; const MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10; const MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50; const MIN_YEAR_SPACE = 20; // \xbf-\xdf is a range for almost all special uppercase letter like Ä and so on const START_UPPER = /^[A-Z\xbf-\xdf][^A-Z\xbf-\xdf]+$/; const END_UPPER = /^[^A-Z\xbf-\xdf]+[A-Z\xbf-\xdf]$/; // \xdf-\xff is a range for almost all special lowercase letter like ä and so on const ALL_UPPER = /^[A-Z\xbf-\xdf]+$/; const ALL_UPPER_INVERTED = /^[^a-z\xdf-\xff]+$/; const ALL_LOWER = /^[a-z\xdf-\xff]+$/; const ALL_LOWER_INVERTED = /^[^A-Z\xbf-\xdf]+$/; const ONE_LOWER = /[a-z\xdf-\xff]/; const ONE_UPPER = /[A-Z\xbf-\xdf]/; const ALPHA_INVERTED = /[^A-Za-z\xbf-\xdf]/gi; const ALL_DIGIT = /^\d+$/; const REFERENCE_YEAR = new Date().getFullYear(); const REGEXEN = { recentYear: /19\d\d|200\d|201\d|202\d/g }; /* * ------------------------------------------------------------------------------- * date matching ---------------------------------------------------------------- * ------------------------------------------------------------------------------- */ class MatchDate { /* * a "date" is recognized as: * any 3-tuple that starts or ends with a 2- or 4-digit year, * with 2 or 0 separator chars (1.1.91 or 1191), * maybe zero-padded (01-01-91 vs 1-1-91), * a month between 1 and 12, * a day between 1 and 31. * * note: this isn't true date parsing in that "feb 31st" is allowed, * this doesn't check for leap years, etc. * * recipe: * start with regex to find maybe-dates, then attempt to map the integers * onto month-day-year to filter the maybe-dates into dates. * finally, remove matches that are substrings of other matches to reduce noise. * * note: instead of using a lazy or greedy regex to find many dates over the full string, * this uses a ^...$ regex against every substring of the password -- less performant but leads * to every possible date match. */ match({ password }) { const matches = [...this.getMatchesWithoutSeparator(password), ...this.getMatchesWithSeparator(password)]; const filteredMatches = this.filterNoise(matches); return sorted(filteredMatches); } getMatchesWithSeparator(password) { const matches = []; const maybeDateWithSeparator = /^(\d{1,4})([\s/\\_.-])(\d{1,2})\2(\d{1,4})$/; // # dates with separators are between length 6 '1/1/91' and 10 '11/11/1991' for (let i = 0; i <= Math.abs(password.length - 6); i += 1) { for (let j = i + 5; j <= i + 9; j += 1) { if (j >= password.length) { break; } const token = password.slice(i, +j + 1 || 9e9); const regexMatch = maybeDateWithSeparator.exec(token); if (regexMatch != null) { const dmy = this.mapIntegersToDayMonthYear([parseInt(regexMatch[1], 10), parseInt(regexMatch[3], 10), parseInt(regexMatch[4], 10)]); if (dmy != null) { matches.push({ pattern: 'date', token, i, j, separator: regexMatch[2], year: dmy.year, month: dmy.month, day: dmy.day }); } } } } return matches; } // eslint-disable-next-line max-statements getMatchesWithoutSeparator(password) { const matches = []; const maybeDateNoSeparator = /^\d{4,8}$/; const metric = candidate => Math.abs(candidate.year - REFERENCE_YEAR); // # dates without separators are between length 4 '1191' and 8 '11111991' for (let i = 0; i <= Math.abs(password.length - 4); i += 1) { for (let j = i + 3; j <= i + 7; j += 1) { if (j >= password.length) { break; } const token = password.slice(i, +j + 1 || 9e9); if (maybeDateNoSeparator.exec(token)) { const candidates = []; const index = token.length; const splittedDates = DATE_SPLITS[index]; splittedDates.forEach(([k, l]) => { const dmy = this.mapIntegersToDayMonthYear([parseInt(token.slice(0, k), 10), parseInt(token.slice(k, l), 10), parseInt(token.slice(l), 10)]); if (dmy != null) { candidates.push(dmy); } }); if (candidates.length > 0) { /* * at this point: different possible dmy mappings for the same i,j substring. * match the candidate date that likely takes the fewest guesses: a year closest * to 2000. * (scoring.REFERENCE_YEAR). * * ie, considering '111504', prefer 11-15-04 to 1-1-1504 * (interpreting '04' as 2004) */ let bestCandidate = candidates[0]; let minDistance = metric(candidates[0]); candidates.slice(1).forEach(candidate => { const distance = metric(candidate); if (distance < minDistance) { bestCandidate = candidate; minDistance = distance; } }); matches.push({ pattern: 'date', token, i, j, separator: '', year: bestCandidate.year, month: bestCandidate.month, day: bestCandidate.day }); } } } } return matches; } /* * matches now contains all valid date strings in a way that is tricky to capture * with regexes only. while thorough, it will contain some unintuitive noise: * * '2015_06_04', in addition to matching 2015_06_04, will also contain * 5(!) other date matches: 15_06_04, 5_06_04, ..., even 2015 (matched as 5/1/2020) * * to reduce noise, remove date matches that are strict substrings of others */ filterNoise(matches) { return matches.filter(match => { let isSubmatch = false; const matchesLength = matches.length; for (let o = 0; o < matchesLength; o += 1) { const otherMatch = matches[o]; if (match !== otherMatch) { if (otherMatch.i <= match.i && otherMatch.j >= match.j) { isSubmatch = true; break; } } } return !isSubmatch; }); } /* * given a 3-tuple, discard if: * middle int is over 31 (for all dmy formats, years are never allowed in the middle) * middle int is zero * any int is over the max allowable year * any int is over two digits but under the min allowable year * 2 integers are over 31, the max allowable day * 2 integers are zero * all integers are over 12, the max allowable month */ // eslint-disable-next-line complexity, max-statements mapIntegersToDayMonthYear(integers) { if (integers[1] > 31 || integers[1] <= 0) { return null; } let over12 = 0; let over31 = 0; let under1 = 0; for (let o = 0, len1 = integers.length; o < len1; o += 1) { const int = integers[o]; if (int > 99 && int < DATE_MIN_YEAR || int > DATE_MAX_YEAR) { return null; } if (int > 31) { over31 += 1; } if (int > 12) { over12 += 1; } if (int <= 0) { under1 += 1; } } if (over31 >= 2 || over12 === 3 || under1 >= 2) { return null; } return this.getDayMonth(integers); } // eslint-disable-next-line max-statements getDayMonth(integers) { // first look for a four digit year: yyyy + daymonth or daymonth + yyyy const possibleYearSplits = [[integers[2], integers.slice(0, 2)], [integers[0], integers.slice(1, 3)] // year first ]; const possibleYearSplitsLength = possibleYearSplits.length; for (let j = 0; j < possibleYearSplitsLength; j += 1) { const [y, rest] = possibleYearSplits[j]; if (DATE_MIN_YEAR <= y && y <= DATE_MAX_YEAR) { const dm = this.mapIntegersToDayMonth(rest); if (dm != null) { return { year: y, month: dm.month, day: dm.day }; } /* * for a candidate that includes a four-digit year, * when the remaining integers don't match to a day and month, * it is not a date. */ return null; } } // given no four-digit year, two digit years are the most flexible int to match, so // try to parse a day-month out of integers[0..1] or integers[1..0] for (let k = 0; k < possibleYearSplitsLength; k += 1) { const [y, rest] = possibleYearSplits[k]; const dm = this.mapIntegersToDayMonth(rest); if (dm != null) { return { year: this.twoToFourDigitYear(y), month: dm.month, day: dm.day }; } } return null; } mapIntegersToDayMonth(integers) { const temp = [integers, integers.slice().reverse()]; for (let i = 0; i < temp.length; i += 1) { const data = temp[i]; const day = data[0]; const month = data[1]; if (day >= 1 && day <= 31 && month >= 1 && month <= 12) { return { day, month }; } } return null; } twoToFourDigitYear(year) { if (year > 99) { return year; } if (year > 50) { // 87 -> 1987 return year + 1900; } // 15 -> 2015 return year + 2000; } } /** * This code is from https://github.com/ka-weihe/fastest-levenshtein * It was copied into this repo because it doesn't have an esm build which results in error for esm only project * TODO if sometimes in the future it will get a esm build we can remove this file and use the original again * https://github.com/ka-weihe/fastest-levenshtein/pull/18 */ const peq = new Uint32Array(0x10000); const myers_32 = (a, b) => { const n = a.length; const m = b.length; const lst = 1 << n - 1; let pv = -1; let mv = 0; let sc = n; let i = n; while (i--) { peq[a.charCodeAt(i)] |= 1 << i; } for (i = 0; i < m; i++) { let eq = peq[b.charCodeAt(i)]; const xv = eq | mv; eq |= (eq & pv) + pv ^ pv; mv |= ~(eq | pv); pv &= eq; if (mv & lst) { sc++; } if (pv & lst) { sc--; } mv = mv << 1 | 1; pv = pv << 1 | ~(xv | mv); mv &= xv; } i = n; while (i--) { peq[a.charCodeAt(i)] = 0; } return sc; }; const myers_x = (b, a) => { const n = a.length; const m = b.length; const mhc = []; const phc = []; const hsize = Math.ceil(n / 32); const vsize = Math.ceil(m / 32); for (let i = 0; i < hsize; i++) { phc[i] = -1; mhc[i] = 0; } let j = 0; for (; j < vsize - 1; j++) { let mv = 0; let pv = -1; const start = j * 32; const vlen = Math.min(32, m) + start; for (let k = start; k < vlen; k++) { peq[b.charCodeAt(k)] |= 1 << k; } for (let i = 0; i < n; i++) { const eq = peq[a.charCodeAt(i)]; const pb = phc[i / 32 | 0] >>> i % 32 & 1; const mb = mhc[i / 32 | 0] >>> i % 32 & 1; const xv = eq | mv; const xh = ((eq | mb) & pv) + pv ^ pv | eq | mb; let ph = mv | ~(xh | pv); let mh = pv & xh; if (ph >>> 31 ^ pb) { phc[i / 32 | 0] ^= 1 << i % 32; } if (mh >>> 31 ^ mb) { mhc[i / 32 | 0] ^= 1 << i % 32; } ph = ph << 1 | pb; mh = mh << 1 | mb; pv = mh | ~(xv | ph); mv = ph & xv; } for (let k = start; k < vlen; k++) { peq[b.charCodeAt(k)] = 0; } } let mv = 0; let pv = -1; const start = j * 32; const vlen = Math.min(32, m - start) + start; for (let k = start; k < vlen; k++) { peq[b.charCodeAt(k)] |= 1 << k; } let score = m; for (let i = 0; i < n; i++) { const eq = peq[a.charCodeAt(i)]; const pb = phc[i / 32 | 0] >>> i % 32 & 1; const mb = mhc[i / 32 | 0] >>> i % 32 & 1; const xv = eq | mv; const xh = ((eq | mb) & pv) + pv ^ pv | eq | mb; let ph = mv | ~(xh | pv); let mh = pv & xh; score += ph >>> m % 32 - 1 & 1; score -= mh >>> m % 32 - 1 & 1; if (ph >>> 31 ^ pb) { phc[i / 32 | 0] ^= 1 << i % 32; } if (mh >>> 31 ^ mb) { mhc[i / 32 | 0] ^= 1 << i % 32; } ph = ph << 1 | pb; mh = mh << 1 | mb; pv = mh | ~(xv | ph); mv = ph & xv; } for (let k = start; k < vlen; k++) { peq[b.charCodeAt(k)] = 0; } return score; }; const distance = (a, b) => { if (a.length < b.length) { const tmp = b; b = a; a = tmp; } if (b.length === 0) { return a.length; } if (a.length <= 32) { return myers_32(a, b); } return myers_x(a, b); }; const getUsedThreshold = (password, entry, threshold) => { const isPasswordToShort = password.length <= entry.length; const isThresholdLongerThanPassword = password.length <= threshold; const shouldUsePasswordLength = isPasswordToShort || isThresholdLongerThanPassword; // if password is too small use the password length divided by 4 while the threshold needs to be at least 1 return shouldUsePasswordLength ? Math.ceil(password.length / 4) : threshold; }; const findLevenshteinDistance = (password, rankedDictionary, threshold) => { let foundDistance = 0; const found = Object.keys(rankedDictionary).find(entry => { const usedThreshold = getUsedThreshold(password, entry, threshold); const foundEntryDistance = distance(password, entry); const isInThreshold = foundEntryDistance <= usedThreshold; if (isInThreshold) { foundDistance = foundEntryDistance; } return isInThreshold; }); if (found) { return { levenshteinDistance: foundDistance, levenshteinDistanceEntry: found }; } return {}; }; var l33tTable = { a: ['4', '@'], b: ['8'], c: ['(', '{', '[', '<'], e: ['3'], g: ['6', '9'], i: ['1', '!', '|'], l: ['1', '|', '7'], o: ['0'], s: ['$', '5'], t: ['+', '7'], x: ['%'], z: ['2'] }; var translationKeys = { warnings: { straightRow: 'straightRow', keyPattern: 'keyPattern', simpleRepeat: 'simpleRepeat', extendedRepeat: 'extendedRepeat', sequences: 'sequences', recentYears: 'recentYears', dates: 'dates', topTen: 'topTen', topHundred: 'topHundred', common: 'common', similarToCommon: 'similarToCommon', wordByItself: 'wordByItself', namesByThemselves: 'namesByThemselves', commonNames: 'commonNames', userInputs: 'userInputs', pwned: 'pwned' }, suggestions: { l33t: 'l33t', reverseWords: 'reverseWords', allUppercase: 'allUppercase', capitalization: 'capitalization', dates: 'dates', recentYears: 'recentYears', associatedYears: 'associatedYears', sequences: 'sequences', repeated: 'repeated', longerKeyboardPattern: 'longerKeyboardPattern', anotherWord: 'anotherWord', useWords: 'useWords', noNeed: 'noNeed', pwned: 'pwned' }, timeEstimation: { ltSecond: 'ltSecond', second: 'second', seconds: 'seconds', minute: 'minute', minutes: 'minutes', hour: 'hour', hours: 'hours', day: 'day', days: 'days', month: 'month', months: 'months', year: 'year', years: 'years', centuries: 'centuries' } }; class Options { constructor() { this.matchers = {}; this.l33tTable = l33tTable; this.dictionary = { userInputs: [] }; this.rankedDictionaries = {}; this.translations = translationKeys; this.graphs = {}; this.availableGraphs = []; this.useLevenshteinDistance = false; this.levenshteinThreshold = 2; this.setRankedDictionaries(); } setOptions(options = {}) { if (options.l33tTable) { this.l33tTable = options.l33tTable; } if (options.dictionary) { this.dictionary = options.dictionary; this.setRankedDictionaries(); } if (options.translations) { this.setTranslations(options.translations); } if (options.graphs) { this.graphs = options.graphs; } if (options.useLevenshteinDistance !== undefined) { this.useLevenshteinDistance = options.useLevenshteinDistance; } if (options.levenshteinThreshold !== undefined) { this.levenshteinThreshold = options.levenshteinThreshold; } } setTranslations(translations) { if (this.checkCustomTranslations(translations)) { this.translations = translations; } else { throw new Error('Invalid translations object fallback to keys'); } } checkCustomTranslations(translations) { let valid = true; Object.keys(translationKeys).forEach(type => { if (type in translations) { const translationType = type; Object.keys(translationKeys[translationType]).forEach(key => { if (!(key in translations[translationType])) { valid = false; } }); } else { valid = false; } }); return valid; } setRankedDictionaries() { const rankedDictionaries = {}; Object.keys(this.dictionary).forEach(name => { rankedDictionaries[name] = this.getRankedDictionary(name); }); this.rankedDictionaries = rankedDictionaries; } getRankedDictionary(name) { const list = this.dictionary[name]; if (name === 'userInputs') { const sanitizedInputs = []; list.forEach(input => { const inputType = typeof input; if (inputType === 'string' || inputType === 'number' || inputType === 'boolean') { sanitizedInputs.push(input.toString().toLowerCase()); } }); return buildRankedDictionary(sanitizedInputs); } return buildRankedDictionary(list); } extendUserInputsDictionary(dictionary) { if (this.dictionary.userInputs) { this.dictionary.userInputs = [...this.dictionary.userInputs, ...dictionary]; } else { this.dictionary.userInputs = dictionary; } this.rankedDictionaries.userInputs = this.getRankedDictionary('userInputs'); } addMatcher(name, matcher) { if (this.matchers[name]) { console.info(`Matcher ${name} already exists`); } else { this.matchers[name] = matcher; } } } const zxcvbnOptions = new Options(); /* * ------------------------------------------------------------------------------- * Dictionary reverse matching -------------------------------------------------- * ------------------------------------------------------------------------------- */ class MatchL33t$1 { constructor(defaultMatch) { this.defaultMatch = defaultMatch; } match({ password }) { const passwordReversed = password.split('').reverse().join(''); return this.defaultMatch({ password: passwordReversed }).map(match => ({ ...match, token: match.token.split('').reverse().join(''), reversed: true, // map coordinates back to original string i: password.length - 1 - match.j, j: password.length - 1 - match.i })); } } /* * ------------------------------------------------------------------------------- * Dictionary l33t matching ----------------------------------------------------- * ------------------------------------------------------------------------------- */ class MatchL33t { constructor(defaultMatch) { this.defaultMatch = defaultMatch; } match({ password }) { const matches = []; const enumeratedSubs = this.enumerateL33tSubs(this.relevantL33tSubtable(password, zxcvbnOptions.l33tTable)); for (let i = 0; i < enumeratedSubs.length; i += 1) { const sub = enumeratedSubs[i]; // corner case: password has no relevant subs. if (empty(sub)) { break; } const subbedPassword = translate(password, sub); const matchedDictionary = this.defaultMatch({ password: subbedPassword }); matchedDictionary.forEach(match => { const token = password.slice(match.i, +match.j + 1 || 9e9); // only return the matches that contain an actual substitution if (token.toLowerCase() !== match.matchedWord) { // subset of mappings in sub that are in use for this match const matchSub = {}; Object.keys(sub).forEach(subbedChr => { const chr = sub[subbedChr]; if (token.indexOf(subbedChr) !== -1) { matchSub[subbedChr] = chr; } }); const subDisplay = Object.keys(matchSub).map(k => `${k} -> ${matchSub[k]}`).join(', '); matches.push({ ...match, l33t: true, token, sub: matchSub, subDisplay }); } }); } // filter single-character l33t matches to reduce noise. // otherwise '1' matches 'i', '4' matches 'a', both very common English words // with low dictionary rank. return matches.filter(match => match.token.length > 1); } // makes a pruned copy of l33t_table that only includes password's possible substitutions relevantL33tSubtable(password, table) { const passwordChars = {}; const subTable = {}; password.split('').forEach(char => { passwordChars[char] = true; }); Object.keys(table).forEach(letter => { const subs = table[letter]; const relevantSubs = subs.filter(sub => sub in passwordChars); if (relevantSubs.length > 0) { subTable[letter] = relevantSubs; } }); return subTable; } // returns the list of possible 1337 replacement dictionaries for a given password enumerateL33tSubs(table) { const tableKeys = Object.keys(table); const subs = this.getSubs(tableKeys, [[]], table); // convert from assoc lists to dicts return subs.map(sub => { const subDict = {}; sub.forEach(([l33tChr, chr]) => { subDict[l33tChr] = chr; }); return subDict; }); } getSubs(keys, subs, table) { if (!keys.length) { return subs; } const firstKey = keys[0]; const restKeys = keys.slice(1); const nextSubs = []; table[firstKey].forEach(l33tChr => { subs.forEach(sub => { let dupL33tIndex = -1; for (let i = 0; i < sub.length; i += 1) { if (sub[i][0] === l33tChr) { dupL33tIndex = i; break; } } if (dupL33tIndex === -1) { const subExtension = sub.concat([[l33tChr, firstKey]]); nextSubs.push(subExtension); } else { const subAlternative = sub.slice(0); subAlternative.splice(dupL33tIndex, 1); subAlternative.push([l33tChr, firstKey]); nextSubs.push(sub); nextSubs.push(subAlternative); } }); }); const newSubs = this.dedup(nextSubs); if (restKeys.length) { return this.getSubs(restKeys, newSubs, table); } return newSubs; } dedup(subs) { const deduped = []; const members = {}; subs.forEach(sub => { const assoc = sub.map((k, index) => [k, index]); assoc.sort(); const label = assoc.map(([k, v]) => `${k},${v}`).join('-'); if (!(label in members)) { members[label] = true; deduped.push(sub); } }); return deduped; } } class MatchDictionary { constructor() { this.l33t = new MatchL33t(this.defaultMatch); this.reverse = new MatchL33t$1(this.defaultMatch); } match({ password }) { const matches = [...this.defaultMatch({ password }), ...this.reverse.match({ password }), ...this.l33t.match({ password })]; return sorted(matches); } defaultMatch({ password }) { const matches = []; const passwordLength = password.length; const passwordLower = password.toLowerCase(); // eslint-disable-next-line complexity Object.keys(zxcvbnOptions.rankedDictionaries).forEach(dictionaryName => { const rankedDict = zxcvbnOptions.rankedDictionaries[dictionaryName]; for (let i = 0; i < passwordLength; i += 1) { for (let j = i; j < passwordLength; j += 1) { const usedPassword = passwordLower.slice(i, +j + 1 || 9e9); const isInDictionary = (usedPassword in rankedDict); let foundLevenshteinDistance = {}; // only use levenshtein distance on full password to minimize the performance drop // and because otherwise there would be to many false positives const isFullPassword = i === 0 && j === passwordLength - 1; if (zxcvbnOptions.useLevenshteinDistance && isFullPassword && !isInDictionary) { foundLevenshteinDistance = findLevenshteinDistance(usedPassword, rankedDict, zxcvbnOptions.levenshteinThreshold); } const isLevenshteinMatch = Object.keys(foundLevenshteinDistance).length !== 0; if (isInDictionary || isLevenshteinMatch) { const usedRankPassword = isLevenshteinMatch ? foundLevenshteinDistance.levenshteinDistanceEntry : usedPassword; const rank = rankedDict[usedRankPassword]; matches.push({ pattern: 'dictionary', i, j, token: password.slice(i, +j + 1 || 9e9), matchedWord: usedPassword, rank, dictionaryName: dictionaryName, reversed: false, l33t: false, ...foundLevenshteinDistance }); } } } }); return matches; } } /* * ------------------------------------------------------------------------------- * regex matching --------------------------------------------------------------- * ------------------------------------------------------------------------------- */ class MatchRegex { match({ password, regexes = REGEXEN }) { const matches = []; Object.keys(regexes).forEach(name => { const regex = regexes[name]; regex.lastIndex = 0; // keeps regexMatch stateless const regexMatch = regex.exec(password); if (regexMatch) { const token = regexMatch[0]; matches.push({ pattern: 'regex', token, i: regexMatch.index, j: regexMatch.index + regexMatch[0].length - 1, regexName: name, regexMatch }); } }); return sorted(matches); } } var utils = { // binomial coefficients // src: http://blog.plover.com/math/choose.html nCk(n, k) { let count = n; if (k > count) { return 0; } if (k === 0) { return 1; } let coEff = 1; for (let i = 1; i <= k; i += 1) { coEff *= count; coEff /= i; count -= 1; } return coEff; }, log10(n) { return Math.log(n) / Math.log(10); // IE doesn't support Math.log10 :( }, log2(n) { return Math.log(n) / Math.log(2); }, factorial(num) { let rval = 1; for (let i = 2; i <= num; i += 1) rval *= i; return rval; } }; var bruteforceMatcher$1 = (({ token }) => { let guesses = BRUTEFORCE_CARDINALITY ** token.length; if (guesses === Number.POSITIVE_INFINITY) { guesses = Number.MAX_VALUE; } let minGuesses; // small detail: make bruteforce matches at minimum one guess bigger than smallest allowed // submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence. if (token.length === 1) { minGuesses = MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1; } else { minGuesses = MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1; } return Math.max(guesses, minGuesses); }); var dateMatcher$1 = (({ year, separator }) => { // base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years const yearSpace = Math.max(Math.abs(year - REFERENCE_YEAR), MIN_YEAR_SPACE); let guesses = yearSpace * 365; // add factor of 4 for separator selection (one of ~4 choices) if (separator) { guesses *= 4; } return guesses; }); const getVariations = cleanedWord => { const wordArray = cleanedWord.split(''); const upperCaseCount = wordArray.filter(char => char.match(ONE_UPPER)).length; const lowerCaseCount = wordArray.filter(char => char.match(ONE_LOWER)).length; let variations = 0; const variationLength = Math.min(upperCaseCount, lowerCaseCount); for (let i = 1; i <= variationLength; i += 1) { variations += utils.nCk(upperCaseCount + lowerCaseCount, i); } return variations; }; var uppercaseVariant = (word => { // clean words of non alpha characters to remove the reward effekt to capitalize the first letter https://github.com/dropbox/zxcvbn/issues/232 const cleanedWord = word.replace(ALPHA_INVERTED, ''); if (cleanedWord.match(ALL_LOWER_INVERTED) || cleanedWord.toLowerCase() === cleanedWord) { return 1; } // a capitalized word is the most common capitalization scheme, // so it only doubles the search space (uncapitalized + capitalized). // all caps and end-capitalized are common enough too, underestimate as 2x factor to be safe. const commonCases = [START_UPPER, END_UPPER, ALL_UPPER_INVERTED]; const commonCasesLength = commonCases.length; for (let i = 0; i < commonCasesLength; i += 1) { const regex = commonCases[i]; if (cleanedWord.match(regex)) { return 2; } } // otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters // with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD), // the number of ways to lowercase U+L letters with L lowercase letters or less. return getVariations(cleanedWord); }); const getCounts = ({ subs, subbed, token }) => { const unsubbed = subs[subbed]; // lower-case match.token before calculating: capitalization shouldn't affect l33t calc. const chrs = token.toLowerCase().split(''); // num of subbed chars const subbedCount = chrs.filter(char => char === subbed).length; // num of unsubbed chars const unsubbedCount = chrs.filter(char => char === unsubbed).length; return { subbedCount, unsubbedCount }; }; var l33tVariant = (({ l33t, sub, token }) => { if (!l33t) { return 1; } let variations = 1; const subs = sub; Object.keys(subs).forEach(subbed => { const { subbedCount, unsubbedCount } = getCounts({ subs, subbed, 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; }); var dictionaryMatcher$1 = (({ rank, reversed, l33t, sub, token }) => { const baseGuesses = rank; // keep these as properties for display purposes const uppercaseVariations = uppercaseVariant(token); const l33tVariations = l33tVariant({ l33t, sub, token }); const reversedVariations = reversed && 2 || 1; const calculation = baseGuesses * uppercaseVariations * l33tVariations * reversedVariations; return { baseGuesses, uppercaseVariations, l33tVariations, calculation }; }); var regexMatcher$1 = (({ regexName, regexMatch, token }) => { const charClassBases = { alphaLower: 26, alphaUpper: 26, alpha: 52, alphanumeric: 62, digits: 10, symbols: 33 }; if (regexName in charClassBases) { return charClassBases[regexName] ** token.length; } // TODO add more regex types for example special dates like 09.11 // eslint-disable-next-line default-case switch (regexName) { case 'recentYear': // conservative estimate of year space: num years from REFERENCE_YEAR. // if year is close to REFERENCE_YEAR, estimate a year space of MIN_YEAR_SPACE. return Math.max(Math.abs(parseInt(regexMatch[0], 10) - REFERENCE_YEAR), MIN_YEAR_SPACE); } return 0; }); var repeatMatcher$1 = (({ baseGuesses, repeatCount }) => baseGuesses * repeatCount); var sequenceMatcher$1 = (({ token, ascending }) => { const firstChr = token.charAt(0); let baseGuesses = 0; const startingPoints = ['a', 'A', 'z', 'Z', '0', '1', '9']; // lower guesses for obvious starting points if (startingPoints.includes(firstChr)) { baseGuesses = 4; } else if (firstChr.match(/\d/)) { baseGuesses = 10; // digits } else { // could give a higher base for uppercase, // assigning 26 to both upper and lower sequences is more conservative. baseGuesses = 26; } // need to try a descending sequence in addition to every ascending sequence -> // 2x guesses if (!ascending) { baseGuesses *= 2; } return baseGuesses * token.length; }); const calcAverageDegree = graph => { let average = 0; Object.keys(graph).forEach(key => { const neighbors = graph[key]; average += neighbors.filter(entry => !!entry).length; }); average /= Object.entries(graph).length; return average; }; const estimatePossiblePatterns = ({ token, graph, turns }) => { const startingPosition = Object.keys(zxcvbnOptions.graphs[graph]).length; const averageDegree = calcAverageDegree(zxcvbnOptions.graphs[graph]); let guesses = 0; const tokenLength = token.length; // # estimate the number of possible patterns w/ tokenLength or less with turns or less. for (let i = 2; i <= tokenLength; i += 1) { const possibleTurns = Math.min(turns, i - 1); for (let j = 1; j <= possibleTurns; j += 1) { guesses += utils.nCk(i - 1, j - 1) * startingPosition * averageDegree ** j; } } return guesses; }; var spatialMatcher$1 = (({ graph, token, shiftedCount, turns }) => { let guesses = estimatePossiblePatterns({ token, graph, turns }); // 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 (shiftedCount) { const unShiftedCount = token.length - shiftedCount; if (shiftedCount === 0 || unShiftedCount === 0) { guesses *= 2; } else { let shiftedVariations = 0; for (let i = 1; i <= Math.min(shiftedCount, unShiftedCount); i += 1) { shiftedVariations += utils.nCk(shiftedCount + unShiftedCount, i); } guesses *= shiftedVariations; } } return Math.round(guesses); }); const getMinGuesses = (match, password) => { let minGuesses = 1; if (match.token.length < password.length) { if (match.token.length === 1) { minGuesses = MIN_SUBMATCH_GUESSES_SINGLE_CHAR; } else { minGuesses = MIN_SUBMATCH_GUESSES_MULTI_CHAR; } } return minGuesses; }; const matchers = { bruteforce: bruteforceMatcher$1, date: dateMatcher$1, dictionary: dictionaryMatcher$1, regex: regexMatcher$1, repeat: repeatMatcher$1, sequence: sequenceMatcher$1, spatial: spatialMatcher$1 }; const getScoring = (name, match) => { if (matchers[name]) { return matchers[name](match); } if (zxcvbnOptions.matchers[name] && 'scoring' in zxcvbnOptions.matchers[name]) { return zxcvbnOptions.matchers[name].scoring(match); } return 0; }; // ------------------------------------------------------------------------------ // guess estimation -- one function per match pattern --------------------------- // ------------------------------------------------------------------------------ var estimateGuesses = ((match, password) => { const extraData = {}; // a match's guess estimate doesn't change. cache it. if ('guesses' in match && match.guesses != null) { return match; } const minGuesses = getMinGuesses(match, password); const estimationResult = getScoring(match.pattern, match); let guesses = 0; if (typeof estimationResult === 'number') { guesses = estimationResult; } else if (match.pattern === 'dictionary') { guesses = estimationResult.calculation; extraData.baseGuesses = estimationResult.baseGuesses; extraData.uppercaseVariations = estimationResult.uppercaseVariations; extraData.l33tVariations = estimationResult.l33tVariations; } const matchGuesses = Math.max(guesses, minGuesses); return { ...match, ...extraData, guesses: matchGuesses, guessesLog10: utils.log10(matchGuesses) }; }); const scoringHelper = { password: '', optimal: {}, excludeAdditive: false, fillArray(size, valueType) { const result = []; for (let i = 0; i < size; i += 1) { let value = []; if (valueType === 'object') { value = {}; } result.push(value); } return result; }, // helper: make bruteforce match objects spanning i to j, inclusive. makeBruteforceMatch(i, j) { return { pattern: 'bruteforce', token: this.password.slice(i, +j + 1 || 9e9), i, j }; }, // helper: considers whether a length-sequenceLength // sequence ending at match m is better (fewer guesses) // than previously encountered sequences, updating state if so. update(match, sequenceLength) { const k = match.j; const estimatedMatch = estimateGuesses(match, this.password); let pi = estimatedMatch.guesses; if (sequenceLength > 1) { // we're considering a length-sequenceLength sequence ending with match m: // obtain the product term in the minimization function by multiplying m's guesses // by the product of the length-(sequenceLength-1) // sequence ending just before m, at m.i - 1. pi *= this.optimal.pi[estimatedMatch.i - 1][sequenceLength - 1]; } // calculate the minimization func let g = utils.factorial(sequenceLength) * pi; if (!this.excludeAdditive) { g += MIN_GUESSES_BEFORE_GROWING_SEQUENCE ** (sequenceLength - 1); } // update state if new best. // first see if any competing sequences covering this prefix, // with sequenceLength or fewer matches, // fare better than this sequence. if so, skip it and return. let shouldSkip = false; Object.keys(this.optimal.g[k]).forEach(competingPatternLength => { const competingMetricMatch = this.optimal.g[k][competingPatternLength]; if (parseInt(competingPatternLength, 10) <= sequenceLength) { if (competingMetricMatch <= g) { shouldSkip = true; } } }); if (!shouldSkip) { // this sequence might be part of the final optimal sequence. this.optimal.g[k][sequenceLength] = g; this.optimal.m[k][sequenceLength] = estimatedMatch; this.optimal.pi[k][sequenceLength] = pi; } }, // helper: evaluate bruteforce matches ending at passwordCharIndex. bruteforceUpdate(passwordCharIndex) { // see if a single bruteforce match spanning the passwordCharIndex-prefix is optimal. let match = this.makeBruteforceMatch(0, passwordCharIndex); this.update(match, 1); for (let i = 1; i <= passwordCharIndex; i += 1) { // generate passwordCharIndex bruteforce matches, spanning from (i=1, j=passwordCharIndex) up to (i=passwordCharIndex, j=passwordCharIndex). // see if adding these new matches to any of the sequences in optimal[i-1] // leads to new bests. match = this.makeBruteforceMatch(i, passwordCharIndex); const tmp = this.optimal.m[i - 1]; // eslint-disable-next-line no-loop-func Object.keys(tmp).forEach(sequenceLength => { const lastMatch = tmp[sequenceLength]; // corner: an optimal sequence will never have two adjacent bruteforce matches. // it is strictly better to have a single bruteforce match spanning the same region: // same contribution to the guess product with a lower length. // --> safe to skip those cases. if (lastMatch.pattern !== 'bruteforce') { // try adding m to this length-sequenceLength sequence. this.update(match, parseInt(sequenceLength, 10) + 1); } }); } }, // helper: step backwards through optimal.m starting at the end, // constructing the final optimal match sequence. unwind(passwordLength) { const optimalMatchSequence = []; let k = passwordLength - 1; // find the final best sequence length and score let sequenceLength = 0; // eslint-disable-next-line no-loss-of-precision let g = 2e308; const temp = this.optimal.g[k]; // safety check for empty passwords if (temp) { Object.keys(temp).forEach(candidateSequenceLength => { const candidateMetricMatch = temp[candidateSequenceLength]; if (candidateMetricMatch < g) { sequenceLength = parseInt(candidateSequenceLength, 10); g = candidateMetricMatch; } }); } while (k >= 0) { const match = this.optimal.m[k][sequenceLength]; optimalMatchSequence.unshift(match); k = match.i - 1; sequenceLength -= 1; } return optimalMatchSequence; } }; var scoring = { // ------------------------------------------------------------------------------ // 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 = sequenceLength! * Product(m.guesses for m in sequence) + D^(sequenceLength - 1) // // where sequenceLength is the length of the sequence. // // the factorial term is the number of ways to order sequenceLength patterns. // // the D^(sequenceLength-1) term is another length penalty, roughly capturing the idea that an // attacker will try lower-length sequences first before trying length-sequenceLength 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^(sequenceLength-1) approximates Sum(D^i for i in [1..sequenceLength-1] // // ------------------------------------------------------------------------------ mostGuessableMatchSequence(password, matches, excludeAdditive = false) { scoringHelper.password = password; scoringHelper.excludeAdditive = excludeAdditive; const passwordLength = password.length; // partition matches into sublists according to ending index j let matchesByCoordinateJ = scoringHelper.fillArray(passwordLength, 'array'); matches.forEach(match => { matchesByCoordinateJ[match.j].push(match); }); // small detail: for deterministic output, sort each sublist by i. matchesByCoordinateJ = matchesByCoordinateJ.map(match => match.sort((m1, m2) => m1.i - m2.i)); scoringHelper.optimal = { // optimal.m[k][sequenceLength] holds final match in the best length-sequenceLength // match sequence covering the // password prefix up to k, inclusive. // if there is no length-sequenceLength sequence that scores better (fewer guesses) than // a shorter match sequence spanning the same prefix, // optimal.m[k][sequenceLength] is undefined. m: scoringHelper.fillArray(passwordLength, 'object'), // 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: scoringHelper.fillArray(passwordLength, 'object'), // same structure as optimal.m -- holds the overall metric. g: scoringHelper.fillArray(passwordLength, 'object') }; for (let k = 0; k < passwordLength; k += 1) { matchesByCoordinateJ[k].forEach(match => { if (match.i > 0) { Object.keys(scoringHelper.optimal.m[match.i - 1]).forEach(sequenceLength => { scoringHelper.update(match, parseInt(sequenceLength, 10) + 1); }); } else { scoringHelper.update(match, 1); } }); scoringHelper.bruteforceUpdate(k); } const optimalMatchSequence = scoringHelper.unwind(passwordLength); const optimalSequenceLength = optimalMatchSequence.length; const guesses = this.getGuesses(password, optimalSequenceLength); return { password, guesses, guessesLog10: utils.log10(guesses), sequence: optimalMatchSequence }; }, getGuesses(password, optimalSequenceLength) { const passwordLength = password.length; let guesses = 0; if (password.length === 0) { guesses = 1; } else { guesses = scoringHelper.optimal.g[passwordLength - 1][optimalSequenceLength]; } return guesses; } }; /* *------------------------------------------------------------------------------- * repeats (aaa, abcabcabc) ------------------------------ *------------------------------------------------------------------------------- */ class MatchRepeat { // eslint-disable-next-line max-statements match({ password, omniMatch }) { const matches = []; let lastIndex = 0; while (lastIndex < password.length) { const greedyMatch = this.getGreedyMatch(password, lastIndex); const lazyMatch = this.getLazyMatch(password, lastIndex); if (greedyMatch == null) { break; } const { match, baseToken } = this.setMatchToken(greedyMatch, lazyMatch); if (match) { const j = match.index + match[0].length - 1; const baseGuesses = this.getBaseGuesses(baseToken, omniMatch); matches.push(this.normalizeMatch(baseToken, j, match, baseGuesses)); lastIndex = j + 1; } } const hasPromises = matches.some(match => { return match instanceof Promise; }); if (hasPromises) { return Promise.all(matches); } return matches; } // eslint-disable-next-line max-params normalizeMatch(baseToken, j, match, baseGuesses) { const baseMatch = { pattern: 'repeat', i: match.index, j, token: match[0], baseToken, baseGuesses: 0, repeatCount: match[0].length / baseToken.length }; if (baseGuesses instanceof Promise) { return baseGuesses.then(resolvedBaseGuesses => { return { ...baseMatch, baseGuesses: resolvedBaseGuesses }; }); } return { ...baseMatch, baseGuesses }; } getGreedyMatch(password, lastIndex) { const greedy = /(.+)\1+/g; greedy.lastIndex = lastIndex; return greedy.exec(password); } getLazyMatch(password, lastIndex) { const lazy = /(.+?)\1+/g; lazy.lastIndex = lastIndex; return lazy.exec(password); } setMatchToken(greedyMatch, lazyMatch) { const lazyAnchored = /^(.+?)\1+$/; let match; let baseToken = ''; if (lazyMatch && greedyMatch[0].length > lazyMatch[0].length) { // greedy beats lazy for 'aabaab' // greedy: [aabaab, aab] // lazy: [aa, a] match = greedyMatch; // greedy's repeated string might itself be repeated, eg. // aabaab in aabaabaabaab. // run an anchored lazy match on greedy's repeated string // to find the shortest repeated string const temp = lazyAnchored.exec(match[0]); if (temp) { baseToken = temp[1]; } } else { // lazy beats greedy for 'aaaaa' // greedy: [aaaa, aa] // lazy: [aaaaa, a] match = lazyMatch; if (match) { baseToken = match[1]; } } return { match, baseToken }; } getBaseGuesses(baseToken, omniMatch) { const matches = omniMatch.match(baseToken); if (matches instanceof Promise) { return matches.then(resolvedMatches => { const baseAnalysis = scoring.mostGuessableMatchSequence(baseToken, resolvedMatches); return baseAnalysis.guesses; }); } const baseAnalysis = scoring.mostGuessableMatchSequence(baseToken, matches); return baseAnalysis.guesses; } } /* *------------------------------------------------------------------------------- * sequences (abcdef) ------------------------------ *------------------------------------------------------------------------------- */ class MatchSequence { constructor() { this.MAX_DELTA = 5; } // eslint-disable-next-line max-statements match({ password }) { /* * Identifies sequences by looking for repeated differences in unicode codepoint. * this allows skipping, such as 9753, and also matches some extended unicode sequences * such as Greek and Cyrillic alphabets. * * for example, consider the input 'abcdb975zy' * * password: a b c d b 9 7 5 z y * index: 0 1 2 3 4 5 6 7 8 9 * delta: 1 1 1 -2 -41 -2 -2 69 1 * * expected result: * [(i, j, delta), ...] = [(0, 3, 1), (5, 7, -2), (8, 9, 1)] */ const result = []; if (password.length === 1) { return []; } let i = 0; let lastDelta = null; const passwordLength = password.length; for (let k = 1; k < passwordLength; k += 1) { const delta = password.charCodeAt(k) - password.charCodeAt(k - 1); if (lastDelta == null) { lastDelta = delta; } if (delta !== lastDelta) { const j = k - 1; this.update({ i, j, delta: lastDelta, password, result }); i = j; lastDelta = delta; } } this.update({ i, j: passwordLength - 1, delta: lastDelta, password, result }); return result; } update({ i, j, delta, password, result }) { if (j - i > 1 || Math.abs(delta) === 1) { const absoluteDelta = Math.abs(delta); if (absoluteDelta > 0 && absoluteDelta <= this.MAX_DELTA) { const token = password.slice(i, +j + 1 || 9e9); const { sequenceName, sequenceSpace } = this.getSequence(token); return result.push({ pattern: 'sequence', i, j, token: password.slice(i, +j + 1 || 9e9), sequenceName, sequenceSpace, ascending: delta > 0 }); } } return null; } getSequence(token) { // TODO conservatively stick with roman alphabet size. // (this could be improved) let sequenceName = 'unicode'; let sequenceSpace = 26; if (ALL_LOWER.test(token)) { sequenceName = 'lower'; sequenceSpace = 26; } else if (ALL_UPPER.test(token)) { sequenceName = 'upper'; sequenceSpace = 26; } else if (ALL_DIGIT.test(token)) { sequenceName = 'digits'; sequenceSpace = 10; } return { sequenceName, sequenceSpace }; } } /* * ------------------------------------------------------------------------------ * spatial match (qwerty/dvorak/keypad and so on) ----------------------------------------- * ------------------------------------------------------------------------------ */ class MatchSpatial { constructor() { this.SHIFTED_RX = /[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]/; } match({ password }) { const matches = []; Object.keys(zxcvbnOptions.graphs).forEach(graphName => { const graph = zxcvbnOptions.graphs[graphName]; extend(matches, this.helper(password, graph, graphName)); }); return sorted(matches); } checkIfShifted(graphName, password, index) { 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, graph, graphName) { let shiftedCount; const matches = []; let i = 0; const passwordLength = password.length; while (i < passwordLength - 1) { let j = i + 1; let lastDirection = 0; let turns = 0; shiftedCount = this.checkIfShifted(graphName, password, i); // eslint-disable-next-line no-constant-condition while (true) { const prevChar = password.charAt(j - 1); const adjacents = graph[prevChar] || []; let found = false; let foundDirection = -1; 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; foundDirection = curDirection; // 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; } // 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; } } class Matching { constructor() { this.matchers = { date: MatchDate, dictionary: MatchDictionary, regex: MatchRegex, // @ts-ignore => TODO resolve this type issue. This is because it is possible to be async repeat: MatchRepeat, sequence: MatchSequence, spatial: MatchSpatial }; } match(password) { const matches = []; const promises = []; const matchers = [...Object.keys(this.matchers), ...Object.keys(zxcvbnOptions.matchers)]; matchers.forEach(key => { if (!this.matchers[key] && !zxcvbnOptions.matchers[key]) { return; } const Matcher = this.matchers[key] ? this.matchers[key] : zxcvbnOptions.matchers[key].Matching; const usedMatcher = new Matcher(); const result = usedMatcher.match({ password, omniMatch: this }); if (result instanceof Promise) { result.then(response => { extend(matches, response); }); promises.push(result); } else { extend(matches, result); } }); if (promises.length > 0) { return new Promise(resolve => { Promise.all(promises).then(() => { resolve(sorted(matches)); }); }); } return sorted(matches); } } const SECOND = 1; const MINUTE = SECOND * 60; const HOUR = MINUTE * 60; const DAY = HOUR * 24; const MONTH = DAY * 31; const YEAR = MONTH * 12; const CENTURY = YEAR * 100; const times = { second: SECOND, minute: MINUTE, hour: HOUR, day: DAY, month: MONTH, year: YEAR, century: CENTURY }; /* * ------------------------------------------------------------------------------- * Estimates time for an attacker --------------------------------------------------------------- * ------------------------------------------------------------------------------- */ class TimeEstimates { translate(displayStr, value) { let key = displayStr; if (value !== undefined && value !== 1) { key += 's'; } const { timeEstimation } = zxcvbnOptions.translations; return timeEstimation[key].replace('{base}', `${value}`); } estimateAttackTimes(guesses) { const crackTimesSeconds = { onlineThrottling100PerHour: guesses / (100 / 3600), onlineNoThrottling10PerSecond: guesses / 10, offlineSlowHashing1e4PerSecond: guesses / 1e4, offlineFastHashing1e10PerSecond: guesses / 1e10 }; const crackTimesDisplay = { onlineThrottling100PerHour: '', onlineNoThrottling10PerSecond: '', offlineSlowHashing1e4PerSecond: '', offlineFastHashing1e10PerSecond: '' }; Object.keys(crackTimesSeconds).forEach(scenario => { const seconds = crackTimesSeconds[scenario]; crackTimesDisplay[scenario] = this.displayTime(seconds); }); return { crackTimesSeconds, crackTimesDisplay, score: this.guessesToScore(guesses) }; } guessesToScore(guesses) { const DELTA = 5; if (guesses < 1e3 + DELTA) { // risky password: "too guessable" return 0; } if (guesses < 1e6 + DELTA) { // modest protection from throttled online attacks: "very guessable" return 1; } if (guesses < 1e8 + DELTA) { // modest protection from unthrottled online attacks: "somewhat guessable" return 2; } if (guesses < 1e10 + DELTA) { // modest protection from offline attacks: "safely unguessable" // assuming a salted, slow hash function like bcrypt, scrypt, PBKDF2, argon, etc return 3; } // strong protection from offline attacks under same scenario: "very unguessable" return 4; } displayTime(seconds) { let displayStr = 'centuries'; let base; const timeKeys = Object.keys(times); const foundIndex = timeKeys.findIndex(time => seconds < times[time]); if (foundIndex > -1) { displayStr = timeKeys[foundIndex - 1]; if (foundIndex !== 0) { base = Math.round(seconds / times[displayStr]); } else { displayStr = 'ltSecond'; } } return this.translate(displayStr, base); } } var bruteforceMatcher = (() => { return null; }); var dateMatcher = (() => { return { warning: zxcvbnOptions.translations.warnings.dates, suggestions: [zxcvbnOptions.translations.suggestions.dates] }; }); const getDictionaryWarningPassword = (match, isSoleMatch) => { let warning = ''; if (isSoleMatch && !match.l33t && !match.reversed) { if (match.rank <= 10) { warning = zxcvbnOptions.translations.warnings.topTen; } else if (match.rank <= 100) { warning = zxcvbnOptions.translations.warnings.topHundred; } else { warning = zxcvbnOptions.translations.warnings.common; } } else if (match.guessesLog10 <= 4) { warning = zxcvbnOptions.translations.warnings.similarToCommon; } return warning; }; const getDictionaryWarningWikipedia = (match, isSoleMatch) => { let warning = ''; if (isSoleMatch) { warning = zxcvbnOptions.translations.warnings.wordByItself; } return warning; }; const getDictionaryWarningNames = (match, isSoleMatch) => { if (isSoleMatch) { return zxcvbnOptions.translations.warnings.namesByThemselves; } return zxcvbnOptions.translations.warnings.commonNames; }; const getDictionaryWarning = (match, isSoleMatch) => { let warning = ''; const dictName = match.dictionaryName; const isAName = dictName === 'lastnames' || dictName.toLowerCase().includes('firstnames'); if (dictName === 'passwords') { warning = getDictionaryWarningPassword(match, isSoleMatch); } else if (dictName.includes('wikipedia')) { warning = getDictionaryWarningWikipedia(match, isSoleMatch); } else if (isAName) { warning = getDictionaryWarningNames(match, isSoleMatch); } else if (dictName === 'userInputs') { warning = zxcvbnOptions.translations.warnings.userInputs; } return warning; }; var dictionaryMatcher = ((match, isSoleMatch) => { const warning = getDictionaryWarning(match, isSoleMatch); const suggestions = []; const word = match.token; if (word.match(START_UPPER)) { suggestions.push(zxcvbnOptions.translations.suggestions.capitalization); } else if (word.match(ALL_UPPER_INVERTED) && word.toLowerCase() !== word) { suggestions.push(zxcvbnOptions.translations.suggestions.allUppercase); } if (match.reversed && match.token.length >= 4) { suggestions.push(zxcvbnOptions.translations.suggestions.reverseWords); } if (match.l33t) { suggestions.push(zxcvbnOptions.translations.suggestions.l33t); } return { warning, suggestions }; }); var regexMatcher = (match => { if (match.regexName === 'recentYear') { return { warning: zxcvbnOptions.translations.warnings.recentYears, suggestions: [zxcvbnOptions.translations.suggestions.recentYears, zxcvbnOptions.translations.suggestions.associatedYears] }; } return { warning: '', suggestions: [] }; }); var repeatMatcher = (match => { let warning = zxcvbnOptions.translations.warnings.extendedRepeat; if (match.baseToken.length === 1) { warning = zxcvbnOptions.translations.warnings.simpleRepeat; } return { warning, suggestions: [zxcvbnOptions.translations.suggestions.repeated] }; }); var sequenceMatcher = (() => { return { warning: zxcvbnOptions.translations.warnings.sequences, suggestions: [zxcvbnOptions.translations.suggestions.sequences] }; }); var spatialMatcher = (match => { let warning = zxcvbnOptions.translations.warnings.keyPattern; if (match.turns === 1) { warning = zxcvbnOptions.translations.warnings.straightRow; } return { warning, suggestions: [zxcvbnOptions.translations.suggestions.longerKeyboardPattern] }; }); const defaultFeedback = { warning: '', suggestions: [] }; /* * ------------------------------------------------------------------------------- * Generate feedback --------------------------------------------------------------- * ------------------------------------------------------------------------------- */ class Feedback { constructor() { this.matchers = { bruteforce: bruteforceMatcher, date: dateMatcher, dictionary: dictionaryMatcher, regex: regexMatcher, repeat: repeatMatcher, sequence: sequenceMatcher, spatial: spatialMatcher }; this.defaultFeedback = { warning: '', suggestions: [] }; this.setDefaultSuggestions(); } setDefaultSuggestions() { this.defaultFeedback.suggestions.push(zxcvbnOptions.translations.suggestions.useWords, zxcvbnOptions.translations.suggestions.noNeed); } getFeedback(score, sequence) { if (sequence.length === 0) { return this.defaultFeedback; } if (score > 2) { return defaultFeedback; } const extraFeedback = zxcvbnOptions.translations.suggestions.anotherWord; const longestMatch = this.getLongestMatch(sequence); let feedback = this.getMatchFeedback(longestMatch, sequence.length === 1); if (feedback !== null && feedback !== undefined) { feedback.suggestions.unshift(extraFeedback); if (feedback.warning == null) { feedback.warning = ''; } } else { feedback = { warning: '', suggestions: [extraFeedback] }; } return feedback; } getLongestMatch(sequence) { let longestMatch = sequence[0]; const slicedSequence = sequence.slice(1); slicedSequence.forEach(match => { if (match.token.length > longestMatch.token.length) { longestMatch = match; } }); return longestMatch; } getMatchFeedback(match, isSoleMatch) { if (this.matchers[match.pattern]) { return this.matchers[match.pattern](match, isSoleMatch); } if (zxcvbnOptions.matchers[match.pattern] && 'feedback' in zxcvbnOptions.matchers[match.pattern]) { return zxcvbnOptions.matchers[match.pattern].feedback(match, isSoleMatch); } return defaultFeedback; } } /** * @link https://davidwalsh.name/javascript-debounce-function */ var debounce = ((func, wait, isImmediate) => { let timeout; return function debounce(...args) { const context = this; const later = () => { timeout = undefined; if (!isImmediate) { func.apply(context, args); } }; const shouldCallNow = isImmediate && !timeout; if (timeout !== undefined) { clearTimeout(timeout); } timeout = setTimeout(later, wait); if (shouldCallNow) { return func.apply(context, args); } return undefined; }; }); const time = () => new Date().getTime(); const createReturnValue = (resolvedMatches, password, start) => { const feedback = new Feedback(); const timeEstimates = new TimeEstimates(); const matchSequence = scoring.mostGuessableMatchSequence(password, resolvedMatches); const calcTime = time() - start; const attackTimes = timeEstimates.estimateAttackTimes(matchSequence.guesses); return { calcTime, ...matchSequence, ...attackTimes, feedback: feedback.getFeedback(attackTimes.score, matchSequence.sequence) }; }; const main = (password, userInputs) => { if (userInputs) { zxcvbnOptions.extendUserInputsDictionary(userInputs); } const matching = new Matching(); return matching.match(password); }; const zxcvbn = (password, userInputs) => { const start = time(); const matches = main(password, userInputs); if (matches instanceof Promise) { throw new Error('You are using a Promised matcher, please use `zxcvbnAsync` for it.'); } return createReturnValue(matches, password, start); }; const zxcvbnAsync = async (password, userInputs) => { const start = time(); const matches = await main(password, userInputs); return createReturnValue(matches, password, start); }; exports.debounce = debounce; exports.zxcvbn = zxcvbn; exports.zxcvbnAsync = zxcvbnAsync; exports.zxcvbnOptions = zxcvbnOptions; Object.defineProperty(exports, '__esModule', { value: true }); return exports; })({}); //# sourceMappingURL=zxcvbn-ts.js.map