zxcvbn-ts.js 78 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216
  1. this.zxcvbnts = this.zxcvbnts || {};
  2. this.zxcvbnts.core = (function (exports) {
  3. 'use strict';
  4. const empty = obj => Object.keys(obj).length === 0;
  5. const extend = (listToExtend, list) =>
  6. // eslint-disable-next-line prefer-spread
  7. listToExtend.push.apply(listToExtend, list);
  8. const translate = (string, chrMap) => {
  9. const tempArray = string.split('');
  10. return tempArray.map(char => chrMap[char] || char).join('');
  11. };
  12. // sort on i primary, j secondary
  13. const sorted = matches => matches.sort((m1, m2) => m1.i - m2.i || m1.j - m2.j);
  14. const buildRankedDictionary = orderedList => {
  15. const result = {};
  16. let counter = 1; // rank starts at 1, not 0
  17. orderedList.forEach(word => {
  18. result[word] = counter;
  19. counter += 1;
  20. });
  21. return result;
  22. };
  23. var dateSplits = {
  24. 4: [
  25. // for length-4 strings, eg 1191 or 9111, two ways to split:
  26. [1, 2], [2, 3] // 91 1 1
  27. ],
  28. 5: [[1, 3], [2, 3],
  29. // [2, 3], // 91 1 11 <- duplicate previous one
  30. [2, 4] // 91 11 1 <- New and must be added as bug fix
  31. ],
  32. 6: [[1, 2], [2, 4], [4, 5] // 1991 1 1
  33. ],
  34. // 1111991
  35. 7: [[1, 3], [2, 3], [4, 5], [4, 6] // 1991 11 1
  36. ],
  37. 8: [[2, 4], [4, 6] // 1991 11 11
  38. ]
  39. };
  40. const DATE_MAX_YEAR = 2050;
  41. const DATE_MIN_YEAR = 1000;
  42. const DATE_SPLITS = dateSplits;
  43. const BRUTEFORCE_CARDINALITY = 10;
  44. const MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10000;
  45. const MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10;
  46. const MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50;
  47. const MIN_YEAR_SPACE = 20;
  48. // \xbf-\xdf is a range for almost all special uppercase letter like Ä and so on
  49. const START_UPPER = /^[A-Z\xbf-\xdf][^A-Z\xbf-\xdf]+$/;
  50. const END_UPPER = /^[^A-Z\xbf-\xdf]+[A-Z\xbf-\xdf]$/;
  51. // \xdf-\xff is a range for almost all special lowercase letter like ä and so on
  52. const ALL_UPPER = /^[A-Z\xbf-\xdf]+$/;
  53. const ALL_UPPER_INVERTED = /^[^a-z\xdf-\xff]+$/;
  54. const ALL_LOWER = /^[a-z\xdf-\xff]+$/;
  55. const ALL_LOWER_INVERTED = /^[^A-Z\xbf-\xdf]+$/;
  56. const ONE_LOWER = /[a-z\xdf-\xff]/;
  57. const ONE_UPPER = /[A-Z\xbf-\xdf]/;
  58. const ALPHA_INVERTED = /[^A-Za-z\xbf-\xdf]/gi;
  59. const ALL_DIGIT = /^\d+$/;
  60. const REFERENCE_YEAR = new Date().getFullYear();
  61. const REGEXEN = {
  62. recentYear: /19\d\d|200\d|201\d|202\d/g
  63. };
  64. /*
  65. * -------------------------------------------------------------------------------
  66. * date matching ----------------------------------------------------------------
  67. * -------------------------------------------------------------------------------
  68. */
  69. class MatchDate {
  70. /*
  71. * a "date" is recognized as:
  72. * any 3-tuple that starts or ends with a 2- or 4-digit year,
  73. * with 2 or 0 separator chars (1.1.91 or 1191),
  74. * maybe zero-padded (01-01-91 vs 1-1-91),
  75. * a month between 1 and 12,
  76. * a day between 1 and 31.
  77. *
  78. * note: this isn't true date parsing in that "feb 31st" is allowed,
  79. * this doesn't check for leap years, etc.
  80. *
  81. * recipe:
  82. * start with regex to find maybe-dates, then attempt to map the integers
  83. * onto month-day-year to filter the maybe-dates into dates.
  84. * finally, remove matches that are substrings of other matches to reduce noise.
  85. *
  86. * note: instead of using a lazy or greedy regex to find many dates over the full string,
  87. * this uses a ^...$ regex against every substring of the password -- less performant but leads
  88. * to every possible date match.
  89. */
  90. match({
  91. password
  92. }) {
  93. const matches = [...this.getMatchesWithoutSeparator(password), ...this.getMatchesWithSeparator(password)];
  94. const filteredMatches = this.filterNoise(matches);
  95. return sorted(filteredMatches);
  96. }
  97. getMatchesWithSeparator(password) {
  98. const matches = [];
  99. const maybeDateWithSeparator = /^(\d{1,4})([\s/\\_.-])(\d{1,2})\2(\d{1,4})$/;
  100. // # dates with separators are between length 6 '1/1/91' and 10 '11/11/1991'
  101. for (let i = 0; i <= Math.abs(password.length - 6); i += 1) {
  102. for (let j = i + 5; j <= i + 9; j += 1) {
  103. if (j >= password.length) {
  104. break;
  105. }
  106. const token = password.slice(i, +j + 1 || 9e9);
  107. const regexMatch = maybeDateWithSeparator.exec(token);
  108. if (regexMatch != null) {
  109. const dmy = this.mapIntegersToDayMonthYear([parseInt(regexMatch[1], 10), parseInt(regexMatch[3], 10), parseInt(regexMatch[4], 10)]);
  110. if (dmy != null) {
  111. matches.push({
  112. pattern: 'date',
  113. token,
  114. i,
  115. j,
  116. separator: regexMatch[2],
  117. year: dmy.year,
  118. month: dmy.month,
  119. day: dmy.day
  120. });
  121. }
  122. }
  123. }
  124. }
  125. return matches;
  126. }
  127. // eslint-disable-next-line max-statements
  128. getMatchesWithoutSeparator(password) {
  129. const matches = [];
  130. const maybeDateNoSeparator = /^\d{4,8}$/;
  131. const metric = candidate => Math.abs(candidate.year - REFERENCE_YEAR);
  132. // # dates without separators are between length 4 '1191' and 8 '11111991'
  133. for (let i = 0; i <= Math.abs(password.length - 4); i += 1) {
  134. for (let j = i + 3; j <= i + 7; j += 1) {
  135. if (j >= password.length) {
  136. break;
  137. }
  138. const token = password.slice(i, +j + 1 || 9e9);
  139. if (maybeDateNoSeparator.exec(token)) {
  140. const candidates = [];
  141. const index = token.length;
  142. const splittedDates = DATE_SPLITS[index];
  143. splittedDates.forEach(([k, l]) => {
  144. const dmy = this.mapIntegersToDayMonthYear([parseInt(token.slice(0, k), 10), parseInt(token.slice(k, l), 10), parseInt(token.slice(l), 10)]);
  145. if (dmy != null) {
  146. candidates.push(dmy);
  147. }
  148. });
  149. if (candidates.length > 0) {
  150. /*
  151. * at this point: different possible dmy mappings for the same i,j substring.
  152. * match the candidate date that likely takes the fewest guesses: a year closest
  153. * to 2000.
  154. * (scoring.REFERENCE_YEAR).
  155. *
  156. * ie, considering '111504', prefer 11-15-04 to 1-1-1504
  157. * (interpreting '04' as 2004)
  158. */
  159. let bestCandidate = candidates[0];
  160. let minDistance = metric(candidates[0]);
  161. candidates.slice(1).forEach(candidate => {
  162. const distance = metric(candidate);
  163. if (distance < minDistance) {
  164. bestCandidate = candidate;
  165. minDistance = distance;
  166. }
  167. });
  168. matches.push({
  169. pattern: 'date',
  170. token,
  171. i,
  172. j,
  173. separator: '',
  174. year: bestCandidate.year,
  175. month: bestCandidate.month,
  176. day: bestCandidate.day
  177. });
  178. }
  179. }
  180. }
  181. }
  182. return matches;
  183. }
  184. /*
  185. * matches now contains all valid date strings in a way that is tricky to capture
  186. * with regexes only. while thorough, it will contain some unintuitive noise:
  187. *
  188. * '2015_06_04', in addition to matching 2015_06_04, will also contain
  189. * 5(!) other date matches: 15_06_04, 5_06_04, ..., even 2015 (matched as 5/1/2020)
  190. *
  191. * to reduce noise, remove date matches that are strict substrings of others
  192. */
  193. filterNoise(matches) {
  194. return matches.filter(match => {
  195. let isSubmatch = false;
  196. const matchesLength = matches.length;
  197. for (let o = 0; o < matchesLength; o += 1) {
  198. const otherMatch = matches[o];
  199. if (match !== otherMatch) {
  200. if (otherMatch.i <= match.i && otherMatch.j >= match.j) {
  201. isSubmatch = true;
  202. break;
  203. }
  204. }
  205. }
  206. return !isSubmatch;
  207. });
  208. }
  209. /*
  210. * given a 3-tuple, discard if:
  211. * middle int is over 31 (for all dmy formats, years are never allowed in the middle)
  212. * middle int is zero
  213. * any int is over the max allowable year
  214. * any int is over two digits but under the min allowable year
  215. * 2 integers are over 31, the max allowable day
  216. * 2 integers are zero
  217. * all integers are over 12, the max allowable month
  218. */
  219. // eslint-disable-next-line complexity, max-statements
  220. mapIntegersToDayMonthYear(integers) {
  221. if (integers[1] > 31 || integers[1] <= 0) {
  222. return null;
  223. }
  224. let over12 = 0;
  225. let over31 = 0;
  226. let under1 = 0;
  227. for (let o = 0, len1 = integers.length; o < len1; o += 1) {
  228. const int = integers[o];
  229. if (int > 99 && int < DATE_MIN_YEAR || int > DATE_MAX_YEAR) {
  230. return null;
  231. }
  232. if (int > 31) {
  233. over31 += 1;
  234. }
  235. if (int > 12) {
  236. over12 += 1;
  237. }
  238. if (int <= 0) {
  239. under1 += 1;
  240. }
  241. }
  242. if (over31 >= 2 || over12 === 3 || under1 >= 2) {
  243. return null;
  244. }
  245. return this.getDayMonth(integers);
  246. }
  247. // eslint-disable-next-line max-statements
  248. getDayMonth(integers) {
  249. // first look for a four digit year: yyyy + daymonth or daymonth + yyyy
  250. const possibleYearSplits = [[integers[2], integers.slice(0, 2)], [integers[0], integers.slice(1, 3)] // year first
  251. ];
  252. const possibleYearSplitsLength = possibleYearSplits.length;
  253. for (let j = 0; j < possibleYearSplitsLength; j += 1) {
  254. const [y, rest] = possibleYearSplits[j];
  255. if (DATE_MIN_YEAR <= y && y <= DATE_MAX_YEAR) {
  256. const dm = this.mapIntegersToDayMonth(rest);
  257. if (dm != null) {
  258. return {
  259. year: y,
  260. month: dm.month,
  261. day: dm.day
  262. };
  263. }
  264. /*
  265. * for a candidate that includes a four-digit year,
  266. * when the remaining integers don't match to a day and month,
  267. * it is not a date.
  268. */
  269. return null;
  270. }
  271. }
  272. // given no four-digit year, two digit years are the most flexible int to match, so
  273. // try to parse a day-month out of integers[0..1] or integers[1..0]
  274. for (let k = 0; k < possibleYearSplitsLength; k += 1) {
  275. const [y, rest] = possibleYearSplits[k];
  276. const dm = this.mapIntegersToDayMonth(rest);
  277. if (dm != null) {
  278. return {
  279. year: this.twoToFourDigitYear(y),
  280. month: dm.month,
  281. day: dm.day
  282. };
  283. }
  284. }
  285. return null;
  286. }
  287. mapIntegersToDayMonth(integers) {
  288. const temp = [integers, integers.slice().reverse()];
  289. for (let i = 0; i < temp.length; i += 1) {
  290. const data = temp[i];
  291. const day = data[0];
  292. const month = data[1];
  293. if (day >= 1 && day <= 31 && month >= 1 && month <= 12) {
  294. return {
  295. day,
  296. month
  297. };
  298. }
  299. }
  300. return null;
  301. }
  302. twoToFourDigitYear(year) {
  303. if (year > 99) {
  304. return year;
  305. }
  306. if (year > 50) {
  307. // 87 -> 1987
  308. return year + 1900;
  309. }
  310. // 15 -> 2015
  311. return year + 2000;
  312. }
  313. }
  314. const peq = new Uint32Array(0x10000);
  315. const myers_32 = (a, b) => {
  316. const n = a.length;
  317. const m = b.length;
  318. const lst = 1 << (n - 1);
  319. let pv = -1;
  320. let mv = 0;
  321. let sc = n;
  322. let i = n;
  323. while (i--) {
  324. peq[a.charCodeAt(i)] |= 1 << i;
  325. }
  326. for (i = 0; i < m; i++) {
  327. let eq = peq[b.charCodeAt(i)];
  328. const xv = eq | mv;
  329. eq |= ((eq & pv) + pv) ^ pv;
  330. mv |= ~(eq | pv);
  331. pv &= eq;
  332. if (mv & lst) {
  333. sc++;
  334. }
  335. if (pv & lst) {
  336. sc--;
  337. }
  338. mv = (mv << 1) | 1;
  339. pv = (pv << 1) | ~(xv | mv);
  340. mv &= xv;
  341. }
  342. i = n;
  343. while (i--) {
  344. peq[a.charCodeAt(i)] = 0;
  345. }
  346. return sc;
  347. };
  348. const myers_x = (b, a) => {
  349. const n = a.length;
  350. const m = b.length;
  351. const mhc = [];
  352. const phc = [];
  353. const hsize = Math.ceil(n / 32);
  354. const vsize = Math.ceil(m / 32);
  355. for (let i = 0; i < hsize; i++) {
  356. phc[i] = -1;
  357. mhc[i] = 0;
  358. }
  359. let j = 0;
  360. for (; j < vsize - 1; j++) {
  361. let mv = 0;
  362. let pv = -1;
  363. const start = j * 32;
  364. const vlen = Math.min(32, m) + start;
  365. for (let k = start; k < vlen; k++) {
  366. peq[b.charCodeAt(k)] |= 1 << k;
  367. }
  368. for (let i = 0; i < n; i++) {
  369. const eq = peq[a.charCodeAt(i)];
  370. const pb = (phc[(i / 32) | 0] >>> i) & 1;
  371. const mb = (mhc[(i / 32) | 0] >>> i) & 1;
  372. const xv = eq | mv;
  373. const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
  374. let ph = mv | ~(xh | pv);
  375. let mh = pv & xh;
  376. if ((ph >>> 31) ^ pb) {
  377. phc[(i / 32) | 0] ^= 1 << i;
  378. }
  379. if ((mh >>> 31) ^ mb) {
  380. mhc[(i / 32) | 0] ^= 1 << i;
  381. }
  382. ph = (ph << 1) | pb;
  383. mh = (mh << 1) | mb;
  384. pv = mh | ~(xv | ph);
  385. mv = ph & xv;
  386. }
  387. for (let k = start; k < vlen; k++) {
  388. peq[b.charCodeAt(k)] = 0;
  389. }
  390. }
  391. let mv = 0;
  392. let pv = -1;
  393. const start = j * 32;
  394. const vlen = Math.min(32, m - start) + start;
  395. for (let k = start; k < vlen; k++) {
  396. peq[b.charCodeAt(k)] |= 1 << k;
  397. }
  398. let score = m;
  399. for (let i = 0; i < n; i++) {
  400. const eq = peq[a.charCodeAt(i)];
  401. const pb = (phc[(i / 32) | 0] >>> i) & 1;
  402. const mb = (mhc[(i / 32) | 0] >>> i) & 1;
  403. const xv = eq | mv;
  404. const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
  405. let ph = mv | ~(xh | pv);
  406. let mh = pv & xh;
  407. score += (ph >>> (m - 1)) & 1;
  408. score -= (mh >>> (m - 1)) & 1;
  409. if ((ph >>> 31) ^ pb) {
  410. phc[(i / 32) | 0] ^= 1 << i;
  411. }
  412. if ((mh >>> 31) ^ mb) {
  413. mhc[(i / 32) | 0] ^= 1 << i;
  414. }
  415. ph = (ph << 1) | pb;
  416. mh = (mh << 1) | mb;
  417. pv = mh | ~(xv | ph);
  418. mv = ph & xv;
  419. }
  420. for (let k = start; k < vlen; k++) {
  421. peq[b.charCodeAt(k)] = 0;
  422. }
  423. return score;
  424. };
  425. const distance = (a, b) => {
  426. if (a.length < b.length) {
  427. const tmp = b;
  428. b = a;
  429. a = tmp;
  430. }
  431. if (b.length === 0) {
  432. return a.length;
  433. }
  434. if (a.length <= 32) {
  435. return myers_32(a, b);
  436. }
  437. return myers_x(a, b);
  438. };
  439. const getUsedThreshold = (password, entry, threshold) => {
  440. const isPasswordToShort = password.length <= entry.length;
  441. const isThresholdLongerThanPassword = password.length <= threshold;
  442. const shouldUsePasswordLength = isPasswordToShort || isThresholdLongerThanPassword;
  443. // if password is too small use the password length divided by 4 while the threshold needs to be at least 1
  444. return shouldUsePasswordLength ? Math.ceil(password.length / 4) : threshold;
  445. };
  446. const findLevenshteinDistance = (password, rankedDictionary, threshold) => {
  447. let foundDistance = 0;
  448. const found = Object.keys(rankedDictionary).find(entry => {
  449. const usedThreshold = getUsedThreshold(password, entry, threshold);
  450. const foundEntryDistance = distance(password, entry);
  451. const isInThreshold = foundEntryDistance <= usedThreshold;
  452. if (isInThreshold) {
  453. foundDistance = foundEntryDistance;
  454. }
  455. return isInThreshold;
  456. });
  457. if (found) {
  458. return {
  459. levenshteinDistance: foundDistance,
  460. levenshteinDistanceEntry: found
  461. };
  462. }
  463. return {};
  464. };
  465. var l33tTable = {
  466. a: ['4', '@'],
  467. b: ['8'],
  468. c: ['(', '{', '[', '<'],
  469. e: ['3'],
  470. g: ['6', '9'],
  471. i: ['1', '!', '|'],
  472. l: ['1', '|', '7'],
  473. o: ['0'],
  474. s: ['$', '5'],
  475. t: ['+', '7'],
  476. x: ['%'],
  477. z: ['2']
  478. };
  479. var translationKeys = {
  480. warnings: {
  481. straightRow: 'straightRow',
  482. keyPattern: 'keyPattern',
  483. simpleRepeat: 'simpleRepeat',
  484. extendedRepeat: 'extendedRepeat',
  485. sequences: 'sequences',
  486. recentYears: 'recentYears',
  487. dates: 'dates',
  488. topTen: 'topTen',
  489. topHundred: 'topHundred',
  490. common: 'common',
  491. similarToCommon: 'similarToCommon',
  492. wordByItself: 'wordByItself',
  493. namesByThemselves: 'namesByThemselves',
  494. commonNames: 'commonNames',
  495. userInputs: 'userInputs',
  496. pwned: 'pwned'
  497. },
  498. suggestions: {
  499. l33t: 'l33t',
  500. reverseWords: 'reverseWords',
  501. allUppercase: 'allUppercase',
  502. capitalization: 'capitalization',
  503. dates: 'dates',
  504. recentYears: 'recentYears',
  505. associatedYears: 'associatedYears',
  506. sequences: 'sequences',
  507. repeated: 'repeated',
  508. longerKeyboardPattern: 'longerKeyboardPattern',
  509. anotherWord: 'anotherWord',
  510. useWords: 'useWords',
  511. noNeed: 'noNeed',
  512. pwned: 'pwned'
  513. },
  514. timeEstimation: {
  515. ltSecond: 'ltSecond',
  516. second: 'second',
  517. seconds: 'seconds',
  518. minute: 'minute',
  519. minutes: 'minutes',
  520. hour: 'hour',
  521. hours: 'hours',
  522. day: 'day',
  523. days: 'days',
  524. month: 'month',
  525. months: 'months',
  526. year: 'year',
  527. years: 'years',
  528. centuries: 'centuries'
  529. }
  530. };
  531. class Options {
  532. constructor() {
  533. this.matchers = {};
  534. this.l33tTable = l33tTable;
  535. this.dictionary = {
  536. userInputs: []
  537. };
  538. this.rankedDictionaries = {};
  539. this.rankedDictionariesMaxWordSize = {};
  540. this.translations = translationKeys;
  541. this.graphs = {};
  542. this.useLevenshteinDistance = false;
  543. this.levenshteinThreshold = 2;
  544. this.l33tMaxSubstitutions = 100;
  545. this.maxLength = 256;
  546. this.setRankedDictionaries();
  547. }
  548. // eslint-disable-next-line max-statements,complexity
  549. setOptions(options = {}) {
  550. if (options.l33tTable) {
  551. this.l33tTable = options.l33tTable;
  552. }
  553. if (options.dictionary) {
  554. this.dictionary = options.dictionary;
  555. this.setRankedDictionaries();
  556. }
  557. if (options.translations) {
  558. this.setTranslations(options.translations);
  559. }
  560. if (options.graphs) {
  561. this.graphs = options.graphs;
  562. }
  563. if (options.useLevenshteinDistance !== undefined) {
  564. this.useLevenshteinDistance = options.useLevenshteinDistance;
  565. }
  566. if (options.levenshteinThreshold !== undefined) {
  567. this.levenshteinThreshold = options.levenshteinThreshold;
  568. }
  569. if (options.l33tMaxSubstitutions !== undefined) {
  570. this.l33tMaxSubstitutions = options.l33tMaxSubstitutions;
  571. }
  572. if (options.maxLength !== undefined) {
  573. this.maxLength = options.maxLength;
  574. }
  575. }
  576. setTranslations(translations) {
  577. if (this.checkCustomTranslations(translations)) {
  578. this.translations = translations;
  579. } else {
  580. throw new Error('Invalid translations object fallback to keys');
  581. }
  582. }
  583. checkCustomTranslations(translations) {
  584. let valid = true;
  585. Object.keys(translationKeys).forEach(type => {
  586. if (type in translations) {
  587. const translationType = type;
  588. Object.keys(translationKeys[translationType]).forEach(key => {
  589. if (!(key in translations[translationType])) {
  590. valid = false;
  591. }
  592. });
  593. } else {
  594. valid = false;
  595. }
  596. });
  597. return valid;
  598. }
  599. setRankedDictionaries() {
  600. const rankedDictionaries = {};
  601. const rankedDictionariesMaxWorkSize = {};
  602. Object.keys(this.dictionary).forEach(name => {
  603. rankedDictionaries[name] = this.getRankedDictionary(name);
  604. rankedDictionariesMaxWorkSize[name] = this.getRankedDictionariesMaxWordSize(name);
  605. });
  606. this.rankedDictionaries = rankedDictionaries;
  607. this.rankedDictionariesMaxWordSize = rankedDictionariesMaxWorkSize;
  608. }
  609. getRankedDictionariesMaxWordSize(name) {
  610. const data = this.dictionary[name].map(el => {
  611. if (typeof el !== 'string') {
  612. return el.toString().length;
  613. }
  614. return el.length;
  615. });
  616. // do not use Math.max(...data) because it can result in max stack size error because every entry will be used as an argument
  617. if (data.length === 0) {
  618. return 0;
  619. }
  620. return data.reduce((a, b) => Math.max(a, b), -Infinity);
  621. }
  622. getRankedDictionary(name) {
  623. const list = this.dictionary[name];
  624. if (name === 'userInputs') {
  625. const sanitizedInputs = [];
  626. list.forEach(input => {
  627. const inputType = typeof input;
  628. if (inputType === 'string' || inputType === 'number' || inputType === 'boolean') {
  629. sanitizedInputs.push(input.toString().toLowerCase());
  630. }
  631. });
  632. return buildRankedDictionary(sanitizedInputs);
  633. }
  634. return buildRankedDictionary(list);
  635. }
  636. extendUserInputsDictionary(dictionary) {
  637. if (this.dictionary.userInputs) {
  638. this.dictionary.userInputs = [...this.dictionary.userInputs, ...dictionary];
  639. } else {
  640. this.dictionary.userInputs = dictionary;
  641. }
  642. this.rankedDictionaries.userInputs = this.getRankedDictionary('userInputs');
  643. this.rankedDictionariesMaxWordSize.userInputs = this.getRankedDictionariesMaxWordSize('userInputs');
  644. }
  645. addMatcher(name, matcher) {
  646. if (this.matchers[name]) {
  647. console.info(`Matcher ${name} already exists`);
  648. } else {
  649. this.matchers[name] = matcher;
  650. }
  651. }
  652. }
  653. const zxcvbnOptions = new Options();
  654. /*
  655. * -------------------------------------------------------------------------------
  656. * Dictionary reverse matching --------------------------------------------------
  657. * -------------------------------------------------------------------------------
  658. */
  659. class MatchReverse {
  660. constructor(defaultMatch) {
  661. this.defaultMatch = defaultMatch;
  662. }
  663. match({
  664. password
  665. }) {
  666. const passwordReversed = password.split('').reverse().join('');
  667. return this.defaultMatch({
  668. password: passwordReversed
  669. }).map(match => ({
  670. ...match,
  671. token: match.token.split('').reverse().join(''),
  672. reversed: true,
  673. // map coordinates back to original string
  674. i: password.length - 1 - match.j,
  675. j: password.length - 1 - match.i
  676. }));
  677. }
  678. }
  679. /*
  680. * -------------------------------------------------------------------------------
  681. * Dictionary l33t matching -----------------------------------------------------
  682. * -------------------------------------------------------------------------------
  683. */
  684. class MatchL33t {
  685. constructor(defaultMatch) {
  686. this.defaultMatch = defaultMatch;
  687. }
  688. match({
  689. password
  690. }) {
  691. const matches = [];
  692. const enumeratedSubs = this.enumerateL33tSubs(this.relevantL33tSubtable(password, zxcvbnOptions.l33tTable));
  693. const length = Math.min(enumeratedSubs.length, zxcvbnOptions.l33tMaxSubstitutions);
  694. for (let i = 0; i < length; i += 1) {
  695. const sub = enumeratedSubs[i];
  696. // corner case: password has no relevant subs.
  697. if (empty(sub)) {
  698. break;
  699. }
  700. const subbedPassword = translate(password, sub);
  701. const matchedDictionary = this.defaultMatch({
  702. password: subbedPassword
  703. });
  704. matchedDictionary.forEach(match => {
  705. const token = password.slice(match.i, +match.j + 1 || 9e9);
  706. // only return the matches that contain an actual substitution
  707. if (token.toLowerCase() !== match.matchedWord) {
  708. // subset of mappings in sub that are in use for this match
  709. const matchSub = {};
  710. Object.keys(sub).forEach(subbedChr => {
  711. const chr = sub[subbedChr];
  712. if (token.indexOf(subbedChr) !== -1) {
  713. matchSub[subbedChr] = chr;
  714. }
  715. });
  716. const subDisplay = Object.keys(matchSub).map(k => `${k} -> ${matchSub[k]}`).join(', ');
  717. matches.push({
  718. ...match,
  719. l33t: true,
  720. token,
  721. sub: matchSub,
  722. subDisplay
  723. });
  724. }
  725. });
  726. }
  727. // filter single-character l33t matches to reduce noise.
  728. // otherwise '1' matches 'i', '4' matches 'a', both very common English words
  729. // with low dictionary rank.
  730. return matches.filter(match => match.token.length > 1);
  731. }
  732. // makes a pruned copy of l33t_table that only includes password's possible substitutions
  733. relevantL33tSubtable(password, table) {
  734. const passwordChars = {};
  735. const subTable = {};
  736. password.split('').forEach(char => {
  737. passwordChars[char] = true;
  738. });
  739. Object.keys(table).forEach(letter => {
  740. const subs = table[letter];
  741. const relevantSubs = subs.filter(sub => sub in passwordChars);
  742. if (relevantSubs.length > 0) {
  743. subTable[letter] = relevantSubs;
  744. }
  745. });
  746. return subTable;
  747. }
  748. // returns the list of possible 1337 replacement dictionaries for a given password
  749. enumerateL33tSubs(table) {
  750. const tableKeys = Object.keys(table);
  751. const subs = this.getSubs(tableKeys, [[]], table);
  752. // convert from assoc lists to dicts
  753. return subs.map(sub => {
  754. const subDict = {};
  755. sub.forEach(([l33tChr, chr]) => {
  756. subDict[l33tChr] = chr;
  757. });
  758. return subDict;
  759. });
  760. }
  761. getSubs(keys, subs, table) {
  762. if (!keys.length) {
  763. return subs;
  764. }
  765. const firstKey = keys[0];
  766. const restKeys = keys.slice(1);
  767. const nextSubs = [];
  768. table[firstKey].forEach(l33tChr => {
  769. subs.forEach(sub => {
  770. let dupL33tIndex = -1;
  771. for (let i = 0; i < sub.length; i += 1) {
  772. if (sub[i][0] === l33tChr) {
  773. dupL33tIndex = i;
  774. break;
  775. }
  776. }
  777. if (dupL33tIndex === -1) {
  778. const subExtension = sub.concat([[l33tChr, firstKey]]);
  779. nextSubs.push(subExtension);
  780. } else {
  781. const subAlternative = sub.slice(0);
  782. subAlternative.splice(dupL33tIndex, 1);
  783. subAlternative.push([l33tChr, firstKey]);
  784. nextSubs.push(sub);
  785. nextSubs.push(subAlternative);
  786. }
  787. });
  788. });
  789. const newSubs = this.dedup(nextSubs);
  790. if (restKeys.length) {
  791. return this.getSubs(restKeys, newSubs, table);
  792. }
  793. return newSubs;
  794. }
  795. dedup(subs) {
  796. const deduped = [];
  797. const members = {};
  798. subs.forEach(sub => {
  799. const assoc = sub.map((k, index) => [k, index]);
  800. assoc.sort();
  801. const label = assoc.map(([k, v]) => `${k},${v}`).join('-');
  802. if (!(label in members)) {
  803. members[label] = true;
  804. deduped.push(sub);
  805. }
  806. });
  807. return deduped;
  808. }
  809. }
  810. class MatchDictionary {
  811. constructor() {
  812. this.l33t = new MatchL33t(this.defaultMatch);
  813. this.reverse = new MatchReverse(this.defaultMatch);
  814. }
  815. match({
  816. password
  817. }) {
  818. const matches = [...this.defaultMatch({
  819. password
  820. }), ...this.reverse.match({
  821. password
  822. }), ...this.l33t.match({
  823. password
  824. })];
  825. return sorted(matches);
  826. }
  827. defaultMatch({
  828. password
  829. }) {
  830. const matches = [];
  831. const passwordLength = password.length;
  832. const passwordLower = password.toLowerCase();
  833. // eslint-disable-next-line complexity,max-statements
  834. Object.keys(zxcvbnOptions.rankedDictionaries).forEach(dictionaryName => {
  835. const rankedDict = zxcvbnOptions.rankedDictionaries[dictionaryName];
  836. const longestDictionaryWordSize = zxcvbnOptions.rankedDictionariesMaxWordSize[dictionaryName];
  837. const searchWidth = Math.min(longestDictionaryWordSize, passwordLength);
  838. for (let i = 0; i < passwordLength; i += 1) {
  839. const searchEnd = Math.min(i + searchWidth, passwordLength);
  840. for (let j = i; j < searchEnd; j += 1) {
  841. const usedPassword = passwordLower.slice(i, +j + 1 || 9e9);
  842. const isInDictionary = (usedPassword in rankedDict);
  843. let foundLevenshteinDistance = {};
  844. // only use levenshtein distance on full password to minimize the performance drop
  845. // and because otherwise there would be to many false positives
  846. const isFullPassword = i === 0 && j === passwordLength - 1;
  847. if (zxcvbnOptions.useLevenshteinDistance && isFullPassword && !isInDictionary) {
  848. foundLevenshteinDistance = findLevenshteinDistance(usedPassword, rankedDict, zxcvbnOptions.levenshteinThreshold);
  849. }
  850. const isLevenshteinMatch = Object.keys(foundLevenshteinDistance).length !== 0;
  851. if (isInDictionary || isLevenshteinMatch) {
  852. const usedRankPassword = isLevenshteinMatch ? foundLevenshteinDistance.levenshteinDistanceEntry : usedPassword;
  853. const rank = rankedDict[usedRankPassword];
  854. matches.push({
  855. pattern: 'dictionary',
  856. i,
  857. j,
  858. token: password.slice(i, +j + 1 || 9e9),
  859. matchedWord: usedPassword,
  860. rank,
  861. dictionaryName: dictionaryName,
  862. reversed: false,
  863. l33t: false,
  864. ...foundLevenshteinDistance
  865. });
  866. }
  867. }
  868. }
  869. });
  870. return matches;
  871. }
  872. }
  873. /*
  874. * -------------------------------------------------------------------------------
  875. * regex matching ---------------------------------------------------------------
  876. * -------------------------------------------------------------------------------
  877. */
  878. class MatchRegex {
  879. match({
  880. password,
  881. regexes = REGEXEN
  882. }) {
  883. const matches = [];
  884. Object.keys(regexes).forEach(name => {
  885. const regex = regexes[name];
  886. regex.lastIndex = 0; // keeps regexMatch stateless
  887. const regexMatch = regex.exec(password);
  888. if (regexMatch) {
  889. const token = regexMatch[0];
  890. matches.push({
  891. pattern: 'regex',
  892. token,
  893. i: regexMatch.index,
  894. j: regexMatch.index + regexMatch[0].length - 1,
  895. regexName: name,
  896. regexMatch
  897. });
  898. }
  899. });
  900. return sorted(matches);
  901. }
  902. }
  903. var utils = {
  904. // binomial coefficients
  905. // src: http://blog.plover.com/math/choose.html
  906. nCk(n, k) {
  907. let count = n;
  908. if (k > count) {
  909. return 0;
  910. }
  911. if (k === 0) {
  912. return 1;
  913. }
  914. let coEff = 1;
  915. for (let i = 1; i <= k; i += 1) {
  916. coEff *= count;
  917. coEff /= i;
  918. count -= 1;
  919. }
  920. return coEff;
  921. },
  922. log10(n) {
  923. return Math.log(n) / Math.log(10); // IE doesn't support Math.log10 :(
  924. },
  925. log2(n) {
  926. return Math.log(n) / Math.log(2);
  927. },
  928. factorial(num) {
  929. let rval = 1;
  930. for (let i = 2; i <= num; i += 1) rval *= i;
  931. return rval;
  932. }
  933. };
  934. var bruteforceMatcher$1 = (({
  935. token
  936. }) => {
  937. let guesses = BRUTEFORCE_CARDINALITY ** token.length;
  938. if (guesses === Number.POSITIVE_INFINITY) {
  939. guesses = Number.MAX_VALUE;
  940. }
  941. let minGuesses;
  942. // small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
  943. // submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
  944. if (token.length === 1) {
  945. minGuesses = MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1;
  946. } else {
  947. minGuesses = MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1;
  948. }
  949. return Math.max(guesses, minGuesses);
  950. });
  951. var dateMatcher$1 = (({
  952. year,
  953. separator
  954. }) => {
  955. // base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years
  956. const yearSpace = Math.max(Math.abs(year - REFERENCE_YEAR), MIN_YEAR_SPACE);
  957. let guesses = yearSpace * 365;
  958. // add factor of 4 for separator selection (one of ~4 choices)
  959. if (separator) {
  960. guesses *= 4;
  961. }
  962. return guesses;
  963. });
  964. const getVariations = cleanedWord => {
  965. const wordArray = cleanedWord.split('');
  966. const upperCaseCount = wordArray.filter(char => char.match(ONE_UPPER)).length;
  967. const lowerCaseCount = wordArray.filter(char => char.match(ONE_LOWER)).length;
  968. let variations = 0;
  969. const variationLength = Math.min(upperCaseCount, lowerCaseCount);
  970. for (let i = 1; i <= variationLength; i += 1) {
  971. variations += utils.nCk(upperCaseCount + lowerCaseCount, i);
  972. }
  973. return variations;
  974. };
  975. var uppercaseVariant = (word => {
  976. // clean words of non alpha characters to remove the reward effekt to capitalize the first letter https://github.com/dropbox/zxcvbn/issues/232
  977. const cleanedWord = word.replace(ALPHA_INVERTED, '');
  978. if (cleanedWord.match(ALL_LOWER_INVERTED) || cleanedWord.toLowerCase() === cleanedWord) {
  979. return 1;
  980. }
  981. // a capitalized word is the most common capitalization scheme,
  982. // so it only doubles the search space (uncapitalized + capitalized).
  983. // all caps and end-capitalized are common enough too, underestimate as 2x factor to be safe.
  984. const commonCases = [START_UPPER, END_UPPER, ALL_UPPER_INVERTED];
  985. const commonCasesLength = commonCases.length;
  986. for (let i = 0; i < commonCasesLength; i += 1) {
  987. const regex = commonCases[i];
  988. if (cleanedWord.match(regex)) {
  989. return 2;
  990. }
  991. }
  992. // otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters
  993. // with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD),
  994. // the number of ways to lowercase U+L letters with L lowercase letters or less.
  995. return getVariations(cleanedWord);
  996. });
  997. const getCounts = ({
  998. subs,
  999. subbed,
  1000. token
  1001. }) => {
  1002. const unsubbed = subs[subbed];
  1003. // lower-case match.token before calculating: capitalization shouldn't affect l33t calc.
  1004. const chrs = token.toLowerCase().split('');
  1005. // num of subbed chars
  1006. const subbedCount = chrs.filter(char => char === subbed).length;
  1007. // num of unsubbed chars
  1008. const unsubbedCount = chrs.filter(char => char === unsubbed).length;
  1009. return {
  1010. subbedCount,
  1011. unsubbedCount
  1012. };
  1013. };
  1014. var l33tVariant = (({
  1015. l33t,
  1016. sub,
  1017. token
  1018. }) => {
  1019. if (!l33t) {
  1020. return 1;
  1021. }
  1022. let variations = 1;
  1023. const subs = sub;
  1024. Object.keys(subs).forEach(subbed => {
  1025. const {
  1026. subbedCount,
  1027. unsubbedCount
  1028. } = getCounts({
  1029. subs,
  1030. subbed,
  1031. token
  1032. });
  1033. if (subbedCount === 0 || unsubbedCount === 0) {
  1034. // for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
  1035. // treat that as doubling the space (attacker needs to try fully subbed chars in addition to
  1036. // unsubbed.)
  1037. variations *= 2;
  1038. } else {
  1039. // this case is similar to capitalization:
  1040. // with aa44a, U = 3, S = 2, attacker needs to try unsubbed + one sub + two subs
  1041. const p = Math.min(unsubbedCount, subbedCount);
  1042. let possibilities = 0;
  1043. for (let i = 1; i <= p; i += 1) {
  1044. possibilities += utils.nCk(unsubbedCount + subbedCount, i);
  1045. }
  1046. variations *= possibilities;
  1047. }
  1048. });
  1049. return variations;
  1050. });
  1051. var dictionaryMatcher$1 = (({
  1052. rank,
  1053. reversed,
  1054. l33t,
  1055. sub,
  1056. token
  1057. }) => {
  1058. const baseGuesses = rank; // keep these as properties for display purposes
  1059. const uppercaseVariations = uppercaseVariant(token);
  1060. const l33tVariations = l33tVariant({
  1061. l33t,
  1062. sub,
  1063. token
  1064. });
  1065. const reversedVariations = reversed && 2 || 1;
  1066. const calculation = baseGuesses * uppercaseVariations * l33tVariations * reversedVariations;
  1067. return {
  1068. baseGuesses,
  1069. uppercaseVariations,
  1070. l33tVariations,
  1071. calculation
  1072. };
  1073. });
  1074. var regexMatcher$1 = (({
  1075. regexName,
  1076. regexMatch,
  1077. token
  1078. }) => {
  1079. const charClassBases = {
  1080. alphaLower: 26,
  1081. alphaUpper: 26,
  1082. alpha: 52,
  1083. alphanumeric: 62,
  1084. digits: 10,
  1085. symbols: 33
  1086. };
  1087. if (regexName in charClassBases) {
  1088. return charClassBases[regexName] ** token.length;
  1089. }
  1090. // TODO add more regex types for example special dates like 09.11
  1091. // eslint-disable-next-line default-case
  1092. switch (regexName) {
  1093. case 'recentYear':
  1094. // conservative estimate of year space: num years from REFERENCE_YEAR.
  1095. // if year is close to REFERENCE_YEAR, estimate a year space of MIN_YEAR_SPACE.
  1096. return Math.max(Math.abs(parseInt(regexMatch[0], 10) - REFERENCE_YEAR), MIN_YEAR_SPACE);
  1097. }
  1098. return 0;
  1099. });
  1100. var repeatMatcher$1 = (({
  1101. baseGuesses,
  1102. repeatCount
  1103. }) => baseGuesses * repeatCount);
  1104. var sequenceMatcher$1 = (({
  1105. token,
  1106. ascending
  1107. }) => {
  1108. const firstChr = token.charAt(0);
  1109. let baseGuesses = 0;
  1110. const startingPoints = ['a', 'A', 'z', 'Z', '0', '1', '9'];
  1111. // lower guesses for obvious starting points
  1112. if (startingPoints.includes(firstChr)) {
  1113. baseGuesses = 4;
  1114. } else if (firstChr.match(/\d/)) {
  1115. baseGuesses = 10; // digits
  1116. } else {
  1117. // could give a higher base for uppercase,
  1118. // assigning 26 to both upper and lower sequences is more conservative.
  1119. baseGuesses = 26;
  1120. }
  1121. // need to try a descending sequence in addition to every ascending sequence ->
  1122. // 2x guesses
  1123. if (!ascending) {
  1124. baseGuesses *= 2;
  1125. }
  1126. return baseGuesses * token.length;
  1127. });
  1128. const calcAverageDegree = graph => {
  1129. let average = 0;
  1130. Object.keys(graph).forEach(key => {
  1131. const neighbors = graph[key];
  1132. average += neighbors.filter(entry => !!entry).length;
  1133. });
  1134. average /= Object.entries(graph).length;
  1135. return average;
  1136. };
  1137. const estimatePossiblePatterns = ({
  1138. token,
  1139. graph,
  1140. turns
  1141. }) => {
  1142. const startingPosition = Object.keys(zxcvbnOptions.graphs[graph]).length;
  1143. const averageDegree = calcAverageDegree(zxcvbnOptions.graphs[graph]);
  1144. let guesses = 0;
  1145. const tokenLength = token.length;
  1146. // # estimate the number of possible patterns w/ tokenLength or less with turns or less.
  1147. for (let i = 2; i <= tokenLength; i += 1) {
  1148. const possibleTurns = Math.min(turns, i - 1);
  1149. for (let j = 1; j <= possibleTurns; j += 1) {
  1150. guesses += utils.nCk(i - 1, j - 1) * startingPosition * averageDegree ** j;
  1151. }
  1152. }
  1153. return guesses;
  1154. };
  1155. var spatialMatcher$1 = (({
  1156. graph,
  1157. token,
  1158. shiftedCount,
  1159. turns
  1160. }) => {
  1161. let guesses = estimatePossiblePatterns({
  1162. token,
  1163. graph,
  1164. turns
  1165. });
  1166. // add extra guesses for shifted keys. (% instead of 5, A instead of a.)
  1167. // math is similar to extra guesses of l33t substitutions in dictionary matches.
  1168. if (shiftedCount) {
  1169. const unShiftedCount = token.length - shiftedCount;
  1170. if (shiftedCount === 0 || unShiftedCount === 0) {
  1171. guesses *= 2;
  1172. } else {
  1173. let shiftedVariations = 0;
  1174. for (let i = 1; i <= Math.min(shiftedCount, unShiftedCount); i += 1) {
  1175. shiftedVariations += utils.nCk(shiftedCount + unShiftedCount, i);
  1176. }
  1177. guesses *= shiftedVariations;
  1178. }
  1179. }
  1180. return Math.round(guesses);
  1181. });
  1182. const getMinGuesses = (match, password) => {
  1183. let minGuesses = 1;
  1184. if (match.token.length < password.length) {
  1185. if (match.token.length === 1) {
  1186. minGuesses = MIN_SUBMATCH_GUESSES_SINGLE_CHAR;
  1187. } else {
  1188. minGuesses = MIN_SUBMATCH_GUESSES_MULTI_CHAR;
  1189. }
  1190. }
  1191. return minGuesses;
  1192. };
  1193. const matchers = {
  1194. bruteforce: bruteforceMatcher$1,
  1195. date: dateMatcher$1,
  1196. dictionary: dictionaryMatcher$1,
  1197. regex: regexMatcher$1,
  1198. repeat: repeatMatcher$1,
  1199. sequence: sequenceMatcher$1,
  1200. spatial: spatialMatcher$1
  1201. };
  1202. const getScoring = (name, match) => {
  1203. if (matchers[name]) {
  1204. return matchers[name](match);
  1205. }
  1206. if (zxcvbnOptions.matchers[name] && 'scoring' in zxcvbnOptions.matchers[name]) {
  1207. return zxcvbnOptions.matchers[name].scoring(match);
  1208. }
  1209. return 0;
  1210. };
  1211. // ------------------------------------------------------------------------------
  1212. // guess estimation -- one function per match pattern ---------------------------
  1213. // ------------------------------------------------------------------------------
  1214. var estimateGuesses = ((match, password) => {
  1215. const extraData = {};
  1216. // a match's guess estimate doesn't change. cache it.
  1217. if ('guesses' in match && match.guesses != null) {
  1218. return match;
  1219. }
  1220. const minGuesses = getMinGuesses(match, password);
  1221. const estimationResult = getScoring(match.pattern, match);
  1222. let guesses = 0;
  1223. if (typeof estimationResult === 'number') {
  1224. guesses = estimationResult;
  1225. } else if (match.pattern === 'dictionary') {
  1226. guesses = estimationResult.calculation;
  1227. extraData.baseGuesses = estimationResult.baseGuesses;
  1228. extraData.uppercaseVariations = estimationResult.uppercaseVariations;
  1229. extraData.l33tVariations = estimationResult.l33tVariations;
  1230. }
  1231. const matchGuesses = Math.max(guesses, minGuesses);
  1232. return {
  1233. ...match,
  1234. ...extraData,
  1235. guesses: matchGuesses,
  1236. guessesLog10: utils.log10(matchGuesses)
  1237. };
  1238. });
  1239. const scoringHelper = {
  1240. password: '',
  1241. optimal: {},
  1242. excludeAdditive: false,
  1243. fillArray(size, valueType) {
  1244. const result = [];
  1245. for (let i = 0; i < size; i += 1) {
  1246. let value = [];
  1247. if (valueType === 'object') {
  1248. value = {};
  1249. }
  1250. result.push(value);
  1251. }
  1252. return result;
  1253. },
  1254. // helper: make bruteforce match objects spanning i to j, inclusive.
  1255. makeBruteforceMatch(i, j) {
  1256. return {
  1257. pattern: 'bruteforce',
  1258. token: this.password.slice(i, +j + 1 || 9e9),
  1259. i,
  1260. j
  1261. };
  1262. },
  1263. // helper: considers whether a length-sequenceLength
  1264. // sequence ending at match m is better (fewer guesses)
  1265. // than previously encountered sequences, updating state if so.
  1266. update(match, sequenceLength) {
  1267. const k = match.j;
  1268. const estimatedMatch = estimateGuesses(match, this.password);
  1269. let pi = estimatedMatch.guesses;
  1270. if (sequenceLength > 1) {
  1271. // we're considering a length-sequenceLength sequence ending with match m:
  1272. // obtain the product term in the minimization function by multiplying m's guesses
  1273. // by the product of the length-(sequenceLength-1)
  1274. // sequence ending just before m, at m.i - 1.
  1275. pi *= this.optimal.pi[estimatedMatch.i - 1][sequenceLength - 1];
  1276. }
  1277. // calculate the minimization func
  1278. let g = utils.factorial(sequenceLength) * pi;
  1279. if (!this.excludeAdditive) {
  1280. g += MIN_GUESSES_BEFORE_GROWING_SEQUENCE ** (sequenceLength - 1);
  1281. }
  1282. // update state if new best.
  1283. // first see if any competing sequences covering this prefix,
  1284. // with sequenceLength or fewer matches,
  1285. // fare better than this sequence. if so, skip it and return.
  1286. let shouldSkip = false;
  1287. Object.keys(this.optimal.g[k]).forEach(competingPatternLength => {
  1288. const competingMetricMatch = this.optimal.g[k][competingPatternLength];
  1289. if (parseInt(competingPatternLength, 10) <= sequenceLength) {
  1290. if (competingMetricMatch <= g) {
  1291. shouldSkip = true;
  1292. }
  1293. }
  1294. });
  1295. if (!shouldSkip) {
  1296. // this sequence might be part of the final optimal sequence.
  1297. this.optimal.g[k][sequenceLength] = g;
  1298. this.optimal.m[k][sequenceLength] = estimatedMatch;
  1299. this.optimal.pi[k][sequenceLength] = pi;
  1300. }
  1301. },
  1302. // helper: evaluate bruteforce matches ending at passwordCharIndex.
  1303. bruteforceUpdate(passwordCharIndex) {
  1304. // see if a single bruteforce match spanning the passwordCharIndex-prefix is optimal.
  1305. let match = this.makeBruteforceMatch(0, passwordCharIndex);
  1306. this.update(match, 1);
  1307. for (let i = 1; i <= passwordCharIndex; i += 1) {
  1308. // generate passwordCharIndex bruteforce matches, spanning from (i=1, j=passwordCharIndex) up to (i=passwordCharIndex, j=passwordCharIndex).
  1309. // see if adding these new matches to any of the sequences in optimal[i-1]
  1310. // leads to new bests.
  1311. match = this.makeBruteforceMatch(i, passwordCharIndex);
  1312. const tmp = this.optimal.m[i - 1];
  1313. // eslint-disable-next-line no-loop-func
  1314. Object.keys(tmp).forEach(sequenceLength => {
  1315. const lastMatch = tmp[sequenceLength];
  1316. // corner: an optimal sequence will never have two adjacent bruteforce matches.
  1317. // it is strictly better to have a single bruteforce match spanning the same region:
  1318. // same contribution to the guess product with a lower length.
  1319. // --> safe to skip those cases.
  1320. if (lastMatch.pattern !== 'bruteforce') {
  1321. // try adding m to this length-sequenceLength sequence.
  1322. this.update(match, parseInt(sequenceLength, 10) + 1);
  1323. }
  1324. });
  1325. }
  1326. },
  1327. // helper: step backwards through optimal.m starting at the end,
  1328. // constructing the final optimal match sequence.
  1329. unwind(passwordLength) {
  1330. const optimalMatchSequence = [];
  1331. let k = passwordLength - 1;
  1332. // find the final best sequence length and score
  1333. let sequenceLength = 0;
  1334. // eslint-disable-next-line no-loss-of-precision
  1335. let g = 2e308;
  1336. const temp = this.optimal.g[k];
  1337. // safety check for empty passwords
  1338. if (temp) {
  1339. Object.keys(temp).forEach(candidateSequenceLength => {
  1340. const candidateMetricMatch = temp[candidateSequenceLength];
  1341. if (candidateMetricMatch < g) {
  1342. sequenceLength = parseInt(candidateSequenceLength, 10);
  1343. g = candidateMetricMatch;
  1344. }
  1345. });
  1346. }
  1347. while (k >= 0) {
  1348. const match = this.optimal.m[k][sequenceLength];
  1349. optimalMatchSequence.unshift(match);
  1350. k = match.i - 1;
  1351. sequenceLength -= 1;
  1352. }
  1353. return optimalMatchSequence;
  1354. }
  1355. };
  1356. var scoring = {
  1357. // ------------------------------------------------------------------------------
  1358. // search --- most guessable match sequence -------------------------------------
  1359. // ------------------------------------------------------------------------------
  1360. //
  1361. // takes a sequence of overlapping matches, returns the non-overlapping sequence with
  1362. // minimum guesses. the following is a O(l_max * (n + m)) dynamic programming algorithm
  1363. // for a length-n password with m candidate matches. l_max is the maximum optimal
  1364. // sequence length spanning each prefix of the password. In practice it rarely exceeds 5 and the
  1365. // search terminates rapidly.
  1366. //
  1367. // the optimal "minimum guesses" sequence is here defined to be the sequence that
  1368. // minimizes the following function:
  1369. //
  1370. // g = sequenceLength! * Product(m.guesses for m in sequence) + D^(sequenceLength - 1)
  1371. //
  1372. // where sequenceLength is the length of the sequence.
  1373. //
  1374. // the factorial term is the number of ways to order sequenceLength patterns.
  1375. //
  1376. // the D^(sequenceLength-1) term is another length penalty, roughly capturing the idea that an
  1377. // attacker will try lower-length sequences first before trying length-sequenceLength sequences.
  1378. //
  1379. // for example, consider a sequence that is date-repeat-dictionary.
  1380. // - an attacker would need to try other date-repeat-dictionary combinations,
  1381. // hence the product term.
  1382. // - an attacker would need to try repeat-date-dictionary, dictionary-repeat-date,
  1383. // ..., hence the factorial term.
  1384. // - an attacker would also likely try length-1 (dictionary) and length-2 (dictionary-date)
  1385. // sequences before length-3. assuming at minimum D guesses per pattern type,
  1386. // D^(sequenceLength-1) approximates Sum(D^i for i in [1..sequenceLength-1]
  1387. //
  1388. // ------------------------------------------------------------------------------
  1389. mostGuessableMatchSequence(password, matches, excludeAdditive = false) {
  1390. scoringHelper.password = password;
  1391. scoringHelper.excludeAdditive = excludeAdditive;
  1392. const passwordLength = password.length;
  1393. // partition matches into sublists according to ending index j
  1394. let matchesByCoordinateJ = scoringHelper.fillArray(passwordLength, 'array');
  1395. matches.forEach(match => {
  1396. matchesByCoordinateJ[match.j].push(match);
  1397. });
  1398. // small detail: for deterministic output, sort each sublist by i.
  1399. matchesByCoordinateJ = matchesByCoordinateJ.map(match => match.sort((m1, m2) => m1.i - m2.i));
  1400. scoringHelper.optimal = {
  1401. // optimal.m[k][sequenceLength] holds final match in the best length-sequenceLength
  1402. // match sequence covering the
  1403. // password prefix up to k, inclusive.
  1404. // if there is no length-sequenceLength sequence that scores better (fewer guesses) than
  1405. // a shorter match sequence spanning the same prefix,
  1406. // optimal.m[k][sequenceLength] is undefined.
  1407. m: scoringHelper.fillArray(passwordLength, 'object'),
  1408. // same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence).
  1409. // optimal.pi allows for fast (non-looping) updates to the minimization function.
  1410. pi: scoringHelper.fillArray(passwordLength, 'object'),
  1411. // same structure as optimal.m -- holds the overall metric.
  1412. g: scoringHelper.fillArray(passwordLength, 'object')
  1413. };
  1414. for (let k = 0; k < passwordLength; k += 1) {
  1415. matchesByCoordinateJ[k].forEach(match => {
  1416. if (match.i > 0) {
  1417. Object.keys(scoringHelper.optimal.m[match.i - 1]).forEach(sequenceLength => {
  1418. scoringHelper.update(match, parseInt(sequenceLength, 10) + 1);
  1419. });
  1420. } else {
  1421. scoringHelper.update(match, 1);
  1422. }
  1423. });
  1424. scoringHelper.bruteforceUpdate(k);
  1425. }
  1426. const optimalMatchSequence = scoringHelper.unwind(passwordLength);
  1427. const optimalSequenceLength = optimalMatchSequence.length;
  1428. const guesses = this.getGuesses(password, optimalSequenceLength);
  1429. return {
  1430. password,
  1431. guesses,
  1432. guessesLog10: utils.log10(guesses),
  1433. sequence: optimalMatchSequence
  1434. };
  1435. },
  1436. getGuesses(password, optimalSequenceLength) {
  1437. const passwordLength = password.length;
  1438. let guesses = 0;
  1439. if (password.length === 0) {
  1440. guesses = 1;
  1441. } else {
  1442. guesses = scoringHelper.optimal.g[passwordLength - 1][optimalSequenceLength];
  1443. }
  1444. return guesses;
  1445. }
  1446. };
  1447. /*
  1448. *-------------------------------------------------------------------------------
  1449. * repeats (aaa, abcabcabc) ------------------------------
  1450. *-------------------------------------------------------------------------------
  1451. */
  1452. class MatchRepeat {
  1453. // eslint-disable-next-line max-statements
  1454. match({
  1455. password,
  1456. omniMatch
  1457. }) {
  1458. const matches = [];
  1459. let lastIndex = 0;
  1460. while (lastIndex < password.length) {
  1461. const greedyMatch = this.getGreedyMatch(password, lastIndex);
  1462. const lazyMatch = this.getLazyMatch(password, lastIndex);
  1463. if (greedyMatch == null) {
  1464. break;
  1465. }
  1466. const {
  1467. match,
  1468. baseToken
  1469. } = this.setMatchToken(greedyMatch, lazyMatch);
  1470. if (match) {
  1471. const j = match.index + match[0].length - 1;
  1472. const baseGuesses = this.getBaseGuesses(baseToken, omniMatch);
  1473. matches.push(this.normalizeMatch(baseToken, j, match, baseGuesses));
  1474. lastIndex = j + 1;
  1475. }
  1476. }
  1477. const hasPromises = matches.some(match => {
  1478. return match instanceof Promise;
  1479. });
  1480. if (hasPromises) {
  1481. return Promise.all(matches);
  1482. }
  1483. return matches;
  1484. }
  1485. // eslint-disable-next-line max-params
  1486. normalizeMatch(baseToken, j, match, baseGuesses) {
  1487. const baseMatch = {
  1488. pattern: 'repeat',
  1489. i: match.index,
  1490. j,
  1491. token: match[0],
  1492. baseToken,
  1493. baseGuesses: 0,
  1494. repeatCount: match[0].length / baseToken.length
  1495. };
  1496. if (baseGuesses instanceof Promise) {
  1497. return baseGuesses.then(resolvedBaseGuesses => {
  1498. return {
  1499. ...baseMatch,
  1500. baseGuesses: resolvedBaseGuesses
  1501. };
  1502. });
  1503. }
  1504. return {
  1505. ...baseMatch,
  1506. baseGuesses
  1507. };
  1508. }
  1509. getGreedyMatch(password, lastIndex) {
  1510. const greedy = /(.+)\1+/g;
  1511. greedy.lastIndex = lastIndex;
  1512. return greedy.exec(password);
  1513. }
  1514. getLazyMatch(password, lastIndex) {
  1515. const lazy = /(.+?)\1+/g;
  1516. lazy.lastIndex = lastIndex;
  1517. return lazy.exec(password);
  1518. }
  1519. setMatchToken(greedyMatch, lazyMatch) {
  1520. const lazyAnchored = /^(.+?)\1+$/;
  1521. let match;
  1522. let baseToken = '';
  1523. if (lazyMatch && greedyMatch[0].length > lazyMatch[0].length) {
  1524. // greedy beats lazy for 'aabaab'
  1525. // greedy: [aabaab, aab]
  1526. // lazy: [aa, a]
  1527. match = greedyMatch;
  1528. // greedy's repeated string might itself be repeated, eg.
  1529. // aabaab in aabaabaabaab.
  1530. // run an anchored lazy match on greedy's repeated string
  1531. // to find the shortest repeated string
  1532. const temp = lazyAnchored.exec(match[0]);
  1533. if (temp) {
  1534. baseToken = temp[1];
  1535. }
  1536. } else {
  1537. // lazy beats greedy for 'aaaaa'
  1538. // greedy: [aaaa, aa]
  1539. // lazy: [aaaaa, a]
  1540. match = lazyMatch;
  1541. if (match) {
  1542. baseToken = match[1];
  1543. }
  1544. }
  1545. return {
  1546. match,
  1547. baseToken
  1548. };
  1549. }
  1550. getBaseGuesses(baseToken, omniMatch) {
  1551. const matches = omniMatch.match(baseToken);
  1552. if (matches instanceof Promise) {
  1553. return matches.then(resolvedMatches => {
  1554. const baseAnalysis = scoring.mostGuessableMatchSequence(baseToken, resolvedMatches);
  1555. return baseAnalysis.guesses;
  1556. });
  1557. }
  1558. const baseAnalysis = scoring.mostGuessableMatchSequence(baseToken, matches);
  1559. return baseAnalysis.guesses;
  1560. }
  1561. }
  1562. /*
  1563. *-------------------------------------------------------------------------------
  1564. * sequences (abcdef) ------------------------------
  1565. *-------------------------------------------------------------------------------
  1566. */
  1567. class MatchSequence {
  1568. constructor() {
  1569. this.MAX_DELTA = 5;
  1570. }
  1571. // eslint-disable-next-line max-statements
  1572. match({
  1573. password
  1574. }) {
  1575. /*
  1576. * Identifies sequences by looking for repeated differences in unicode codepoint.
  1577. * this allows skipping, such as 9753, and also matches some extended unicode sequences
  1578. * such as Greek and Cyrillic alphabets.
  1579. *
  1580. * for example, consider the input 'abcdb975zy'
  1581. *
  1582. * password: a b c d b 9 7 5 z y
  1583. * index: 0 1 2 3 4 5 6 7 8 9
  1584. * delta: 1 1 1 -2 -41 -2 -2 69 1
  1585. *
  1586. * expected result:
  1587. * [(i, j, delta), ...] = [(0, 3, 1), (5, 7, -2), (8, 9, 1)]
  1588. */
  1589. const result = [];
  1590. if (password.length === 1) {
  1591. return [];
  1592. }
  1593. let i = 0;
  1594. let lastDelta = null;
  1595. const passwordLength = password.length;
  1596. for (let k = 1; k < passwordLength; k += 1) {
  1597. const delta = password.charCodeAt(k) - password.charCodeAt(k - 1);
  1598. if (lastDelta == null) {
  1599. lastDelta = delta;
  1600. }
  1601. if (delta !== lastDelta) {
  1602. const j = k - 1;
  1603. this.update({
  1604. i,
  1605. j,
  1606. delta: lastDelta,
  1607. password,
  1608. result
  1609. });
  1610. i = j;
  1611. lastDelta = delta;
  1612. }
  1613. }
  1614. this.update({
  1615. i,
  1616. j: passwordLength - 1,
  1617. delta: lastDelta,
  1618. password,
  1619. result
  1620. });
  1621. return result;
  1622. }
  1623. update({
  1624. i,
  1625. j,
  1626. delta,
  1627. password,
  1628. result
  1629. }) {
  1630. if (j - i > 1 || Math.abs(delta) === 1) {
  1631. const absoluteDelta = Math.abs(delta);
  1632. if (absoluteDelta > 0 && absoluteDelta <= this.MAX_DELTA) {
  1633. const token = password.slice(i, +j + 1 || 9e9);
  1634. const {
  1635. sequenceName,
  1636. sequenceSpace
  1637. } = this.getSequence(token);
  1638. return result.push({
  1639. pattern: 'sequence',
  1640. i,
  1641. j,
  1642. token: password.slice(i, +j + 1 || 9e9),
  1643. sequenceName,
  1644. sequenceSpace,
  1645. ascending: delta > 0
  1646. });
  1647. }
  1648. }
  1649. return null;
  1650. }
  1651. getSequence(token) {
  1652. // TODO conservatively stick with roman alphabet size.
  1653. // (this could be improved)
  1654. let sequenceName = 'unicode';
  1655. let sequenceSpace = 26;
  1656. if (ALL_LOWER.test(token)) {
  1657. sequenceName = 'lower';
  1658. sequenceSpace = 26;
  1659. } else if (ALL_UPPER.test(token)) {
  1660. sequenceName = 'upper';
  1661. sequenceSpace = 26;
  1662. } else if (ALL_DIGIT.test(token)) {
  1663. sequenceName = 'digits';
  1664. sequenceSpace = 10;
  1665. }
  1666. return {
  1667. sequenceName,
  1668. sequenceSpace
  1669. };
  1670. }
  1671. }
  1672. /*
  1673. * ------------------------------------------------------------------------------
  1674. * spatial match (qwerty/dvorak/keypad and so on) -----------------------------------------
  1675. * ------------------------------------------------------------------------------
  1676. */
  1677. class MatchSpatial {
  1678. constructor() {
  1679. this.SHIFTED_RX = /[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]/;
  1680. }
  1681. match({
  1682. password
  1683. }) {
  1684. const matches = [];
  1685. Object.keys(zxcvbnOptions.graphs).forEach(graphName => {
  1686. const graph = zxcvbnOptions.graphs[graphName];
  1687. extend(matches, this.helper(password, graph, graphName));
  1688. });
  1689. return sorted(matches);
  1690. }
  1691. checkIfShifted(graphName, password, index) {
  1692. if (!graphName.includes('keypad') &&
  1693. // initial character is shifted
  1694. this.SHIFTED_RX.test(password.charAt(index))) {
  1695. return 1;
  1696. }
  1697. return 0;
  1698. }
  1699. // eslint-disable-next-line complexity, max-statements
  1700. helper(password, graph, graphName) {
  1701. let shiftedCount;
  1702. const matches = [];
  1703. let i = 0;
  1704. const passwordLength = password.length;
  1705. while (i < passwordLength - 1) {
  1706. let j = i + 1;
  1707. let lastDirection = 0;
  1708. let turns = 0;
  1709. shiftedCount = this.checkIfShifted(graphName, password, i);
  1710. // eslint-disable-next-line no-constant-condition
  1711. while (true) {
  1712. const prevChar = password.charAt(j - 1);
  1713. const adjacents = graph[prevChar] || [];
  1714. let found = false;
  1715. let foundDirection = -1;
  1716. let curDirection = -1;
  1717. // consider growing pattern by one character if j hasn't gone over the edge.
  1718. if (j < passwordLength) {
  1719. const curChar = password.charAt(j);
  1720. const adjacentsLength = adjacents.length;
  1721. for (let k = 0; k < adjacentsLength; k += 1) {
  1722. const adjacent = adjacents[k];
  1723. curDirection += 1;
  1724. // eslint-disable-next-line max-depth
  1725. if (adjacent) {
  1726. const adjacentIndex = adjacent.indexOf(curChar);
  1727. // eslint-disable-next-line max-depth
  1728. if (adjacentIndex !== -1) {
  1729. found = true;
  1730. foundDirection = curDirection;
  1731. // eslint-disable-next-line max-depth
  1732. if (adjacentIndex === 1) {
  1733. // # index 1 in the adjacency means the key is shifted,
  1734. // # 0 means unshifted: A vs a, % vs 5, etc.
  1735. // # for example, 'q' is adjacent to the entry '2@'.
  1736. // # @ is shifted w/ index 1, 2 is unshifted.
  1737. shiftedCount += 1;
  1738. }
  1739. // eslint-disable-next-line max-depth
  1740. if (lastDirection !== foundDirection) {
  1741. // # adding a turn is correct even in the initial
  1742. // case when last_direction is null:
  1743. // # every spatial pattern starts with a turn.
  1744. turns += 1;
  1745. lastDirection = foundDirection;
  1746. }
  1747. break;
  1748. }
  1749. }
  1750. }
  1751. }
  1752. // if the current pattern continued, extend j and try to grow again
  1753. if (found) {
  1754. j += 1;
  1755. // otherwise push the pattern discovered so far, if any...
  1756. } else {
  1757. // don't consider length 1 or 2 chains.
  1758. if (j - i > 2) {
  1759. matches.push({
  1760. pattern: 'spatial',
  1761. i,
  1762. j: j - 1,
  1763. token: password.slice(i, j),
  1764. graph: graphName,
  1765. turns,
  1766. shiftedCount
  1767. });
  1768. }
  1769. // ...and then start a new search for the rest of the password.
  1770. i = j;
  1771. break;
  1772. }
  1773. }
  1774. }
  1775. return matches;
  1776. }
  1777. }
  1778. class Matching {
  1779. constructor() {
  1780. this.matchers = {
  1781. date: MatchDate,
  1782. dictionary: MatchDictionary,
  1783. regex: MatchRegex,
  1784. // @ts-ignore => TODO resolve this type issue. This is because it is possible to be async
  1785. repeat: MatchRepeat,
  1786. sequence: MatchSequence,
  1787. spatial: MatchSpatial
  1788. };
  1789. }
  1790. match(password) {
  1791. const matches = [];
  1792. const promises = [];
  1793. const matchers = [...Object.keys(this.matchers), ...Object.keys(zxcvbnOptions.matchers)];
  1794. matchers.forEach(key => {
  1795. if (!this.matchers[key] && !zxcvbnOptions.matchers[key]) {
  1796. return;
  1797. }
  1798. const Matcher = this.matchers[key] ? this.matchers[key] : zxcvbnOptions.matchers[key].Matching;
  1799. const usedMatcher = new Matcher();
  1800. const result = usedMatcher.match({
  1801. password,
  1802. omniMatch: this
  1803. });
  1804. if (result instanceof Promise) {
  1805. result.then(response => {
  1806. extend(matches, response);
  1807. });
  1808. promises.push(result);
  1809. } else {
  1810. extend(matches, result);
  1811. }
  1812. });
  1813. if (promises.length > 0) {
  1814. return new Promise(resolve => {
  1815. Promise.all(promises).then(() => {
  1816. resolve(sorted(matches));
  1817. });
  1818. });
  1819. }
  1820. return sorted(matches);
  1821. }
  1822. }
  1823. const SECOND = 1;
  1824. const MINUTE = SECOND * 60;
  1825. const HOUR = MINUTE * 60;
  1826. const DAY = HOUR * 24;
  1827. const MONTH = DAY * 31;
  1828. const YEAR = MONTH * 12;
  1829. const CENTURY = YEAR * 100;
  1830. const times = {
  1831. second: SECOND,
  1832. minute: MINUTE,
  1833. hour: HOUR,
  1834. day: DAY,
  1835. month: MONTH,
  1836. year: YEAR,
  1837. century: CENTURY
  1838. };
  1839. /*
  1840. * -------------------------------------------------------------------------------
  1841. * Estimates time for an attacker ---------------------------------------------------------------
  1842. * -------------------------------------------------------------------------------
  1843. */
  1844. class TimeEstimates {
  1845. translate(displayStr, value) {
  1846. let key = displayStr;
  1847. if (value !== undefined && value !== 1) {
  1848. key += 's';
  1849. }
  1850. const {
  1851. timeEstimation
  1852. } = zxcvbnOptions.translations;
  1853. return timeEstimation[key].replace('{base}', `${value}`);
  1854. }
  1855. estimateAttackTimes(guesses) {
  1856. const crackTimesSeconds = {
  1857. onlineThrottling100PerHour: guesses / (100 / 3600),
  1858. onlineNoThrottling10PerSecond: guesses / 10,
  1859. offlineSlowHashing1e4PerSecond: guesses / 1e4,
  1860. offlineFastHashing1e10PerSecond: guesses / 1e10
  1861. };
  1862. const crackTimesDisplay = {
  1863. onlineThrottling100PerHour: '',
  1864. onlineNoThrottling10PerSecond: '',
  1865. offlineSlowHashing1e4PerSecond: '',
  1866. offlineFastHashing1e10PerSecond: ''
  1867. };
  1868. Object.keys(crackTimesSeconds).forEach(scenario => {
  1869. const seconds = crackTimesSeconds[scenario];
  1870. crackTimesDisplay[scenario] = this.displayTime(seconds);
  1871. });
  1872. return {
  1873. crackTimesSeconds,
  1874. crackTimesDisplay,
  1875. score: this.guessesToScore(guesses)
  1876. };
  1877. }
  1878. guessesToScore(guesses) {
  1879. const DELTA = 5;
  1880. if (guesses < 1e3 + DELTA) {
  1881. // risky password: "too guessable"
  1882. return 0;
  1883. }
  1884. if (guesses < 1e6 + DELTA) {
  1885. // modest protection from throttled online attacks: "very guessable"
  1886. return 1;
  1887. }
  1888. if (guesses < 1e8 + DELTA) {
  1889. // modest protection from unthrottled online attacks: "somewhat guessable"
  1890. return 2;
  1891. }
  1892. if (guesses < 1e10 + DELTA) {
  1893. // modest protection from offline attacks: "safely unguessable"
  1894. // assuming a salted, slow hash function like bcrypt, scrypt, PBKDF2, argon, etc
  1895. return 3;
  1896. }
  1897. // strong protection from offline attacks under same scenario: "very unguessable"
  1898. return 4;
  1899. }
  1900. displayTime(seconds) {
  1901. let displayStr = 'centuries';
  1902. let base;
  1903. const timeKeys = Object.keys(times);
  1904. const foundIndex = timeKeys.findIndex(time => seconds < times[time]);
  1905. if (foundIndex > -1) {
  1906. displayStr = timeKeys[foundIndex - 1];
  1907. if (foundIndex !== 0) {
  1908. base = Math.round(seconds / times[displayStr]);
  1909. } else {
  1910. displayStr = 'ltSecond';
  1911. }
  1912. }
  1913. return this.translate(displayStr, base);
  1914. }
  1915. }
  1916. var bruteforceMatcher = (() => {
  1917. return null;
  1918. });
  1919. var dateMatcher = (() => {
  1920. return {
  1921. warning: zxcvbnOptions.translations.warnings.dates,
  1922. suggestions: [zxcvbnOptions.translations.suggestions.dates]
  1923. };
  1924. });
  1925. const getDictionaryWarningPassword = (match, isSoleMatch) => {
  1926. let warning = '';
  1927. if (isSoleMatch && !match.l33t && !match.reversed) {
  1928. if (match.rank <= 10) {
  1929. warning = zxcvbnOptions.translations.warnings.topTen;
  1930. } else if (match.rank <= 100) {
  1931. warning = zxcvbnOptions.translations.warnings.topHundred;
  1932. } else {
  1933. warning = zxcvbnOptions.translations.warnings.common;
  1934. }
  1935. } else if (match.guessesLog10 <= 4) {
  1936. warning = zxcvbnOptions.translations.warnings.similarToCommon;
  1937. }
  1938. return warning;
  1939. };
  1940. const getDictionaryWarningWikipedia = (match, isSoleMatch) => {
  1941. let warning = '';
  1942. if (isSoleMatch) {
  1943. warning = zxcvbnOptions.translations.warnings.wordByItself;
  1944. }
  1945. return warning;
  1946. };
  1947. const getDictionaryWarningNames = (match, isSoleMatch) => {
  1948. if (isSoleMatch) {
  1949. return zxcvbnOptions.translations.warnings.namesByThemselves;
  1950. }
  1951. return zxcvbnOptions.translations.warnings.commonNames;
  1952. };
  1953. const getDictionaryWarning = (match, isSoleMatch) => {
  1954. let warning = '';
  1955. const dictName = match.dictionaryName;
  1956. const isAName = dictName === 'lastnames' || dictName.toLowerCase().includes('firstnames');
  1957. if (dictName === 'passwords') {
  1958. warning = getDictionaryWarningPassword(match, isSoleMatch);
  1959. } else if (dictName.includes('wikipedia')) {
  1960. warning = getDictionaryWarningWikipedia(match, isSoleMatch);
  1961. } else if (isAName) {
  1962. warning = getDictionaryWarningNames(match, isSoleMatch);
  1963. } else if (dictName === 'userInputs') {
  1964. warning = zxcvbnOptions.translations.warnings.userInputs;
  1965. }
  1966. return warning;
  1967. };
  1968. var dictionaryMatcher = ((match, isSoleMatch) => {
  1969. const warning = getDictionaryWarning(match, isSoleMatch);
  1970. const suggestions = [];
  1971. const word = match.token;
  1972. if (word.match(START_UPPER)) {
  1973. suggestions.push(zxcvbnOptions.translations.suggestions.capitalization);
  1974. } else if (word.match(ALL_UPPER_INVERTED) && word.toLowerCase() !== word) {
  1975. suggestions.push(zxcvbnOptions.translations.suggestions.allUppercase);
  1976. }
  1977. if (match.reversed && match.token.length >= 4) {
  1978. suggestions.push(zxcvbnOptions.translations.suggestions.reverseWords);
  1979. }
  1980. if (match.l33t) {
  1981. suggestions.push(zxcvbnOptions.translations.suggestions.l33t);
  1982. }
  1983. return {
  1984. warning,
  1985. suggestions
  1986. };
  1987. });
  1988. var regexMatcher = (match => {
  1989. if (match.regexName === 'recentYear') {
  1990. return {
  1991. warning: zxcvbnOptions.translations.warnings.recentYears,
  1992. suggestions: [zxcvbnOptions.translations.suggestions.recentYears, zxcvbnOptions.translations.suggestions.associatedYears]
  1993. };
  1994. }
  1995. return {
  1996. warning: '',
  1997. suggestions: []
  1998. };
  1999. });
  2000. var repeatMatcher = (match => {
  2001. let warning = zxcvbnOptions.translations.warnings.extendedRepeat;
  2002. if (match.baseToken.length === 1) {
  2003. warning = zxcvbnOptions.translations.warnings.simpleRepeat;
  2004. }
  2005. return {
  2006. warning,
  2007. suggestions: [zxcvbnOptions.translations.suggestions.repeated]
  2008. };
  2009. });
  2010. var sequenceMatcher = (() => {
  2011. return {
  2012. warning: zxcvbnOptions.translations.warnings.sequences,
  2013. suggestions: [zxcvbnOptions.translations.suggestions.sequences]
  2014. };
  2015. });
  2016. var spatialMatcher = (match => {
  2017. let warning = zxcvbnOptions.translations.warnings.keyPattern;
  2018. if (match.turns === 1) {
  2019. warning = zxcvbnOptions.translations.warnings.straightRow;
  2020. }
  2021. return {
  2022. warning,
  2023. suggestions: [zxcvbnOptions.translations.suggestions.longerKeyboardPattern]
  2024. };
  2025. });
  2026. const defaultFeedback = {
  2027. warning: '',
  2028. suggestions: []
  2029. };
  2030. /*
  2031. * -------------------------------------------------------------------------------
  2032. * Generate feedback ---------------------------------------------------------------
  2033. * -------------------------------------------------------------------------------
  2034. */
  2035. class Feedback {
  2036. constructor() {
  2037. this.matchers = {
  2038. bruteforce: bruteforceMatcher,
  2039. date: dateMatcher,
  2040. dictionary: dictionaryMatcher,
  2041. regex: regexMatcher,
  2042. repeat: repeatMatcher,
  2043. sequence: sequenceMatcher,
  2044. spatial: spatialMatcher
  2045. };
  2046. this.defaultFeedback = {
  2047. warning: '',
  2048. suggestions: []
  2049. };
  2050. this.setDefaultSuggestions();
  2051. }
  2052. setDefaultSuggestions() {
  2053. this.defaultFeedback.suggestions.push(zxcvbnOptions.translations.suggestions.useWords, zxcvbnOptions.translations.suggestions.noNeed);
  2054. }
  2055. getFeedback(score, sequence) {
  2056. if (sequence.length === 0) {
  2057. return this.defaultFeedback;
  2058. }
  2059. if (score > 2) {
  2060. return defaultFeedback;
  2061. }
  2062. const extraFeedback = zxcvbnOptions.translations.suggestions.anotherWord;
  2063. const longestMatch = this.getLongestMatch(sequence);
  2064. let feedback = this.getMatchFeedback(longestMatch, sequence.length === 1);
  2065. if (feedback !== null && feedback !== undefined) {
  2066. feedback.suggestions.unshift(extraFeedback);
  2067. if (feedback.warning == null) {
  2068. feedback.warning = '';
  2069. }
  2070. } else {
  2071. feedback = {
  2072. warning: '',
  2073. suggestions: [extraFeedback]
  2074. };
  2075. }
  2076. return feedback;
  2077. }
  2078. getLongestMatch(sequence) {
  2079. let longestMatch = sequence[0];
  2080. const slicedSequence = sequence.slice(1);
  2081. slicedSequence.forEach(match => {
  2082. if (match.token.length > longestMatch.token.length) {
  2083. longestMatch = match;
  2084. }
  2085. });
  2086. return longestMatch;
  2087. }
  2088. getMatchFeedback(match, isSoleMatch) {
  2089. if (this.matchers[match.pattern]) {
  2090. return this.matchers[match.pattern](match, isSoleMatch);
  2091. }
  2092. if (zxcvbnOptions.matchers[match.pattern] && 'feedback' in zxcvbnOptions.matchers[match.pattern]) {
  2093. return zxcvbnOptions.matchers[match.pattern].feedback(match, isSoleMatch);
  2094. }
  2095. return defaultFeedback;
  2096. }
  2097. }
  2098. /**
  2099. * @link https://davidwalsh.name/javascript-debounce-function
  2100. * @param func needs to implement a function which is debounced
  2101. * @param wait how long do you want to wait till the previous declared function is executed
  2102. * @param isImmediate defines if you want to execute the function on the first execution or the last execution inside the time window. `true` for first and `false` for last.
  2103. */
  2104. var debounce = ((func, wait, isImmediate) => {
  2105. let timeout;
  2106. return function debounce(...args) {
  2107. const context = this;
  2108. const later = () => {
  2109. timeout = undefined;
  2110. if (!isImmediate) {
  2111. func.apply(context, args);
  2112. }
  2113. };
  2114. const shouldCallNow = isImmediate && !timeout;
  2115. if (timeout !== undefined) {
  2116. clearTimeout(timeout);
  2117. }
  2118. timeout = setTimeout(later, wait);
  2119. if (shouldCallNow) {
  2120. return func.apply(context, args);
  2121. }
  2122. return undefined;
  2123. };
  2124. });
  2125. const time = () => new Date().getTime();
  2126. const createReturnValue = (resolvedMatches, password, start) => {
  2127. const feedback = new Feedback();
  2128. const timeEstimates = new TimeEstimates();
  2129. const matchSequence = scoring.mostGuessableMatchSequence(password, resolvedMatches);
  2130. const calcTime = time() - start;
  2131. const attackTimes = timeEstimates.estimateAttackTimes(matchSequence.guesses);
  2132. return {
  2133. calcTime,
  2134. ...matchSequence,
  2135. ...attackTimes,
  2136. feedback: feedback.getFeedback(attackTimes.score, matchSequence.sequence)
  2137. };
  2138. };
  2139. const main = (password, userInputs) => {
  2140. if (userInputs) {
  2141. zxcvbnOptions.extendUserInputsDictionary(userInputs);
  2142. }
  2143. const matching = new Matching();
  2144. return matching.match(password);
  2145. };
  2146. const zxcvbn = (password, userInputs) => {
  2147. const start = time();
  2148. const matches = main(password, userInputs);
  2149. if (matches instanceof Promise) {
  2150. throw new Error('You are using a Promised matcher, please use `zxcvbnAsync` for it.');
  2151. }
  2152. return createReturnValue(matches, password, start);
  2153. };
  2154. const zxcvbnAsync = async (password, userInputs) => {
  2155. const usedPassword = password.substring(0, zxcvbnOptions.maxLength);
  2156. const start = time();
  2157. const matches = await main(usedPassword, userInputs);
  2158. return createReturnValue(matches, usedPassword, start);
  2159. };
  2160. exports.Options = Options;
  2161. exports.debounce = debounce;
  2162. exports.zxcvbn = zxcvbn;
  2163. exports.zxcvbnAsync = zxcvbnAsync;
  2164. exports.zxcvbnOptions = zxcvbnOptions;
  2165. return exports;
  2166. })({});
  2167. //# sourceMappingURL=zxcvbn-ts.js.map