diff --git a/data-scripts/_generators/PasswordGenerator.ts b/data-scripts/_generators/PasswordGenerator.ts index 5e2d40df..b4d2f52d 100644 --- a/data-scripts/_generators/PasswordGenerator.ts +++ b/data-scripts/_generators/PasswordGenerator.ts @@ -4,7 +4,7 @@ import sprintfClass from 'sprintf-js' import { MatchExtended } from '@zxcvbn-ts/core/src/types' import Matching from '../../packages/libraries/main/src/Matching' import estimateGuesses from '../../packages/libraries/main/src/scoring/estimate' -import { zxcvbnOptions } from '../../packages/libraries/main/src/Options' +import Options from '../../packages/libraries/main/src/Options' const CUTOFF = 10 const BATCH_SIZE = 1000000 @@ -12,8 +12,8 @@ const BATCH_SIZE = 1000000 // eslint-disable-next-line @typescript-eslint/no-unused-vars const { sprintf } = sprintfClass -zxcvbnOptions.setOptions() -const matching = new Matching() +const zxcvbnOptions = new Options() +const matching = new Matching(zxcvbnOptions) interface Counts { [key: string]: number @@ -49,7 +49,7 @@ export default class PasswordGenerator { // eslint-disable-next-line no-restricted-syntax for (const match of matches) { - if (estimateGuesses(match, password).guesses < xatoRank) { + if (estimateGuesses(zxcvbnOptions, match, password).guesses < xatoRank) { return false } } diff --git a/docs/.vuepress/components/Comparison.vue b/docs/.vuepress/components/Comparison.vue index cf92c350..b372b8f7 100755 --- a/docs/.vuepress/components/Comparison.vue +++ b/docs/.vuepress/components/Comparison.vue @@ -33,10 +33,7 @@ diff --git a/docs/guide/languages/README.md b/docs/guide/languages/README.md index af90198b..97df1f76 100644 --- a/docs/guide/languages/README.md +++ b/docs/guide/languages/README.md @@ -23,7 +23,7 @@ If you don't have an own translation system or want to use predefined translatio Each language pack has its own translation file that you can use like this: ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import { translations } from '@zxcvbn-ts/language-en' const password = 'somePassword' @@ -31,9 +31,8 @@ const options = { translations, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` ## Dictionary @@ -43,7 +42,7 @@ This makes the library tiny but inefficient compared to the original library. It is recommended to use at least the common and english language package. ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en' @@ -55,9 +54,8 @@ const options = { }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` ## Keyboard patterns @@ -66,7 +64,7 @@ By default, `zxcvbn-ts` don't use any keyboard patterns to let the developer dec It is recommended to use at least the common keyboard patterns. ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' const password = 'somePassword' @@ -74,9 +72,8 @@ const options = { graphs: zxcvbnCommonPackage.adjacencyGraphs, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` ## Add a new language package diff --git a/docs/guide/lazy-loading/README.md b/docs/guide/lazy-loading/README.md index dc8ee7fe..1f63f5ad 100644 --- a/docs/guide/lazy-loading/README.md +++ b/docs/guide/lazy-loading/README.md @@ -9,7 +9,7 @@ Webpack supports lazy-loading with some configuration; check out the [documentat Here's how you import it: ```js -import { zxcvbn } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' ``` This is how you lazy load dictionaries: @@ -43,8 +43,9 @@ Somewhere in your application you can call the "loadOptions" function, then the const run = async () => { const password = 'asdnlja978o' const options = await loadOptions() - zxcvbnOptions.setOptions(options) - const results = zxcvbn(password) + + const zxcvbn = new ZxcvbnFactory(options) + const results = zxcvbn.check(password) console.log(results) } ``` diff --git a/docs/guide/matcher/README.md b/docs/guide/matcher/README.md index 4318b83a..30e7768f 100644 --- a/docs/guide/matcher/README.md +++ b/docs/guide/matcher/README.md @@ -50,7 +50,7 @@ Custom matchers can be created if needed, including asynchronous matchers. If cr Here is an example of how to create a custom matcher to check for minimum password length. Please note that we do not recommend using a minimum length matcher. ```ts -import { zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import { MatchEstimated, ExtendedMatch, @@ -87,7 +87,9 @@ const minLengthMatcher: Matcher = { }, } -zxcvbnOptions.addMatcher('minLength', minLengthMatcher) +new ZxcvbnFactory(options, { + 'minLength': minLengthMatcher +}) ``` The Matching function needs to return an array of matched tokens. The four default properties (pattern, token, i, and j) are mandatory but the object can be extended as needed. diff --git a/docs/guide/migration/README.md b/docs/guide/migration/README.md index 321bcae7..a37ea3d9 100644 --- a/docs/guide/migration/README.md +++ b/docs/guide/migration/README.md @@ -1,5 +1,58 @@ # Migration +## `zxcvbn-ts 3.x.x` to `zxcvbn-ts 4.x.x` + +### Move from singleton options to class based approach + +Old: +``` +zxcvbnOptions.setOptions(options) + +zxcvbn(password) +``` + +New: + +``` +const zxcvbn = new ZxcvbnFactory(options, customMatcher) + +zxcvbn.check(password) +``` + + +### Custom matcher setup changed + +This is an example for the pwned custom matcher changes. Generally the options doesn't need to be transferred anymore. + +Old: +``` +zxcvbnOptions.setOptions(options) + +const pwnedOptions = { + url: string, + networkErrorHandler: Function +} +const matcherPwned = matcherPwnedFactory(crossFetch, zxcvbnOptions, pwnedOptions) +zxcvbnOptions.addMatcher('pwned', matcherPwned) + +zxcvbn(password) +``` + +New: + +``` +const pwnedOptions = { + url: string, + networkErrorHandler: Function +} +const customMatcher = { + pwned: matcherPwnedFactory(fetch, pwnedOptions), +} + +const zxcvbn = new ZxcvbnFactory(options, customMatcher) +zxcvbn.check(password) +``` + ## `zxcvbn-ts 2.x.x` to `zxcvbn-ts 3.x.x` ### language packages no longer have a default export diff --git a/docs/guide/user-input/README.md b/docs/guide/user-input/README.md index eac82b82..b47a0339 100644 --- a/docs/guide/user-input/README.md +++ b/docs/guide/user-input/README.md @@ -4,7 +4,7 @@ Often you want to check if the password matches some user content like their use For this purpose, add a `userInputs` dictionary with its own sanitizer. ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' const password = 'somePassword' const options = { @@ -12,16 +12,16 @@ const options = { userInputs: ['someEmail@email.de', 'someUsername'], }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` If you need to add the userInputs more dynamically your can add them as the second argument of the normal zxcvbn function like this ```js -import { zxcvbn } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' const password = 'somePassword' +const zxcvbn = new ZxcvbnFactory() zxcvbn(password, ['someEmail@email.de', 'someUsername']) ``` \ No newline at end of file diff --git a/packages/languages/ar/README.md b/packages/languages/ar/README.md index e48c525a..f0b81f55 100644 --- a/packages/languages/ar/README.md +++ b/packages/languages/ar/README.md @@ -15,7 +15,7 @@ The Arabic dictionary and language package for **zxcvbn-ts** ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnArPackage from '@zxcvbn-ts/language-ar' @@ -28,7 +28,6 @@ const options = { ...zxcvbnArPackage.dictionary, }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/common/README.md b/packages/languages/common/README.md index 8615fba5..74e0a05f 100644 --- a/packages/languages/common/README.md +++ b/packages/languages/common/README.md @@ -15,7 +15,7 @@ The common dictionary and language package for zxcvbn-ts ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' const password = 'somePassword' @@ -23,7 +23,6 @@ const options = { ...zxcvbnCommonPackage, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/cs/README.md b/packages/languages/cs/README.md index b06d9afe..faa5ad5e 100644 --- a/packages/languages/cs/README.md +++ b/packages/languages/cs/README.md @@ -16,7 +16,7 @@ The Czech dictionary and language package for zxcvbn-ts ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnCsPackage from '@zxcvbn-ts/language-cs' @@ -29,8 +29,6 @@ const options = { ...zxcvbnCsPackage.dictionary, }, } - -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/de/README.md b/packages/languages/de/README.md index c40b8611..45fada14 100644 --- a/packages/languages/de/README.md +++ b/packages/languages/de/README.md @@ -15,7 +15,7 @@ The German dictionary and language package for **zxcvbn-ts** ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnDePackage from '@zxcvbn-ts/language-de' @@ -28,7 +28,6 @@ const options = { ...zxcvbnDePackage.dictionary, }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/en/README.md b/packages/languages/en/README.md index 3b76ccef..b14106ac 100644 --- a/packages/languages/en/README.md +++ b/packages/languages/en/README.md @@ -15,7 +15,7 @@ The English dictionary and language package for zxcvbn-ts ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en' @@ -29,7 +29,6 @@ const options = { }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/es-es/README.md b/packages/languages/es-es/README.md index d6044f68..1fe56ead 100644 --- a/packages/languages/es-es/README.md +++ b/packages/languages/es-es/README.md @@ -15,7 +15,7 @@ The European Spanish dictionary and language package for zxcvbn-ts ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnEsEsPackage from '@zxcvbn-ts/language-es-es' @@ -29,7 +29,6 @@ const options = { }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/fi/README.md b/packages/languages/fi/README.md index 7dda0408..42ea9171 100644 --- a/packages/languages/fi/README.md +++ b/packages/languages/fi/README.md @@ -19,7 +19,7 @@ Data sources for first and last names: ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnFiPackage from '@zxcvbn-ts/language-fi' @@ -33,9 +33,8 @@ const options = { }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` ## source: diff --git a/packages/languages/fr/README.md b/packages/languages/fr/README.md index d1b7cea6..de41a5f7 100644 --- a/packages/languages/fr/README.md +++ b/packages/languages/fr/README.md @@ -15,7 +15,7 @@ The French dictionary and language package for zxcvbn-ts ## Setup ```js -import zxcvbn from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr' @@ -28,5 +28,6 @@ const options = { }, } -zxcvbn(password, options) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/id/README.md b/packages/languages/id/README.md index d3223db3..2cf0e72b 100644 --- a/packages/languages/id/README.md +++ b/packages/languages/id/README.md @@ -21,7 +21,7 @@ The Indonesia dictionary and language package for zxcvbn-ts ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnIdPackage from '@zxcvbn-ts/language-id' @@ -35,7 +35,6 @@ const options = { }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/it/README.md b/packages/languages/it/README.md index 5fccd566..ddabfbaa 100644 --- a/packages/languages/it/README.md +++ b/packages/languages/it/README.md @@ -15,7 +15,7 @@ The Italian dictionary and language package for zxcvbn-ts ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnItPackage from '@zxcvbn-ts/language-it' @@ -29,7 +29,6 @@ const options = { }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/ja/README.md b/packages/languages/ja/README.md index f0d4a7ee..36311526 100644 --- a/packages/languages/ja/README.md +++ b/packages/languages/ja/README.md @@ -15,7 +15,7 @@ The Japanese dictionary and language package for zxcvbn-ts ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja' @@ -29,7 +29,6 @@ const options = { }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/languages/nl-be/README.md b/packages/languages/nl-be/README.md index 29184f0e..eaf8c94d 100644 --- a/packages/languages/nl-be/README.md +++ b/packages/languages/nl-be/README.md @@ -15,7 +15,7 @@ Contains Dutch words specific to Belgium and common Dutch words ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnNlBePackage from '@zxcvbn-ts/language-nl-be' @@ -28,9 +28,8 @@ const options = { ...zxcvbnNlBePackage.dictionary, }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` ## Sources diff --git a/packages/languages/pl/README.md b/packages/languages/pl/README.md index c9b7ec67..e146b033 100644 --- a/packages/languages/pl/README.md +++ b/packages/languages/pl/README.md @@ -15,7 +15,7 @@ The Polish dictionary and language package for zxcvbn-ts ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnPlPackage from '@zxcvbn-ts/language-pl' @@ -29,9 +29,8 @@ const options = { }, } -zxcvbnOptions.setOptions(options) - -zxcvbn(password) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` ## Sources diff --git a/packages/languages/pt-br/README.md b/packages/languages/pt-br/README.md index 548da830..c6f211d2 100644 --- a/packages/languages/pt-br/README.md +++ b/packages/languages/pt-br/README.md @@ -15,7 +15,7 @@ The Brazilian portuguese dictionary and language package for zxcvbn-ts ## Setup ```js -import zxcvbn from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnPtBrPackage from '@zxcvbn-ts/language-pt-br' @@ -28,5 +28,6 @@ const options = { }, } -zxcvbn(password, options) +const zxcvbn = new ZxcvbnFactory(options) +zxcvbn.check(password) ``` diff --git a/packages/libraries/main/README.md b/packages/libraries/main/README.md index 88e88996..8b085aef 100644 --- a/packages/libraries/main/README.md +++ b/packages/libraries/main/README.md @@ -18,11 +18,10 @@ and recognizes common patterns like dates, repetitions (e.g. 'aaa'), sequences ( ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en' -const password = 'somePassword' const options = { dictionary: { ...zxcvbnCommonPackage.dictionary, @@ -31,7 +30,8 @@ const options = { graphs: zxcvbnCommonPackage.adjacencyGraphs, translations: zxcvbnEnPackage.translations, } -zxcvbnOptions.setOptions(options) +const zxcvbn = new ZxcvbnFactory(options) +const password = 'somePassword' zxcvbn(password) ``` diff --git a/packages/libraries/main/src/Feedback.ts b/packages/libraries/main/src/Feedback.ts index 44ba2119..53f16440 100644 --- a/packages/libraries/main/src/Feedback.ts +++ b/packages/libraries/main/src/Feedback.ts @@ -1,4 +1,4 @@ -import { zxcvbnOptions } from './Options' +import Options from './Options' import { DefaultFeedbackFunction, FeedbackType, MatchEstimated } from './types' import bruteforceMatcher from './matcher/bruteforce/feedback' import dateMatcher from './matcher/date/feedback' @@ -23,7 +23,7 @@ type Matchers = { * ------------------------------------------------------------------------------- */ class Feedback { - readonly matchers: Matchers = { + private readonly matchers: Matchers = { bruteforce: bruteforceMatcher, date: dateMatcher, dictionary: dictionaryMatcher, @@ -34,30 +34,30 @@ class Feedback { separator: separatorMatcher, } - defaultFeedback: FeedbackType = { + private defaultFeedback: FeedbackType = { warning: null, suggestions: [], } - constructor() { + constructor(private options: Options) { this.setDefaultSuggestions() } - setDefaultSuggestions() { + private setDefaultSuggestions() { this.defaultFeedback.suggestions.push( - zxcvbnOptions.translations.suggestions.useWords, - zxcvbnOptions.translations.suggestions.noNeed, + this.options.translations.suggestions.useWords, + this.options.translations.suggestions.noNeed, ) } - getFeedback(score: number, sequence: MatchEstimated[]) { + public getFeedback(score: number, sequence: MatchEstimated[]) { if (sequence.length === 0) { return this.defaultFeedback } if (score > 2) { return defaultFeedback } - const extraFeedback = zxcvbnOptions.translations.suggestions.anotherWord + const extraFeedback = this.options.translations.suggestions.anotherWord const longestMatch = this.getLongestMatch(sequence) let feedback = this.getMatchFeedback(longestMatch, sequence.length === 1) if (feedback !== null && feedback !== undefined) { @@ -71,7 +71,7 @@ class Feedback { return feedback } - getLongestMatch(sequence: MatchEstimated[]) { + private getLongestMatch(sequence: MatchEstimated[]) { let longestMatch = sequence[0] const slicedSequence = sequence.slice(1) slicedSequence.forEach((match: MatchEstimated) => { @@ -82,15 +82,19 @@ class Feedback { return longestMatch } - getMatchFeedback(match: MatchEstimated, isSoleMatch: boolean) { + private getMatchFeedback(match: MatchEstimated, isSoleMatch: boolean) { if (this.matchers[match.pattern]) { - return this.matchers[match.pattern](match, isSoleMatch) + return this.matchers[match.pattern](this.options, match, isSoleMatch) } if ( - zxcvbnOptions.matchers[match.pattern] && - 'feedback' in zxcvbnOptions.matchers[match.pattern] + this.options.matchers[match.pattern] && + 'feedback' in this.options.matchers[match.pattern] ) { - return zxcvbnOptions.matchers[match.pattern].feedback(match, isSoleMatch) + return this.options.matchers[match.pattern].feedback( + this.options, + match, + isSoleMatch, + ) } return defaultFeedback } diff --git a/packages/libraries/main/src/Matching.ts b/packages/libraries/main/src/Matching.ts index e7d6ee11..2a08d4e2 100644 --- a/packages/libraries/main/src/Matching.ts +++ b/packages/libraries/main/src/Matching.ts @@ -1,5 +1,5 @@ -import { extend, sorted } from './helper' -import { MatchExtended, MatchingType } from './types' +import { extend, sorted } from './utils/helper' +import { MatchExtended, MatchingType, UserInputsOptions } from './types' import dateMatcher from './matcher/date/matching' import dictionaryMatcher from './matcher/dictionary/matching' import regexMatcher from './matcher/regex/matching' @@ -7,7 +7,7 @@ import repeatMatcher from './matcher/repeat/matching' import sequenceMatcher from './matcher/sequence/matching' import spatialMatcher from './matcher/spatial/matching' import separatorMatcher from './matcher/separator/matching' -import { zxcvbnOptions } from './Options' +import Options from './Options' /* * ------------------------------------------------------------------------------- @@ -20,7 +20,7 @@ type Matchers = { } class Matching { - readonly matchers: Matchers = { + private readonly matchers: Matchers = { date: dateMatcher, dictionary: dictionaryMatcher, regex: regexMatcher, @@ -31,38 +31,40 @@ class Matching { separator: separatorMatcher, } - match(password: string): MatchExtended[] | Promise { - const matches: MatchExtended[] = [] + constructor(private options: Options) {} - const promises: Promise[] = [] - 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, + private matcherFactory(name: string) { + if (!this.matchers[name] && !this.options.matchers[name]) { + return null + } + const Matcher = this.matchers[name] + ? this.matchers[name] + : this.options.matchers[name].Matching + + return new Matcher(this.options) + } + + private processResult( + matches: MatchExtended[], + promises: Promise[], + result: MatchExtended[] | Promise, + ) { + if (result instanceof Promise) { + result.then((response) => { + extend(matches, response) }) + promises.push(result) + } else { + extend(matches, result) + } + } - if (result instanceof Promise) { - result.then((response) => { - extend(matches, response) - }) - promises.push(result) - } else { - extend(matches, result) - } - }) + private handlePromises( + matches: MatchExtended[], + promises: Promise[], + ) { if (promises.length > 0) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { Promise.all(promises) .then(() => { resolve(sorted(matches)) @@ -74,6 +76,35 @@ class Matching { } return sorted(matches) } + + match( + password: string, + userInputsOptions?: UserInputsOptions, + ): MatchExtended[] | Promise { + const matches: MatchExtended[] = [] + + const promises: Promise[] = [] + const matchers = [ + ...Object.keys(this.matchers), + ...Object.keys(this.options.matchers), + ] + matchers.forEach((key) => { + const matcher = this.matcherFactory(key) + if (!matcher) { + return + } + const result = matcher.match({ + password, + omniMatch: this, + userInputsOptions, + }) + + // extends matches and promises by references + this.processResult(matches, promises, result) + }) + + return this.handlePromises(matches, promises) + } } export default Matching diff --git a/packages/libraries/main/src/Options.ts b/packages/libraries/main/src/Options.ts index 1e72ab5e..3a1dc833 100644 --- a/packages/libraries/main/src/Options.ts +++ b/packages/libraries/main/src/Options.ts @@ -1,4 +1,4 @@ -import { buildRankedDictionary } from './helper' +import { buildRankedDictionary } from './utils/helper' import { TranslationKeys, OptionsType, @@ -8,45 +8,53 @@ import { RankedDictionaries, Matchers, Matcher, + UserInputsOptions, + RankedDictionary, } from './types' import l33tTable from './data/l33tTable' import translationKeys from './data/translationKeys' import TrieNode from './matcher/dictionary/variants/matching/unmunger/TrieNode' import l33tTableToTrieNode from './matcher/dictionary/variants/matching/unmunger/l33tTableToTrieNode' -export class Options { - matchers: Matchers = {} +export default class Options { + public matchers: Matchers = {} - l33tTable: OptionsL33tTable = l33tTable + public l33tTable: OptionsL33tTable = l33tTable - trieNodeRoot: TrieNode = l33tTableToTrieNode(l33tTable, new TrieNode()) + public trieNodeRoot: TrieNode = l33tTableToTrieNode(l33tTable, new TrieNode()) - dictionary: OptionsDictionary = { + public dictionary: OptionsDictionary = { userInputs: [], } - rankedDictionaries: RankedDictionaries = {} + public rankedDictionaries: RankedDictionaries = {} - rankedDictionariesMaxWordSize: Record = {} + public rankedDictionariesMaxWordSize: Record = {} - translations: TranslationKeys = translationKeys + public translations: TranslationKeys = translationKeys - graphs: OptionsGraph = {} + public graphs: OptionsGraph = {} - useLevenshteinDistance: boolean = false + public useLevenshteinDistance: boolean = false - levenshteinThreshold: number = 2 + public levenshteinThreshold: number = 2 - l33tMaxSubstitutions: number = 100 + public l33tMaxSubstitutions: number = 100 - maxLength: number = 256 + public maxLength: number = 256 - constructor() { - this.setRankedDictionaries() + constructor( + options: OptionsType = {}, + customMatchers: Record = {}, + ) { + this.setOptions(options) + Object.entries(customMatchers).forEach(([name, matcher]) => { + this.addMatcher(name, matcher) + }) } // eslint-disable-next-line max-statements,complexity - setOptions(options: OptionsType = {}) { + private setOptions(options: OptionsType = {}) { if (options.l33tTable) { this.l33tTable = options.l33tTable this.trieNodeRoot = l33tTableToTrieNode(options.l33tTable, new TrieNode()) @@ -83,7 +91,7 @@ export class Options { } } - setTranslations(translations: TranslationKeys) { + private setTranslations(translations: TranslationKeys) { if (this.checkCustomTranslations(translations)) { this.translations = translations } else { @@ -91,7 +99,7 @@ export class Options { } } - checkCustomTranslations(translations: TranslationKeys) { + private checkCustomTranslations(translations: TranslationKeys) { let valid = true Object.keys(translationKeys).forEach((type) => { if (type in translations) { @@ -108,7 +116,7 @@ export class Options { return valid } - setRankedDictionaries() { + private setRankedDictionaries() { const rankedDictionaries: RankedDictionaries = {} const rankedDictionariesMaxWorkSize: Record = {} Object.keys(this.dictionary).forEach((name) => { @@ -120,7 +128,7 @@ export class Options { this.rankedDictionariesMaxWordSize = rankedDictionariesMaxWorkSize } - getRankedDictionariesMaxWordSize(list: (string | number)[]) { + private getRankedDictionariesMaxWordSize(list: (string | number)[]) { const data = list.map((el) => { if (typeof el !== 'string') { return el.toString().length @@ -135,7 +143,7 @@ export class Options { return data.reduce((a, b) => Math.max(a, b), -Infinity) } - buildSanitizedRankedDictionary(list: (string | number)[]) { + private buildSanitizedRankedDictionary(list: (string | number)[]) { const sanitizedInputs: string[] = [] list.forEach((input: string | number | boolean) => { @@ -152,19 +160,24 @@ export class Options { return buildRankedDictionary(sanitizedInputs) } - extendUserInputsDictionary(dictionary: (string | number)[]) { - if (!this.dictionary.userInputs) { - this.dictionary.userInputs = [] + public getUserInputsOptions( + dictionary?: (string | number)[], + ): UserInputsOptions { + let rankedDictionary: RankedDictionary = {} + let rankedDictionaryMaxWordSize: number = 0 + if (dictionary) { + rankedDictionary = this.buildSanitizedRankedDictionary(dictionary) + rankedDictionaryMaxWordSize = + this.getRankedDictionariesMaxWordSize(dictionary) } - const newList = [...this.dictionary.userInputs, ...dictionary] - this.rankedDictionaries.userInputs = - this.buildSanitizedRankedDictionary(newList) - this.rankedDictionariesMaxWordSize.userInputs = - this.getRankedDictionariesMaxWordSize(newList) + return { + rankedDictionary, + rankedDictionaryMaxWordSize, + } } - public addMatcher(name: string, matcher: Matcher) { + private addMatcher(name: string, matcher: Matcher) { if (this.matchers[name]) { console.info(`Matcher ${name} already exists`) } else { @@ -172,5 +185,3 @@ export class Options { } } } - -export const zxcvbnOptions = new Options() diff --git a/packages/libraries/main/src/TimeEstimates.ts b/packages/libraries/main/src/TimeEstimates.ts index de6a305a..271a0c61 100644 --- a/packages/libraries/main/src/TimeEstimates.ts +++ b/packages/libraries/main/src/TimeEstimates.ts @@ -1,4 +1,4 @@ -import { zxcvbnOptions } from './Options' +import Options from './Options' import { CrackTimesDisplay, CrackTimesSeconds, Score } from './types' const SECOND = 1 @@ -25,19 +25,9 @@ const times = { * ------------------------------------------------------------------------------- */ class TimeEstimates { - translate(displayStr: string, value: number | undefined) { - let key = displayStr - if (value !== undefined && value !== 1) { - key += 's' - } - const { timeEstimation } = zxcvbnOptions.translations - return timeEstimation[key as keyof typeof timeEstimation].replace( - '{base}', - `${value}`, - ) - } + constructor(private options: Options) {} - estimateAttackTimes(guesses: number) { + public estimateAttackTimes(guesses: number) { const crackTimesSeconds: CrackTimesSeconds = { onlineThrottling100PerHour: guesses / (100 / 3600), onlineNoThrottling10PerSecond: guesses / 10, @@ -62,7 +52,7 @@ class TimeEstimates { } } - guessesToScore(guesses: number): Score { + private guessesToScore(guesses: number): Score { const DELTA = 5 if (guesses < 1e3 + DELTA) { // risky password: "too guessable" @@ -85,7 +75,7 @@ class TimeEstimates { return 4 } - displayTime(seconds: number) { + private displayTime(seconds: number) { let displayStr = 'centuries' let base const timeKeys = Object.keys(times) @@ -102,6 +92,18 @@ class TimeEstimates { } return this.translate(displayStr, base) } + + private translate(displayStr: string, value: number | undefined) { + let key = displayStr + if (value !== undefined && value !== 1) { + key += 's' + } + const { timeEstimation } = this.options.translations + return timeEstimation[key as keyof typeof timeEstimation].replace( + '{base}', + `${value}`, + ) + } } export default TimeEstimates diff --git a/packages/libraries/main/src/index.ts b/packages/libraries/main/src/index.ts index 04c38405..2a9a9484 100644 --- a/packages/libraries/main/src/index.ts +++ b/packages/libraries/main/src/index.ts @@ -1,67 +1,91 @@ import Matching from './Matching' -import scoring from './scoring' import TimeEstimates from './TimeEstimates' import Feedback from './Feedback' -import { zxcvbnOptions, Options } from './Options' -import debounce from './debounce' -import { MatchExtended, ZxcvbnResult } from './types' +import Options from './Options' +import debounce from './utils/debounce' +import { + Matcher, + MatchEstimated, + MatchExtended, + OptionsType, + ZxcvbnResult, +} from './types' +import Scoring from './scoring' const time = () => new Date().getTime() -const createReturnValue = ( - resolvedMatches: MatchExtended[], - password: string, - start: number, -): ZxcvbnResult => { - 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), +class ZxcvbnFactory { + private options: Options + + private scoring: Scoring + + constructor( + options: OptionsType = {}, + customMatchers: Record = {}, + ) { + this.options = new Options(options, customMatchers) + this.scoring = new Scoring(this.options) } -} -const main = (password: string, userInputs?: (string | number)[]) => { - if (userInputs) { - zxcvbnOptions.extendUserInputsDictionary(userInputs) + private estimateAttackTimes(guesses: number) { + const timeEstimates = new TimeEstimates(this.options) + return timeEstimates.estimateAttackTimes(guesses) } - const matching = new Matching() + private getFeedback(score: number, sequence: MatchEstimated[]) { + const feedback = new Feedback(this.options) + return feedback.getFeedback(score, sequence) + } - return matching.match(password) -} + private createReturnValue( + resolvedMatches: MatchExtended[], + password: string, + start: number, + ): ZxcvbnResult { + const matchSequence = this.scoring.mostGuessableMatchSequence( + password, + resolvedMatches, + ) + const calcTime = time() - start + const attackTimes = this.estimateAttackTimes(matchSequence.guesses) -export const zxcvbn = (password: string, userInputs?: (string | number)[]) => { - const start = time() - const matches = main(password, userInputs) + return { + calcTime, + ...matchSequence, + ...attackTimes, + feedback: this.getFeedback(attackTimes.score, matchSequence.sequence), + } + } - if (matches instanceof Promise) { - throw new Error( - 'You are using a Promised matcher, please use `zxcvbnAsync` for it.', - ) + private main(password: string, userInputs?: (string | number)[]) { + const userInputsOptions = this.options.getUserInputsOptions(userInputs) + + const matching = new Matching(this.options) + + return matching.match(password, userInputsOptions) } - return createReturnValue(matches, password, start) -} -export const zxcvbnAsync = async ( - password: string, - userInputs?: (string | number)[], -): Promise => { - const usedPassword = password.substring(0, zxcvbnOptions.maxLength) - const start = time() - const matches = await main(usedPassword, userInputs) + public check(password: string, userInputs?: (string | number)[]) { + const reducedPassword = password.substring(0, this.options.maxLength) + const start = time() + const matches = this.main(reducedPassword, userInputs) - return createReturnValue(matches, usedPassword, start) + if (matches instanceof Promise) { + throw new Error( + 'You are using a Promised matcher, please use `zxcvbnAsync` for it.', + ) + } + return this.createReturnValue(matches, reducedPassword, start) + } + + public async checkAsync(password: string, userInputs?: (string | number)[]) { + const reducedPassword = password.substring(0, this.options.maxLength) + const start = time() + const matches = await this.main(reducedPassword, userInputs) + + return this.createReturnValue(matches, reducedPassword, start) + } } export * from './types' -export { zxcvbnOptions, Options, debounce } +export { ZxcvbnFactory, debounce } diff --git a/packages/libraries/main/src/matcher/date/feedback.ts b/packages/libraries/main/src/matcher/date/feedback.ts index 26250b8d..d6dc28a7 100644 --- a/packages/libraries/main/src/matcher/date/feedback.ts +++ b/packages/libraries/main/src/matcher/date/feedback.ts @@ -1,8 +1,8 @@ -import { zxcvbnOptions } from '../../Options' +import Options from '../../Options' -export default () => { +export default (options: Options) => { return { - warning: zxcvbnOptions.translations.warnings.dates, - suggestions: [zxcvbnOptions.translations.suggestions.dates], + warning: options.translations.warnings.dates, + suggestions: [options.translations.suggestions.dates], } } diff --git a/packages/libraries/main/src/matcher/date/matching.ts b/packages/libraries/main/src/matcher/date/matching.ts index d60e40dd..9ea993b2 100644 --- a/packages/libraries/main/src/matcher/date/matching.ts +++ b/packages/libraries/main/src/matcher/date/matching.ts @@ -4,12 +4,11 @@ import { DATE_SPLITS, REFERENCE_YEAR, } from '../../data/const' -import { sorted } from '../../helper' -import { DateMatch } from '../../types' +import { sorted } from '../../utils/helper' +import { DateMatch, MatchOptions } from '../../types' +import Options from '../../Options' -interface DateMatchOptions { - password: string -} +type DateMatchOptions = Pick /* * ------------------------------------------------------------------------------- @@ -17,6 +16,8 @@ interface DateMatchOptions { * ------------------------------------------------------------------------------- */ class MatchDate { + constructor(private options: Options) {} + /* * a "date" is recognized as: * any 3-tuple that starts or ends with a 2- or 4-digit year, diff --git a/packages/libraries/main/src/matcher/dictionary/feedback.ts b/packages/libraries/main/src/matcher/dictionary/feedback.ts index 8c683084..a4f815ed 100644 --- a/packages/libraries/main/src/matcher/dictionary/feedback.ts +++ b/packages/libraries/main/src/matcher/dictionary/feedback.ts @@ -1,79 +1,90 @@ -import { zxcvbnOptions } from '../../Options' +import Options from '../../Options' import { MatchEstimated } from '../../types' import { ALL_UPPER_INVERTED, START_UPPER } from '../../data/const' const getDictionaryWarningPassword = ( + options: Options, match: MatchEstimated, isSoleMatch?: boolean, ) => { let warning: string | null = null if (isSoleMatch && !match.l33t && !match.reversed) { if (match.rank <= 10) { - warning = zxcvbnOptions.translations.warnings.topTen + warning = options.translations.warnings.topTen } else if (match.rank <= 100) { - warning = zxcvbnOptions.translations.warnings.topHundred + warning = options.translations.warnings.topHundred } else { - warning = zxcvbnOptions.translations.warnings.common + warning = options.translations.warnings.common } } else if (match.guessesLog10 <= 4) { - warning = zxcvbnOptions.translations.warnings.similarToCommon + warning = options.translations.warnings.similarToCommon } return warning } const getDictionaryWarningWikipedia = ( + options: Options, match: MatchEstimated, isSoleMatch?: boolean, ) => { let warning: string | null = null if (isSoleMatch) { - warning = zxcvbnOptions.translations.warnings.wordByItself + warning = options.translations.warnings.wordByItself } return warning } const getDictionaryWarningNames = ( + options: Options, match: MatchEstimated, isSoleMatch?: boolean, ) => { if (isSoleMatch) { - return zxcvbnOptions.translations.warnings.namesByThemselves + return options.translations.warnings.namesByThemselves } - return zxcvbnOptions.translations.warnings.commonNames + return options.translations.warnings.commonNames } -const getDictionaryWarning = (match: MatchEstimated, isSoleMatch?: boolean) => { +const getDictionaryWarning = ( + options: Options, + match: MatchEstimated, + isSoleMatch?: boolean, +) => { let warning: string | null = null const dictName = match.dictionaryName const isAName = dictName === 'lastnames' || dictName.toLowerCase().includes('firstnames') if (dictName === 'passwords') { - warning = getDictionaryWarningPassword(match, isSoleMatch) + warning = getDictionaryWarningPassword(options, match, isSoleMatch) } else if (dictName.includes('wikipedia')) { - warning = getDictionaryWarningWikipedia(match, isSoleMatch) + warning = getDictionaryWarningWikipedia(options, match, isSoleMatch) } else if (isAName) { - warning = getDictionaryWarningNames(match, isSoleMatch) + warning = getDictionaryWarningNames(options, match, isSoleMatch) } else if (dictName === 'userInputs') { - warning = zxcvbnOptions.translations.warnings.userInputs + warning = options.translations.warnings.userInputs } return warning } -export default (match: MatchEstimated, isSoleMatch?: boolean) => { - const warning = getDictionaryWarning(match, isSoleMatch) +export default ( + options: Options, + match: MatchEstimated, + isSoleMatch?: boolean, +) => { + const warning = getDictionaryWarning(options, match, isSoleMatch) const suggestions: string[] = [] const word = match.token if (word.match(START_UPPER)) { - suggestions.push(zxcvbnOptions.translations.suggestions.capitalization) + suggestions.push(options.translations.suggestions.capitalization) } else if (word.match(ALL_UPPER_INVERTED) && word.toLowerCase() !== word) { - suggestions.push(zxcvbnOptions.translations.suggestions.allUppercase) + suggestions.push(options.translations.suggestions.allUppercase) } if (match.reversed && match.token.length >= 4) { - suggestions.push(zxcvbnOptions.translations.suggestions.reverseWords) + suggestions.push(options.translations.suggestions.reverseWords) } if (match.l33t) { - suggestions.push(zxcvbnOptions.translations.suggestions.l33t) + suggestions.push(options.translations.suggestions.l33t) } return { warning, diff --git a/packages/libraries/main/src/matcher/dictionary/matching.ts b/packages/libraries/main/src/matcher/dictionary/matching.ts index b865447d..ac253620 100644 --- a/packages/libraries/main/src/matcher/dictionary/matching.ts +++ b/packages/libraries/main/src/matcher/dictionary/matching.ts @@ -1,45 +1,53 @@ import findLevenshteinDistance, { FindLevenshteinDistanceResult, -} from '../../levenshtein' -import { sorted } from '../../helper' -import { zxcvbnOptions } from '../../Options' +} from '../../utils/levenshtein' +import { sorted } from '../../utils/helper' +import Options from '../../Options' import { DictionaryNames, DictionaryMatch, L33tMatch } from '../../types' import Reverse from './variants/matching/reverse' import L33t from './variants/matching/l33t' import { DictionaryMatchOptions } from './types' +import mergeUserInputDictionary from '../../utils/mergeUserInputDictionary' class MatchDictionary { l33t: L33t reverse: Reverse - constructor() { - this.l33t = new L33t(this.defaultMatch) - this.reverse = new Reverse(this.defaultMatch) + constructor(private options: Options) { + this.l33t = new L33t(options, this.defaultMatch) + this.reverse = new Reverse(options, this.defaultMatch) } - match({ password }: DictionaryMatchOptions) { + match(matchOptions: DictionaryMatchOptions) { const matches = [ - ...(this.defaultMatch({ - password, - }) as DictionaryMatch[]), - ...(this.reverse.match({ password }) as DictionaryMatch[]), - ...(this.l33t.match({ password }) as L33tMatch[]), + ...(this.defaultMatch(matchOptions) as DictionaryMatch[]), + ...(this.reverse.match(matchOptions) as DictionaryMatch[]), + ...(this.l33t.match(matchOptions) as L33tMatch[]), ] return sorted(matches) } - defaultMatch({ password, useLevenshtein = true }: DictionaryMatchOptions) { + defaultMatch({ + password, + userInputsOptions, + useLevenshtein = true, + }: DictionaryMatchOptions) { const matches: DictionaryMatch[] = [] const passwordLength = password.length const passwordLower = password.toLowerCase() + const { rankedDictionaries, rankedDictionariesMaxWordSize } = + mergeUserInputDictionary( + this.options.rankedDictionaries, + this.options.rankedDictionariesMaxWordSize, + userInputsOptions, + ) // eslint-disable-next-line complexity,max-statements - Object.keys(zxcvbnOptions.rankedDictionaries).forEach((dictionaryName) => { - const rankedDict = - zxcvbnOptions.rankedDictionaries[dictionaryName as DictionaryNames] + Object.keys(rankedDictionaries).forEach((dictionaryName) => { + const rankedDict = rankedDictionaries[dictionaryName as DictionaryNames] const longestDictionaryWordSize = - zxcvbnOptions.rankedDictionariesMaxWordSize[dictionaryName] + rankedDictionariesMaxWordSize[dictionaryName] const searchWidth = Math.min(longestDictionaryWordSize, passwordLength) for (let i = 0; i < passwordLength; i += 1) { const searchEnd = Math.min(i + searchWidth, passwordLength) @@ -52,7 +60,7 @@ class MatchDictionary { // and because otherwise there would be to many false positives const isFullPassword = i === 0 && j === passwordLength - 1 if ( - zxcvbnOptions.useLevenshteinDistance && + this.options.useLevenshteinDistance && isFullPassword && !isInDictionary && useLevenshtein @@ -60,7 +68,7 @@ class MatchDictionary { foundLevenshteinDistance = findLevenshteinDistance( usedPassword, rankedDict, - zxcvbnOptions.levenshteinThreshold, + this.options.levenshteinThreshold, ) } const isLevenshteinMatch = diff --git a/packages/libraries/main/src/matcher/dictionary/types.ts b/packages/libraries/main/src/matcher/dictionary/types.ts index a5058a71..34b8d961 100644 --- a/packages/libraries/main/src/matcher/dictionary/types.ts +++ b/packages/libraries/main/src/matcher/dictionary/types.ts @@ -1,10 +1,14 @@ -import { DictionaryMatch } from '../../types' +import { DictionaryMatch, MatchOptions } from '../../types' -export interface DictionaryMatchOptions { - password: string +export interface DictionaryMatchOptionsLevenshtein extends MatchOptions { useLevenshtein?: boolean } +export type DictionaryMatchOptions = Pick< + DictionaryMatchOptionsLevenshtein, + 'password' | 'userInputsOptions' | 'useLevenshtein' +> + export type DefaultMatch = ( options: DictionaryMatchOptions, ) => DictionaryMatch[] diff --git a/packages/libraries/main/src/matcher/dictionary/variants/matching/l33t.ts b/packages/libraries/main/src/matcher/dictionary/variants/matching/l33t.ts index ab88929a..8bac2e59 100644 --- a/packages/libraries/main/src/matcher/dictionary/variants/matching/l33t.ts +++ b/packages/libraries/main/src/matcher/dictionary/variants/matching/l33t.ts @@ -1,6 +1,6 @@ -import { zxcvbnOptions } from '../../../../Options' +import Options from '../../../../Options' import { DictionaryMatch, L33tMatch } from '../../../../types' -import { DefaultMatch } from '../../types' +import { DefaultMatch, DictionaryMatchOptions } from '../../types' import getCleanPasswords, { PasswordChanges, PasswordWithSubs, @@ -54,11 +54,10 @@ const getExtras = ( * ------------------------------------------------------------------------------- */ class MatchL33t { - defaultMatch: DefaultMatch - - constructor(defaultMatch: DefaultMatch) { - this.defaultMatch = defaultMatch - } + constructor( + private options: Options, + private defaultMatch: DefaultMatch, + ) {} isAlreadyIncluded(matches: L33tMatch[], newMatch: L33tMatch) { return matches.some((l33tMatch) => { @@ -68,12 +67,12 @@ class MatchL33t { }) } - match({ password }: { password: string }) { + match(matchOptions: DictionaryMatchOptions) { const matches: L33tMatch[] = [] const subbedPasswords = getCleanPasswords( - password, - zxcvbnOptions.l33tMaxSubstitutions, - zxcvbnOptions.trieNodeRoot, + matchOptions.password, + this.options.l33tMaxSubstitutions, + this.options.trieNodeRoot, ) let hasFullMatch = false subbedPasswords.forEach((subbedPassword) => { @@ -81,15 +80,20 @@ class MatchL33t { return } const matchedDictionary = this.defaultMatch({ + ...matchOptions, password: subbedPassword.password, useLevenshtein: subbedPassword.isFullSubstitution, }) matchedDictionary.forEach((match: DictionaryMatch) => { if (!hasFullMatch) { - hasFullMatch = match.i === 0 && match.j === password.length - 1 + hasFullMatch = + match.i === 0 && match.j === matchOptions.password.length - 1 } const extras = getExtras(subbedPassword, match.i, match.j) - const token = password.slice(extras.i, +extras.j + 1 || 9e9) + const token = matchOptions.password.slice( + extras.i, + +extras.j + 1 || 9e9, + ) const newMatch: L33tMatch = { ...match, l33t: true, diff --git a/packages/libraries/main/src/matcher/dictionary/variants/matching/reverse.ts b/packages/libraries/main/src/matcher/dictionary/variants/matching/reverse.ts index ecc571ae..6fe81340 100644 --- a/packages/libraries/main/src/matcher/dictionary/variants/matching/reverse.ts +++ b/packages/libraries/main/src/matcher/dictionary/variants/matching/reverse.ts @@ -1,5 +1,6 @@ import { DictionaryMatch } from '../../../../types' -import { DefaultMatch } from '../../types' +import { DefaultMatch, DictionaryMatchOptions } from '../../types' +import Options from '../../../../Options' /* * ------------------------------------------------------------------------------- @@ -7,23 +8,23 @@ import { DefaultMatch } from '../../types' * ------------------------------------------------------------------------------- */ class MatchReverse { - defaultMatch: DefaultMatch + constructor( + private options: Options, + private defaultMatch: DefaultMatch, + ) {} - constructor(defaultMatch: DefaultMatch) { - this.defaultMatch = defaultMatch - } - - match({ password }: { password: string }) { - const passwordReversed = password.split('').reverse().join('') + match(matchOptions: DictionaryMatchOptions) { + const passwordReversed = matchOptions.password.split('').reverse().join('') return this.defaultMatch({ + ...matchOptions, password: passwordReversed, }).map((match: DictionaryMatch) => ({ ...match, token: match.token.split('').reverse().join(''), // reverse back reversed: true, // map coordinates back to original string - i: password.length - 1 - match.j, - j: password.length - 1 - match.i, + i: matchOptions.password.length - 1 - match.j, + j: matchOptions.password.length - 1 - match.i, })) } } diff --git a/packages/libraries/main/src/matcher/dictionary/variants/matching/unmunger/getCleanPasswords.ts b/packages/libraries/main/src/matcher/dictionary/variants/matching/unmunger/getCleanPasswords.ts index 45bc77c7..cf743d73 100644 --- a/packages/libraries/main/src/matcher/dictionary/variants/matching/unmunger/getCleanPasswords.ts +++ b/packages/libraries/main/src/matcher/dictionary/variants/matching/unmunger/getCleanPasswords.ts @@ -76,7 +76,11 @@ class CleanPasswords { if (index === this.substr.length) { if (onlyFullSub === isFullSub) { - this.finalPasswords.push({ password: this.buffer.join(''), changes, isFullSubstitution: onlyFullSub }) + this.finalPasswords.push({ + password: this.buffer.join(''), + changes, + isFullSubstitution: onlyFullSub, + }) } return } @@ -93,10 +97,7 @@ class CleanPasswords { // Skip if this would be a 4th or more consecutive substitution of the same letter // this should work in all language as there shouldn't be the same letter more than four times in a row // So we can ignore the rest to save calculation time - if ( - lastSubLetter === sub && - consecutiveSubCount >= 3 - ) { + if (lastSubLetter === sub && consecutiveSubCount >= 3) { // eslint-disable-next-line no-continue continue } @@ -120,9 +121,7 @@ class CleanPasswords { changes: newSubs, lastSubLetter: sub, consecutiveSubCount: - lastSubLetter === sub - ? consecutiveSubCount + 1 - : 1, + lastSubLetter === sub ? consecutiveSubCount + 1 : 1, }) // backtrack by ignoring the added postfix this.buffer.pop() diff --git a/packages/libraries/main/src/matcher/regex/feedback.ts b/packages/libraries/main/src/matcher/regex/feedback.ts index db552ccb..56b74936 100644 --- a/packages/libraries/main/src/matcher/regex/feedback.ts +++ b/packages/libraries/main/src/matcher/regex/feedback.ts @@ -1,13 +1,13 @@ -import { zxcvbnOptions } from '../../Options' +import Options from '../../Options' import { MatchEstimated } from '../../types' -export default (match: MatchEstimated) => { +export default (options: Options, match: MatchEstimated) => { if (match.regexName === 'recentYear') { return { - warning: zxcvbnOptions.translations.warnings.recentYears, + warning: options.translations.warnings.recentYears, suggestions: [ - zxcvbnOptions.translations.suggestions.recentYears, - zxcvbnOptions.translations.suggestions.associatedYears, + options.translations.suggestions.recentYears, + options.translations.suggestions.associatedYears, ], } } diff --git a/packages/libraries/main/src/matcher/regex/matching.ts b/packages/libraries/main/src/matcher/regex/matching.ts index 99cda779..fb5b0463 100644 --- a/packages/libraries/main/src/matcher/regex/matching.ts +++ b/packages/libraries/main/src/matcher/regex/matching.ts @@ -1,11 +1,9 @@ import { REGEXEN } from '../../data/const' -import { sorted } from '../../helper' -import { RegexMatch } from '../../types' +import { sorted } from '../../utils/helper' +import { MatchOptions, RegexMatch } from '../../types' +import Options from '../../Options' -interface RegexMatchOptions { - password: string - regexes?: typeof REGEXEN -} +type RegexMatchOptions = Pick type RegexesKeys = keyof typeof REGEXEN /* @@ -14,10 +12,12 @@ type RegexesKeys = keyof typeof REGEXEN * ------------------------------------------------------------------------------- */ class MatchRegex { - match({ password, regexes = REGEXEN }: RegexMatchOptions) { + constructor(private options: Options) {} + + match({ password }: RegexMatchOptions) { const matches: RegexMatch[] = [] - Object.keys(regexes).forEach((name) => { - const regex = regexes[name as RegexesKeys] + Object.keys(REGEXEN).forEach((name) => { + const regex = REGEXEN[name as RegexesKeys] regex.lastIndex = 0 // keeps regexMatch stateless let regexMatch: RegExpExecArray | null diff --git a/packages/libraries/main/src/matcher/repeat/feedback.ts b/packages/libraries/main/src/matcher/repeat/feedback.ts index 077283a5..a6499d21 100644 --- a/packages/libraries/main/src/matcher/repeat/feedback.ts +++ b/packages/libraries/main/src/matcher/repeat/feedback.ts @@ -1,14 +1,14 @@ -import { zxcvbnOptions } from '../../Options' +import Options from '../../Options' import { MatchEstimated } from '../../types' -export default (match: MatchEstimated) => { - let warning = zxcvbnOptions.translations.warnings.extendedRepeat +export default (options: Options, match: MatchEstimated) => { + let warning = options.translations.warnings.extendedRepeat if (match.baseToken.length === 1) { - warning = zxcvbnOptions.translations.warnings.simpleRepeat + warning = options.translations.warnings.simpleRepeat } return { warning, - suggestions: [zxcvbnOptions.translations.suggestions.repeated], + suggestions: [options.translations.suggestions.repeated], } } diff --git a/packages/libraries/main/src/matcher/repeat/matching.ts b/packages/libraries/main/src/matcher/repeat/matching.ts index 91e47fe0..9cf39b3c 100644 --- a/packages/libraries/main/src/matcher/repeat/matching.ts +++ b/packages/libraries/main/src/matcher/repeat/matching.ts @@ -1,19 +1,22 @@ -import { RepeatMatch } from '../../types' -import scoring from '../../scoring' +import { MatchOptions, RepeatMatch } from '../../types' +import Scoring from '../../scoring' import Matching from '../../Matching' +import Options from '../../Options' -interface RepeatMatchOptions { - password: string - omniMatch: Matching -} /* *------------------------------------------------------------------------------- * repeats (aaa, abcabcabc) ------------------------------ *------------------------------------------------------------------------------- */ class MatchRepeat { + private scoring: Scoring + + constructor(private options: Options) { + this.scoring = new Scoring(options) + } + // eslint-disable-next-line max-statements - match({ password, omniMatch }: RepeatMatchOptions) { + match({ password, omniMatch }: MatchOptions) { const matches: (RepeatMatch | Promise)[] = [] let lastIndex = 0 while (lastIndex < password.length) { @@ -123,14 +126,17 @@ class MatchRepeat { const matches = omniMatch.match(baseToken) if (matches instanceof Promise) { return matches.then((resolvedMatches) => { - const baseAnalysis = scoring.mostGuessableMatchSequence( + const baseAnalysis = this.scoring.mostGuessableMatchSequence( baseToken, resolvedMatches, ) return baseAnalysis.guesses }) } - const baseAnalysis = scoring.mostGuessableMatchSequence(baseToken, matches) + const baseAnalysis = this.scoring.mostGuessableMatchSequence( + baseToken, + matches, + ) return baseAnalysis.guesses } } diff --git a/packages/libraries/main/src/matcher/separator/matching.ts b/packages/libraries/main/src/matcher/separator/matching.ts index bfdf78b8..7d75cbfc 100644 --- a/packages/libraries/main/src/matcher/separator/matching.ts +++ b/packages/libraries/main/src/matcher/separator/matching.ts @@ -1,9 +1,8 @@ import { SEPERATOR_CHARS } from '../../data/const' -import { SeparatorMatch } from '../../types' +import { MatchOptions, SeparatorMatch } from '../../types' +import Options from '../../Options' -interface SeparatorMatchOptions { - password: string -} +type SeparatorMatchOptions = Pick const separatorRegex = new RegExp(`[${SEPERATOR_CHARS.join('')}]`) @@ -13,6 +12,8 @@ const separatorRegex = new RegExp(`[${SEPERATOR_CHARS.join('')}]`) *------------------------------------------------------------------------------- */ class MatchSeparator { + constructor(private options: Options) {} + static getMostUsedSeparatorChar(password: string): string | undefined { const mostUsedSeperators = [ ...password diff --git a/packages/libraries/main/src/matcher/sequence/feedback.ts b/packages/libraries/main/src/matcher/sequence/feedback.ts index d4098320..e437b60c 100644 --- a/packages/libraries/main/src/matcher/sequence/feedback.ts +++ b/packages/libraries/main/src/matcher/sequence/feedback.ts @@ -1,8 +1,8 @@ -import { zxcvbnOptions } from '../../Options' +import Options from '../../Options' -export default () => { +export default (options: Options) => { return { - warning: zxcvbnOptions.translations.warnings.sequences, - suggestions: [zxcvbnOptions.translations.suggestions.sequences], + warning: options.translations.warnings.sequences, + suggestions: [options.translations.suggestions.sequences], } } diff --git a/packages/libraries/main/src/matcher/sequence/matching.ts b/packages/libraries/main/src/matcher/sequence/matching.ts index eac3e1ae..f5fa7c0b 100644 --- a/packages/libraries/main/src/matcher/sequence/matching.ts +++ b/packages/libraries/main/src/matcher/sequence/matching.ts @@ -1,5 +1,6 @@ import { ALL_UPPER, ALL_LOWER, ALL_DIGIT } from '../../data/const' -import { SequenceMatch } from '../../types' +import { MatchOptions, SequenceMatch } from '../../types' +import Options from '../../Options' type UpdateParams = { i: number @@ -9,9 +10,7 @@ type UpdateParams = { result: any[] } -interface SequenceMatchOptions { - password: string -} +type SequenceMatchOptions = Pick /* *------------------------------------------------------------------------------- * sequences (abcdef) ------------------------------ @@ -20,6 +19,8 @@ interface SequenceMatchOptions { class MatchSequence { MAX_DELTA = 5 + constructor(private options: Options) {} + // eslint-disable-next-line max-statements match({ password }: SequenceMatchOptions) { /* diff --git a/packages/libraries/main/src/matcher/spatial/feedback.ts b/packages/libraries/main/src/matcher/spatial/feedback.ts index 1d1181c0..f5ef7388 100644 --- a/packages/libraries/main/src/matcher/spatial/feedback.ts +++ b/packages/libraries/main/src/matcher/spatial/feedback.ts @@ -1,13 +1,13 @@ -import { zxcvbnOptions } from '../../Options' +import Options from '../../Options' import { MatchEstimated } from '../../types' -export default (match: MatchEstimated) => { - let warning = zxcvbnOptions.translations.warnings.keyPattern +export default (options: Options, match: MatchEstimated) => { + let warning = options.translations.warnings.keyPattern if (match.turns === 1) { - warning = zxcvbnOptions.translations.warnings.straightRow + warning = options.translations.warnings.straightRow } return { warning, - suggestions: [zxcvbnOptions.translations.suggestions.longerKeyboardPattern], + suggestions: [options.translations.suggestions.longerKeyboardPattern], } } diff --git a/packages/libraries/main/src/matcher/spatial/matching.ts b/packages/libraries/main/src/matcher/spatial/matching.ts index 596e5f3e..a1c0afa1 100644 --- a/packages/libraries/main/src/matcher/spatial/matching.ts +++ b/packages/libraries/main/src/matcher/spatial/matching.ts @@ -1,10 +1,8 @@ -import { sorted, extend } from '../../helper' -import { zxcvbnOptions } from '../../Options' -import { LooseObject, SpatialMatch } from '../../types' +import { sorted, extend } from '../../utils/helper' +import Options from '../../Options' +import { LooseObject, MatchOptions, SpatialMatch } from '../../types' -interface SpatialMatchOptions { - password: string -} +type SpatialMatchOptions = Pick /* * ------------------------------------------------------------------------------ * spatial match (qwerty/dvorak/keypad and so on) ----------------------------------------- @@ -13,10 +11,12 @@ interface SpatialMatchOptions { class MatchSpatial { SHIFTED_RX = /[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]/ + constructor(private options: Options) {} + match({ password }: SpatialMatchOptions) { const matches: SpatialMatch[] = [] - Object.keys(zxcvbnOptions.graphs).forEach((graphName) => { - const graph = zxcvbnOptions.graphs[graphName] + Object.keys(this.options.graphs).forEach((graphName) => { + const graph = this.options.graphs[graphName] extend(matches, this.helper(password, graph, graphName)) }) return sorted(matches) diff --git a/packages/libraries/main/src/matcher/spatial/scoring.ts b/packages/libraries/main/src/matcher/spatial/scoring.ts index c4f78699..fe5849e0 100644 --- a/packages/libraries/main/src/matcher/spatial/scoring.ts +++ b/packages/libraries/main/src/matcher/spatial/scoring.ts @@ -1,10 +1,14 @@ import utils from '../../scoring/utils' -import { zxcvbnOptions } from '../../Options' -import { LooseObject, MatchEstimated, MatchExtended } from '../../types' +import Options from '../../Options' +import { + LooseObject, + MatchEstimated, + MatchExtended, + OptionsGraphEntry, +} from '../../types' interface EstimatePossiblePatternsOptions { token: string - graph: string turns: number } @@ -18,13 +22,12 @@ const calcAverageDegree = (graph: LooseObject) => { return average } -const estimatePossiblePatterns = ({ - token, - graph, - turns, -}: EstimatePossiblePatternsOptions) => { - const startingPosition = Object.keys(zxcvbnOptions.graphs[graph]).length - const averageDegree = calcAverageDegree(zxcvbnOptions.graphs[graph]) +const estimatePossiblePatterns = ( + graphEntry: OptionsGraphEntry, + { token, turns }: EstimatePossiblePatternsOptions, +) => { + const startingPosition = Object.keys(graphEntry).length + const averageDegree = calcAverageDegree(graphEntry) let guesses = 0 const tokenLength = token.length @@ -38,13 +41,14 @@ const estimatePossiblePatterns = ({ return guesses } -export default ({ - graph, - token, - shiftedCount, - turns, -}: MatchExtended | MatchEstimated) => { - let guesses = estimatePossiblePatterns({ token, graph, turns }) +export default ( + { graph, token, shiftedCount, turns }: MatchExtended | MatchEstimated, + options: Options, +) => { + let guesses = estimatePossiblePatterns(options.graphs[graph], { + token, + 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. diff --git a/packages/libraries/main/src/scoring/estimate.ts b/packages/libraries/main/src/scoring/estimate.ts index 0ed9dffe..28c84f0f 100644 --- a/packages/libraries/main/src/scoring/estimate.ts +++ b/packages/libraries/main/src/scoring/estimate.ts @@ -3,7 +3,7 @@ import { MIN_SUBMATCH_GUESSES_MULTI_CHAR, } from '../data/const' import utils from './utils' -import { zxcvbnOptions } from '../Options' +import Options from '../Options' import { DefaultScoringFunction, LooseObject, @@ -49,15 +49,16 @@ const matchers: Matchers = { separator: separatorMatcher, } -const getScoring = (name: string, match: MatchExtended | MatchEstimated) => { +const getScoring = ( + options: Options, + name: string, + match: MatchExtended | MatchEstimated, +) => { if (matchers[name]) { - return matchers[name](match) + return matchers[name](match, options) } - if ( - zxcvbnOptions.matchers[name] && - 'scoring' in zxcvbnOptions.matchers[name] - ) { - return zxcvbnOptions.matchers[name].scoring(match) + if (options.matchers[name] && 'scoring' in options.matchers[name]) { + return options.matchers[name].scoring(match, options) } return 0 } @@ -66,7 +67,11 @@ const getScoring = (name: string, match: MatchExtended | MatchEstimated) => { // guess estimation -- one function per match pattern --------------------------- // ------------------------------------------------------------------------------ // eslint-disable-next-line complexity, max-statements -export default (match: MatchExtended | MatchEstimated, password: string) => { +export default ( + options: Options, + match: MatchExtended | MatchEstimated, + password: string, +) => { const extraData: LooseObject = {} // a match's guess estimate doesn't change. cache it. if ('guesses' in match && match.guesses != null) { @@ -75,7 +80,7 @@ export default (match: MatchExtended | MatchEstimated, password: string) => { const minGuesses = getMinGuesses(match, password) - const estimationResult = getScoring(match.pattern, match) + const estimationResult = getScoring(options, match.pattern, match) let guesses = 0 if (typeof estimationResult === 'number') { guesses = estimationResult diff --git a/packages/libraries/main/src/scoring/index.ts b/packages/libraries/main/src/scoring/index.ts index 6c759caf..3185d8a0 100644 --- a/packages/libraries/main/src/scoring/index.ts +++ b/packages/libraries/main/src/scoring/index.ts @@ -7,6 +7,7 @@ import { MatchEstimated, LooseObject, } from '../types' +import Options from '../Options' const scoringHelper = { password: '', @@ -37,9 +38,9 @@ const scoringHelper = { // 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: MatchExtended, sequenceLength: number) { + update(options: Options, match: MatchExtended, sequenceLength: number) { const k = match.j - const estimatedMatch = estimateGuesses(match, this.password) + const estimatedMatch = estimateGuesses(options, match, this.password) let pi = estimatedMatch.guesses as number if (sequenceLength > 1) { // we're considering a length-sequenceLength sequence ending with match m: @@ -75,10 +76,10 @@ const scoringHelper = { }, // helper: evaluate bruteforce matches ending at passwordCharIndex. - bruteforceUpdate(passwordCharIndex: number) { + bruteforceUpdate(options: Options, passwordCharIndex: number) { // see if a single bruteforce match spanning the passwordCharIndex-prefix is optimal. let match = this.makeBruteforceMatch(0, passwordCharIndex) - this.update(match, 1) + this.update(options, 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). @@ -95,7 +96,7 @@ const scoringHelper = { // --> safe to skip those cases. if (lastMatch.pattern !== 'bruteforce') { // try adding m to this length-sequenceLength sequence. - this.update(match, parseInt(sequenceLength, 10) + 1) + this.update(options, match, parseInt(sequenceLength, 10) + 1) } }) } @@ -131,7 +132,9 @@ const scoringHelper = { }, } -export default { +export default class Scoring { + constructor(private options: Options) {} + // ------------------------------------------------------------------------------ // search --- most guessable match sequence ------------------------------------- // ------------------------------------------------------------------------------ @@ -206,14 +209,18 @@ export default { if (match.i > 0) { Object.keys(scoringHelper.optimal.m[match.i - 1]).forEach( (sequenceLength) => { - scoringHelper.update(match, parseInt(sequenceLength, 10) + 1) + scoringHelper.update( + this.options, + match, + parseInt(sequenceLength, 10) + 1, + ) }, ) } else { - scoringHelper.update(match, 1) + scoringHelper.update(this.options, match, 1) } }) - scoringHelper.bruteforceUpdate(k) + scoringHelper.bruteforceUpdate(this.options, k) } const optimalMatchSequence = scoringHelper.unwind(passwordLength) const optimalSequenceLength = optimalMatchSequence.length @@ -224,7 +231,7 @@ export default { guessesLog10: utils.log10(guesses), sequence: optimalMatchSequence, } - }, + } getGuesses(password: string, optimalSequenceLength: number) { const passwordLength = password.length @@ -236,5 +243,5 @@ export default { scoringHelper.optimal.g[passwordLength - 1][optimalSequenceLength] } return guesses - }, + } } diff --git a/packages/libraries/main/src/types.ts b/packages/libraries/main/src/types.ts index 045647d2..c7ece43f 100644 --- a/packages/libraries/main/src/types.ts +++ b/packages/libraries/main/src/types.ts @@ -4,6 +4,7 @@ import { REGEXEN } from './data/const' import { DictionaryReturn } from './matcher/dictionary/scoring' import Matching from './Matching' import { PasswordChanges } from './matcher/dictionary/variants/matching/unmunger/getCleanPasswords' +import Options from './Options' export type TranslationKeys = typeof translationKeys export type L33tTableDefault = typeof l33tTableDefault @@ -220,26 +221,34 @@ export interface RankedDictionaries { } export type DefaultFeedbackFunction = ( + options: Options, match: MatchEstimated, isSoleMatch?: boolean, ) => FeedbackType | null export type DefaultScoringFunction = ( match: MatchExtended | MatchEstimated, + options: Options, ) => number | DictionaryReturn +export interface UserInputsOptions { + rankedDictionary: RankedDictionary + rankedDictionaryMaxWordSize: number +} export interface MatchOptions { password: string /** * @description This is the original Matcher so that one can use other matchers to define a baseGuess. An usage example is the repeat matcher */ omniMatch: Matching + userInputsOptions?: UserInputsOptions } -export type MatchingType = new () => { +export type MatchingType = new (options: Options) => { match({ password, omniMatch, + userInputsOptions, }: MatchOptions): MatchExtended[] | Promise } diff --git a/packages/libraries/main/src/debounce.ts b/packages/libraries/main/src/utils/debounce.ts similarity index 100% rename from packages/libraries/main/src/debounce.ts rename to packages/libraries/main/src/utils/debounce.ts diff --git a/packages/libraries/main/src/helper.ts b/packages/libraries/main/src/utils/helper.ts similarity index 95% rename from packages/libraries/main/src/helper.ts rename to packages/libraries/main/src/utils/helper.ts index 3d283d3c..c0d9f1c7 100644 --- a/packages/libraries/main/src/helper.ts +++ b/packages/libraries/main/src/utils/helper.ts @@ -1,4 +1,4 @@ -import { LooseObject, MatchExtended } from './types' +import { LooseObject, MatchExtended } from '../types' export const empty = (obj: LooseObject) => Object.keys(obj).length === 0 diff --git a/packages/libraries/main/src/levenshtein.ts b/packages/libraries/main/src/utils/levenshtein.ts similarity index 97% rename from packages/libraries/main/src/levenshtein.ts rename to packages/libraries/main/src/utils/levenshtein.ts index 2c83a206..a921ce6f 100644 --- a/packages/libraries/main/src/levenshtein.ts +++ b/packages/libraries/main/src/utils/levenshtein.ts @@ -1,5 +1,5 @@ import { distance } from 'fastest-levenshtein' -import { LooseObject } from './types' +import { LooseObject } from '../types' const getUsedThreshold = ( password: string, diff --git a/packages/libraries/main/src/utils/mergeUserInputDictionary.ts b/packages/libraries/main/src/utils/mergeUserInputDictionary.ts new file mode 100644 index 00000000..7cff38e6 --- /dev/null +++ b/packages/libraries/main/src/utils/mergeUserInputDictionary.ts @@ -0,0 +1,35 @@ +import { RankedDictionaries, UserInputsOptions } from '../types' + +export default ( + optionsRankedDictionaries: RankedDictionaries, + optionsRankedDictionariesMaxWordSize: Record, + userInputsOptions?: UserInputsOptions, +) => { + const rankedDictionaries = { + ...optionsRankedDictionaries, + } + const rankedDictionariesMaxWordSize = { + ...optionsRankedDictionariesMaxWordSize, + } + if (!userInputsOptions) { + return { + rankedDictionaries, + rankedDictionariesMaxWordSize, + } + } + + rankedDictionaries.userInputs = { + ...(rankedDictionaries.userInputs || {}), + ...userInputsOptions.rankedDictionary, + } + + rankedDictionariesMaxWordSize.userInputs = Math.max( + userInputsOptions.rankedDictionaryMaxWordSize, + rankedDictionariesMaxWordSize.userInputs || 0, + ) + + return { + rankedDictionaries, + rankedDictionariesMaxWordSize, + } +} diff --git a/packages/libraries/main/test/asyncMatcher.spec.ts b/packages/libraries/main/test/asyncMatcher.spec.ts index a27addcf..3868af2d 100644 --- a/packages/libraries/main/test/asyncMatcher.spec.ts +++ b/packages/libraries/main/test/asyncMatcher.spec.ts @@ -1,17 +1,8 @@ import * as zxcvbnCommonPackage from '../../../languages/common/src' import * as zxcvbnEnPackage from '../../../languages/en/src' -import { zxcvbn, zxcvbnAsync, zxcvbnOptions } from '../src' +import { ZxcvbnFactory } from '../src' import { Matcher, MatchExtended } from '../src/types' -zxcvbnOptions.setOptions({ - dictionary: { - ...zxcvbnCommonPackage.dictionary, - ...zxcvbnEnPackage.dictionary, - }, - graphs: zxcvbnCommonPackage.adjacencyGraphs, - translations: zxcvbnEnPackage.translations, -}) - const asyncMatcher: Matcher = { Matching: class MatchAsync { match({ password }: { password: string }): Promise { @@ -41,11 +32,22 @@ const asyncMatcher: Matcher = { }, } -zxcvbnOptions.addMatcher('minLength', asyncMatcher) - describe('asyncMatcher', () => { + const zxcvbn = new ZxcvbnFactory( + { + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations, + }, + { + minLength: asyncMatcher, + }, + ) it('should use async matcher as a promise', async () => { - const promiseResult = zxcvbnAsync('ep8fkw8ds') + const promiseResult = zxcvbn.checkAsync('ep8fkw8ds') expect(promiseResult instanceof Promise).toBe(true) const result = await promiseResult @@ -54,7 +56,7 @@ describe('asyncMatcher', () => { it('should throw an error for wrong function usage', async () => { expect(() => { - zxcvbn('ep8fkw8ds') + zxcvbn.check('ep8fkw8ds') }).toThrow( 'You are using a Promised matcher, please use `zxcvbnAsync` for it.', ) diff --git a/packages/libraries/main/test/customMatcher.spec.ts b/packages/libraries/main/test/customMatcher.spec.ts index 51b266c7..183f9d68 100644 --- a/packages/libraries/main/test/customMatcher.spec.ts +++ b/packages/libraries/main/test/customMatcher.spec.ts @@ -1,20 +1,14 @@ import * as zxcvbnCommonPackage from '../../../languages/common/src' import * as zxcvbnEnPackage from '../../../languages/en/src' -import { zxcvbn, zxcvbnOptions } from '../src' +import { ZxcvbnFactory } from '../src' import { Match, Matcher } from '../src/types' -import { sorted } from '../src/helper' - -zxcvbnOptions.setOptions({ - dictionary: { - ...zxcvbnCommonPackage.dictionary, - ...zxcvbnEnPackage.dictionary, - }, - graphs: zxcvbnCommonPackage.adjacencyGraphs, - translations: zxcvbnEnPackage.translations, -}) +import { sorted } from '../src/utils/helper' +import Options from '../src/Options' const minLengthMatcher: Matcher = { Matching: class MatchMinLength { + constructor(private options: Options) {} + minLength = 10 match({ password }: { password: string }) { @@ -40,12 +34,22 @@ const minLengthMatcher: Matcher = { return match.token.length * 10 }, } - -zxcvbnOptions.addMatcher('minLength', minLengthMatcher) - describe('customMatcher', () => { + const zxcvbn = new ZxcvbnFactory( + { + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations, + }, + { + minLength: minLengthMatcher, + }, + ) it('should use minLength custom matcher', () => { - const result = zxcvbn('ep8fkw8ds') + const result = zxcvbn.check('ep8fkw8ds') expect(result.calcTime).toBeDefined() result.calcTime = 0 expect(result).toEqual({ diff --git a/packages/libraries/main/test/feedback.spec.ts b/packages/libraries/main/test/feedback.spec.ts index 4f1e033a..00067e6f 100644 --- a/packages/libraries/main/test/feedback.spec.ts +++ b/packages/libraries/main/test/feedback.spec.ts @@ -1,14 +1,13 @@ import translations from '../../../languages/en/src/translations' -import { zxcvbnOptions } from '../src/Options' +import Options from '../src/Options' import Feedback from '../src/Feedback' -zxcvbnOptions.setOptions({ +const zxcvbnOptions = new Options({ translations, }) - describe('feedback', () => { describe('with default translations', () => { - const feedbackClass = new Feedback() + const feedbackClass = new Feedback(zxcvbnOptions) it('should return no feedback for a good password', () => { // @ts-ignore diff --git a/packages/libraries/main/test/main.spec.ts b/packages/libraries/main/test/main.spec.ts index 2f5801e1..8a7202b6 100644 --- a/packages/libraries/main/test/main.spec.ts +++ b/packages/libraries/main/test/main.spec.ts @@ -1,11 +1,12 @@ import * as zxcvbnCommonPackage from '../../../languages/common/src' import * as zxcvbnEnPackage from '../../../languages/en/src' -import { zxcvbn, zxcvbnOptions } from '../src' +import { ZxcvbnFactory } from '../src' import passwordTests from './helper/passwordTests' describe('main', () => { + let zxcvbn: ZxcvbnFactory beforeEach(() => { - zxcvbnOptions.setOptions({ + zxcvbn = new ZxcvbnFactory({ dictionary: { ...zxcvbnCommonPackage.dictionary, ...zxcvbnEnPackage.dictionary, @@ -16,7 +17,7 @@ describe('main', () => { }) it('should check without userInputs', () => { - const result = zxcvbn('test') + const result = zxcvbn.check('test') expect(result.calcTime).toBeDefined() result.calcTime = 0 expect(result).toEqual({ @@ -63,11 +64,17 @@ describe('main', () => { }) it('should check with userInputs', () => { - zxcvbnOptions.setOptions({ - // @ts-ignore - dictionary: { userInputs: ['test', 12, true, []] }, + const zxcvbnCustom = new ZxcvbnFactory({ + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + // @ts-ignore + userInputs: ['test', 12, true, []], + }, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations, }) - const result = zxcvbn('test') + const result = zxcvbnCustom.check('test') result.calcTime = 0 expect(result).toEqual({ crackTimesDisplay: { @@ -113,7 +120,7 @@ describe('main', () => { }) it('should check with userInputs on the fly', () => { - const result = zxcvbn('onTheFly', ['onTheFly']) + const result = zxcvbn.check('onTheFly', ['onTheFly']) result.calcTime = 0 expect(result).toEqual({ calcTime: 0, @@ -160,21 +167,21 @@ describe('main', () => { describe('attack vectors', () => { it('should not die while processing and have a appropriate calcTime for l33t attack', () => { - const result = zxcvbn( + const result = zxcvbn.check( '4@8({[ { - const result = zxcvbn( + const result = zxcvbn.check( '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!', ) expect(result.calcTime).toBeLessThan(2000) }) it('should not die while processing and have a appropriate calcTime for regex attacks', () => { - const result = zxcvbn(`\x00\x00${'\x00'.repeat(100)}\n`) + const result = zxcvbn.check(`\x00\x00${'\x00'.repeat(100)}\n`) expect(result.calcTime).toBeLessThan(2000) }) }) @@ -182,13 +189,15 @@ describe('main', () => { describe('password tests', () => { passwordTests.forEach((data) => { it(`should resolve ${data.password}`, () => { - zxcvbnOptions.setOptions({ + const zxcvbnCustom = new ZxcvbnFactory({ dictionary: { ...zxcvbnCommonPackage.dictionary, ...zxcvbnEnPackage.dictionary, }, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations, }) - const result = zxcvbn(data.password) + const result = zxcvbnCustom.check(data.password) result.calcTime = 0 expect(JSON.stringify(result)).toEqual(JSON.stringify(data)) }) diff --git a/packages/libraries/main/test/matcher/date/matching.spec.ts b/packages/libraries/main/test/matcher/date/matching.spec.ts index f8e8330c..82061250 100644 --- a/packages/libraries/main/test/matcher/date/matching.spec.ts +++ b/packages/libraries/main/test/matcher/date/matching.spec.ts @@ -1,9 +1,11 @@ import MatchDate from '../../../src/matcher/date/matching' import checkMatches from '../../helper/checkMatches' import genpws from '../../helper/genpws' +import Options from '../../../src/Options' describe('date matching', () => { - const matchDate = new MatchDate() + const zxcvbnOptions = new Options() + const matchDate = new MatchDate(zxcvbnOptions) let password let matches let msg diff --git a/packages/libraries/main/test/matcher/dictionary/matching.spec.ts b/packages/libraries/main/test/matcher/dictionary/matching.spec.ts index 116eb81d..8cab1772 100644 --- a/packages/libraries/main/test/matcher/dictionary/matching.spec.ts +++ b/packages/libraries/main/test/matcher/dictionary/matching.spec.ts @@ -3,19 +3,18 @@ import * as zxcvbnEnPackage from '../../../../../languages/en/src' import MatchDictionary from '../../../src/matcher/dictionary/matching' import checkMatches from '../../helper/checkMatches' import genpws from '../../helper/genpws' -import { zxcvbnOptions } from '../../../src/Options' - -zxcvbnOptions.setOptions({ - dictionary: { - ...zxcvbnCommonPackage.dictionary, - ...zxcvbnEnPackage.dictionary, - }, - translations: zxcvbnEnPackage.translations, -}) +import Options from '../../../src/Options' describe('dictionary matching', () => { describe('Default dictionary', () => { - const matchDictionary = new MatchDictionary() + const zxcvbnOptions = new Options({ + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + translations: zxcvbnEnPackage.translations, + }) + const matchDictionary = new MatchDictionary(zxcvbnOptions) const matches = matchDictionary .match({ password: 'we' }) // @ts-ignore @@ -41,10 +40,11 @@ describe('dictionary matching', () => { d1: ['motherboard', 'mother', 'board', 'abcd', 'cdef'], d2: ['z', '8', '99', '$', 'asdf1234&*'], } - zxcvbnOptions.setOptions({ + const zxcvbnOptions = new Options({ dictionary: testDicts, + translations: zxcvbnEnPackage.translations, }) - const matchDictionary = new MatchDictionary() + const matchDictionary = new MatchDictionary(zxcvbnOptions) const dm = (pw: string) => matchDictionary .match({ password: pw }) @@ -155,12 +155,13 @@ describe('dictionary matching', () => { }) describe('with user input', () => { - zxcvbnOptions.setOptions({ + const zxcvbnOptions = new Options({ dictionary: { userInputs: ['foo', 'bar'], }, + translations: zxcvbnEnPackage.translations, }) - const matchDictionary = new MatchDictionary() + const matchDictionary = new MatchDictionary(zxcvbnOptions) const matches = matchDictionary .match({ password: 'foobar' }) // @ts-ignore diff --git a/packages/libraries/main/test/matcher/dictionary/variant/matching/dictionaryReverse.spec.ts b/packages/libraries/main/test/matcher/dictionary/variant/matching/dictionaryReverse.spec.ts index 240178e0..6af8d6dc 100644 --- a/packages/libraries/main/test/matcher/dictionary/variant/matching/dictionaryReverse.spec.ts +++ b/packages/libraries/main/test/matcher/dictionary/variant/matching/dictionaryReverse.spec.ts @@ -1,19 +1,18 @@ import MatchDictionaryReverse from '../../../../../src/matcher/dictionary/variants/matching/reverse' import MatchDictionary from '../../../../../src/matcher/dictionary/matching' import checkMatches from '../../../../helper/checkMatches' -import { zxcvbnOptions } from '../../../../../src/Options' - -zxcvbnOptions.setOptions() -const dictionaryMatcher = new MatchDictionary() +import Options from '../../../../../src/Options' describe('dictionary reverse matching', () => { const testDicts = { d1: [123, 321, 456, 654], } - zxcvbnOptions.setOptions({ + const zxcvbnOptions = new Options({ dictionary: testDicts, }) + const dictionaryMatcher = new MatchDictionary(zxcvbnOptions) const matchDictionaryReverse = new MatchDictionaryReverse( + zxcvbnOptions, dictionaryMatcher.defaultMatch, ) const password = '0123456789' diff --git a/packages/libraries/main/test/matcher/dictionary/variant/matching/l33t.spec.ts b/packages/libraries/main/test/matcher/dictionary/variant/matching/l33t.spec.ts index 15e5576f..56166fe4 100644 --- a/packages/libraries/main/test/matcher/dictionary/variant/matching/l33t.spec.ts +++ b/packages/libraries/main/test/matcher/dictionary/variant/matching/l33t.spec.ts @@ -1,11 +1,8 @@ import MatchL33t from '../../../../../src/matcher/dictionary/variants/matching/l33t' import MatchDictionary from '../../../../../src/matcher/dictionary/matching' import checkMatches from '../../../../helper/checkMatches' -import { zxcvbnOptions } from '../../../../../src/Options' -import { sorted } from '../../../../../src/helper' - -zxcvbnOptions.setOptions() -const dictionaryMatcher = new MatchDictionary() +import Options from '../../../../../src/Options' +import { sorted } from '../../../../../src/utils/helper' describe('l33t matching', () => { let msg @@ -24,18 +21,24 @@ describe('l33t matching', () => { } describe('default const', () => { - const matchL33t = new MatchL33t(dictionaryMatcher.defaultMatch) + const zxcvbnOptions = new Options() + const dictionaryMatcher = new MatchDictionary(zxcvbnOptions) + const matchL33t = new MatchL33t( + zxcvbnOptions, + dictionaryMatcher.defaultMatch, + ) it("doesn't match single-character l33ted words", () => { expect(matchL33t.match({ password: '4 1 @' })).toEqual([]) }) }) - zxcvbnOptions.setOptions({ + const zxcvbnOptions = new Options({ dictionary: dicts, l33tTable: testTable, l33tMaxSubstitutions: 15, }) - const matchL33t = new MatchL33t(dictionaryMatcher.defaultMatch) + const dictionaryMatcher = new MatchDictionary(zxcvbnOptions) + const matchL33t = new MatchL33t(zxcvbnOptions, dictionaryMatcher.defaultMatch) describe('main match', () => { it("doesn't match ''", () => { diff --git a/packages/libraries/main/test/matcher/dictionary/variant/scoring/l33t.spec.ts b/packages/libraries/main/test/matcher/dictionary/variant/scoring/l33t.spec.ts index d3c449ca..23069e4f 100644 --- a/packages/libraries/main/test/matcher/dictionary/variant/scoring/l33t.spec.ts +++ b/packages/libraries/main/test/matcher/dictionary/variant/scoring/l33t.spec.ts @@ -1,6 +1,6 @@ import l33t from '../../../../../src/matcher/dictionary/variants/scoring/l33t' import utils from '../../../../../src/scoring/utils' -import { empty } from '../../../../../src/helper' +import { empty } from '../../../../../src/utils/helper' import { LooseObject } from '../../../../../src/types' const { nCk } = utils diff --git a/packages/libraries/main/test/matcher/regex/matching.spec.ts b/packages/libraries/main/test/matcher/regex/matching.spec.ts index 6e9fb5a3..897d3a67 100644 --- a/packages/libraries/main/test/matcher/regex/matching.spec.ts +++ b/packages/libraries/main/test/matcher/regex/matching.spec.ts @@ -1,5 +1,6 @@ import MatchRegex from '../../../src/matcher/regex/matching' import checkMatches from '../../helper/checkMatches' +import Options from '../../../src/Options' describe('regex matching', () => { const data = [ @@ -15,7 +16,8 @@ describe('regex matching', () => { }, ] - const matchRegex = new MatchRegex() + const zxcvbnOptions = new Options() + const matchRegex = new MatchRegex(zxcvbnOptions) data.forEach(({ pattern, regexNames, ijs }) => { const matches = matchRegex.match({ password: pattern }) const msg = `matches ${pattern} as a ${regexNames[0]} pattern` diff --git a/packages/libraries/main/test/matcher/repeat/matching.spec.ts b/packages/libraries/main/test/matcher/repeat/matching.spec.ts index 09acb44e..0b63e2ac 100644 --- a/packages/libraries/main/test/matcher/repeat/matching.spec.ts +++ b/packages/libraries/main/test/matcher/repeat/matching.spec.ts @@ -2,13 +2,13 @@ import MatchRepeat from '../../../src/matcher/repeat/matching' import checkMatches from '../../helper/checkMatches' import genpws from '../../helper/genpws' import MatchOmni from '../../../src/Matching' -import { zxcvbnOptions } from '../../../src/Options' +import Options from '../../../src/Options' import { RepeatMatch } from '../../../src/types' -zxcvbnOptions.setOptions() -const omniMatch = new MatchOmni() +const zxcvbnOptions = new Options() +const omniMatch = new MatchOmni(zxcvbnOptions) describe('repeat matching', () => { - const matchRepeat = new MatchRepeat() + const matchRepeat = new MatchRepeat(zxcvbnOptions) it("doesn't match length repeat patterns", () => { const data = ['', '#'] diff --git a/packages/libraries/main/test/matcher/repeat/scoring.spec.ts b/packages/libraries/main/test/matcher/repeat/scoring.spec.ts index fc2e9287..a14aee4f 100644 --- a/packages/libraries/main/test/matcher/repeat/scoring.spec.ts +++ b/packages/libraries/main/test/matcher/repeat/scoring.spec.ts @@ -1,13 +1,13 @@ import repeatGuesses from '../../../src/matcher/repeat/scoring' -import scoring from '../../../src/scoring' +import Scoring from '../../../src/scoring' import MatchOmni from '../../../src/Matching' -import { zxcvbnOptions } from '../../../src/Options' +import Options from '../../../src/Options' import { MatchExtended } from '../../../src/types' -zxcvbnOptions.setOptions() - -const omniMatch = new MatchOmni() +const zxcvbnOptions = new Options() +const omniMatch = new MatchOmni(zxcvbnOptions) describe('scoring guesses repeated', () => { + const scoring = new Scoring(zxcvbnOptions) const data: [string, string, number][] = [ ['aa', 'a', 2], ['999', '9', 3], diff --git a/packages/libraries/main/test/matcher/separator/matching.spec.ts b/packages/libraries/main/test/matcher/separator/matching.spec.ts index a8c7deea..e364a151 100644 --- a/packages/libraries/main/test/matcher/separator/matching.spec.ts +++ b/packages/libraries/main/test/matcher/separator/matching.spec.ts @@ -1,8 +1,10 @@ import MatchSeparator from '../../../src/matcher/separator/matching' import checkMatches from '../../helper/checkMatches' +import Options from '../../../src/Options' describe('separator matching', () => { - const matchSeparator = new MatchSeparator() + const zxcvbnOptions = new Options() + const matchSeparator = new MatchSeparator(zxcvbnOptions) it("doesn't match length separators", () => { const data = [''] diff --git a/packages/libraries/main/test/matcher/sequence/matching.spec.ts b/packages/libraries/main/test/matcher/sequence/matching.spec.ts index 070eb3f4..6d12544e 100644 --- a/packages/libraries/main/test/matcher/sequence/matching.spec.ts +++ b/packages/libraries/main/test/matcher/sequence/matching.spec.ts @@ -1,9 +1,11 @@ import MatchSequence from '../../../src/matcher/sequence/matching' import checkMatches from '../../helper/checkMatches' import genpws from '../../helper/genpws' +import Options from '../../../src/Options' describe('sequence matching', () => { - const matchSequence = new MatchSequence() + const zxcvbnOptions = new Options() + const matchSequence = new MatchSequence(zxcvbnOptions) it("doesn't match length sequences", () => { const data = ['', 'a', '1'] diff --git a/packages/libraries/main/test/matcher/spatial/matching.spec.ts b/packages/libraries/main/test/matcher/spatial/matching.spec.ts index 0c3ac58d..bf60172f 100644 --- a/packages/libraries/main/test/matcher/spatial/matching.spec.ts +++ b/packages/libraries/main/test/matcher/spatial/matching.spec.ts @@ -1,14 +1,15 @@ import MatchSpatial from '../../../src/matcher/spatial/matching' import checkMatches from '../../helper/checkMatches' import * as zxcvbnCommonPackage from '../../../../../languages/common/src' -import { zxcvbnOptions } from '../../../src/Options' +import Options from '../../../src/Options' import { LooseObject } from '../../../src/types' const { adjacencyGraphs } = zxcvbnCommonPackage describe('spatial matching', () => { it("doesn't match 1- and 2-character spatial patterns", () => { - const matchSpatial = new MatchSpatial() + const zxcvbnOptions = new Options() + const matchSpatial = new MatchSpatial(zxcvbnOptions) const data = ['', '/', 'qw', '*/'] data.forEach((password) => { expect(matchSpatial.match({ password })).toEqual([]) @@ -17,10 +18,8 @@ describe('spatial matching', () => { const graphs: LooseObject = { qwerty: adjacencyGraphs.qwerty, } - zxcvbnOptions.setOptions({ - graphs, - }) - const matchSpatial = new MatchSpatial() + const zxcvbnOptions = new Options({ graphs }) + const matchSpatial = new MatchSpatial(zxcvbnOptions) const pattern = '6tfGHJ' const matches = matchSpatial.match({ password: `rz!${pattern}%z` }) const msg = @@ -60,10 +59,8 @@ describe('spatial matching specific patterns vs keyboards', () => { data.forEach(([pattern, keyboard, turns, shifts]) => { const graphs: any = {} graphs[keyboard] = adjacencyGraphs[keyboard as keyof typeof adjacencyGraphs] - zxcvbnOptions.setOptions({ - graphs, - }) - const matchSpatial = new MatchSpatial() + const zxcvbnOptions = new Options({ graphs }) + const matchSpatial = new MatchSpatial(zxcvbnOptions) const matches = matchSpatial.match({ password: pattern }) const msg = `matches '${pattern}' as a ${keyboard} pattern` diff --git a/packages/libraries/main/test/matcher/spatial/scoring.spec.ts b/packages/libraries/main/test/matcher/spatial/scoring.spec.ts index a999fca7..df8f061f 100644 --- a/packages/libraries/main/test/matcher/spatial/scoring.spec.ts +++ b/packages/libraries/main/test/matcher/spatial/scoring.spec.ts @@ -1,12 +1,11 @@ import * as zxcvbnCommonPackage from '../../../../../languages/common/src' import spatialGuesses from '../../../src/matcher/spatial/scoring' -import { zxcvbnOptions } from '../../../src/Options' - -zxcvbnOptions.setOptions({ - graphs: zxcvbnCommonPackage.adjacencyGraphs, -}) +import Options from '../../../src/Options' describe('scoring: guesses spatial', () => { + const zxcvbnOptions = new Options({ + graphs: zxcvbnCommonPackage.adjacencyGraphs, + }) it('with no turns or shifts, guesses is starts * degree * (len-1)', () => { const match = { token: 'zxcvbn', @@ -16,7 +15,7 @@ describe('scoring: guesses spatial', () => { } // @ts-ignore - expect(spatialGuesses(match)).toEqual(2160) + expect(spatialGuesses(match, zxcvbnOptions)).toEqual(2160) }) it('guesses is added for shifted keys, similar to capitals in dictionary matching', () => { @@ -29,7 +28,7 @@ describe('scoring: guesses spatial', () => { } // @ts-ignore - expect(spatialGuesses(match)).toEqual(45360) + expect(spatialGuesses(match, zxcvbnOptions)).toEqual(45360) }) it('when everything is shifted, guesses are doubled', () => { @@ -41,7 +40,7 @@ describe('scoring: guesses spatial', () => { guesses: null, } // @ts-ignore - expect(spatialGuesses(match)).toEqual(4320) + expect(spatialGuesses(match, zxcvbnOptions)).toEqual(4320) }) it('spatial guesses accounts for turn positions, directions and starting keys', () => { @@ -53,6 +52,6 @@ describe('scoring: guesses spatial', () => { } // @ts-ignore - expect(spatialGuesses(match)).toEqual(558461) + expect(spatialGuesses(match, zxcvbnOptions)).toEqual(558461) }) }) diff --git a/packages/libraries/main/test/matching.spec.ts b/packages/libraries/main/test/matching.spec.ts index d72205a1..a24c4523 100644 --- a/packages/libraries/main/test/matching.spec.ts +++ b/packages/libraries/main/test/matching.spec.ts @@ -1,10 +1,10 @@ import * as zxcvbnCommonPackage from '../../../languages/common/src' import * as zxcvbnEnPackage from '../../../languages/en/src' import MatchOmni from '../src/Matching' -import { zxcvbnOptions } from '../src/Options' +import Options from '../src/Options' import { MatchExtended } from '../src/types' -zxcvbnOptions.setOptions({ +const zxcvbnOptions = new Options({ dictionary: { ...zxcvbnCommonPackage.dictionary, ...zxcvbnEnPackage.dictionary, @@ -13,7 +13,7 @@ zxcvbnOptions.setOptions({ }) describe('omnimatch matching', () => { - const omniMatch = new MatchOmni() + const omniMatch = new MatchOmni(zxcvbnOptions) it("doesn't match ''", () => { expect(omniMatch.match('')).toEqual([]) diff --git a/packages/libraries/main/test/options.spec.ts b/packages/libraries/main/test/options.spec.ts index ff93bc74..22c193b0 100644 --- a/packages/libraries/main/test/options.spec.ts +++ b/packages/libraries/main/test/options.spec.ts @@ -1,10 +1,10 @@ -import { zxcvbnOptions } from '../src/Options' +import Options from '../src/Options' import translationKeys from '../src/data/translationKeys' describe('Options', () => { describe('translations', () => { it('should return default feedback for no sequence on custom translations', () => { - zxcvbnOptions.setOptions({ translations: translationKeys }) + const zxcvbnOptions = new Options({ translations: translationKeys }) expect(zxcvbnOptions.translations).toEqual(translationKeys) }) const customTranslations = { @@ -17,7 +17,8 @@ describe('Options', () => { it('should return error for wrong custom translations', () => { expect(() => { // @ts-ignore - zxcvbnOptions.setOptions({ translations: customTranslations }) + // eslint-disable-next-line no-new + new Options({ translations: customTranslations }) }).toThrow('Invalid translations object fallback to keys') }) }) diff --git a/packages/libraries/main/test/scoring/guesses/calc.spec.ts b/packages/libraries/main/test/scoring/guesses/calc.spec.ts index 33da19ec..f82c7578 100644 --- a/packages/libraries/main/test/scoring/guesses/calc.spec.ts +++ b/packages/libraries/main/test/scoring/guesses/calc.spec.ts @@ -1,13 +1,15 @@ import estimate from '../../../src/scoring/estimate' import dateGuesses from '../../../src/matcher/date/scoring' +import Options from '../../../src/Options' describe('scoring', () => { + const zxcvbnOptions = new Options() it('estimate_guesses returns cached guesses when available', () => { const match = { guesses: 1, } // @ts-ignore - expect(estimate(match, '')).toEqual({ + expect(estimate(zxcvbnOptions, match, '')).toEqual({ guesses: 1, }) }) @@ -22,7 +24,7 @@ describe('scoring', () => { day: 14, } // @ts-ignore - expect(estimate(match, '1977')).toEqual({ + expect(estimate(zxcvbnOptions, match, '1977')).toEqual({ pattern: 'date', token: usedYear.toString(), year: usedYear, diff --git a/packages/libraries/main/test/scoring/search.spec.ts b/packages/libraries/main/test/scoring/search.spec.ts index fc2509f0..adcfbbc8 100644 --- a/packages/libraries/main/test/scoring/search.spec.ts +++ b/packages/libraries/main/test/scoring/search.spec.ts @@ -1,9 +1,9 @@ -import scoring from '../../src/scoring' -import { zxcvbnOptions } from '../../src/Options' - -zxcvbnOptions.setOptions() +import Scoring from '../../src/scoring' +import Options from '../../src/Options' describe('scoring search', () => { + const zxcvbnOptions = new Options() + const scoring = new Scoring(zxcvbnOptions) const getMatch = (i: number, j: number, guesses: number) => ({ i, j, diff --git a/packages/libraries/main/test/timeEstimates.spec.ts b/packages/libraries/main/test/timeEstimates.spec.ts index 623c705a..6605102e 100644 --- a/packages/libraries/main/test/timeEstimates.spec.ts +++ b/packages/libraries/main/test/timeEstimates.spec.ts @@ -1,14 +1,14 @@ import translations from '../../../languages/en/src/translations' import TimeEstimates from '../src/TimeEstimates' -import { zxcvbnOptions } from '../src/Options' +import Options from '../src/Options' -zxcvbnOptions.setOptions({ +const zxcvbnOptions = new Options({ translations, }) // TODO add tests describe('timeEstimates', () => { - const timeEstimates = new TimeEstimates() + const timeEstimates = new TimeEstimates(zxcvbnOptions) it('should be very weak', () => { const attackTimes = timeEstimates.estimateAttackTimes(10) diff --git a/packages/libraries/main/test/debounce.spec.ts b/packages/libraries/main/test/utils/debounce.spec.ts similarity index 91% rename from packages/libraries/main/test/debounce.spec.ts rename to packages/libraries/main/test/utils/debounce.spec.ts index f82b16ad..0123a7e5 100644 --- a/packages/libraries/main/test/debounce.spec.ts +++ b/packages/libraries/main/test/utils/debounce.spec.ts @@ -1,4 +1,4 @@ -import debounce from '../src/debounce' +import debounce from '../../src/utils/debounce' describe('debounce', () => { it('should call handler immediately', async () => { diff --git a/packages/libraries/main/test/helper.spec.ts b/packages/libraries/main/test/utils/helper.spec.ts similarity index 97% rename from packages/libraries/main/test/helper.spec.ts rename to packages/libraries/main/test/utils/helper.spec.ts index 52906f9c..9228d383 100644 --- a/packages/libraries/main/test/helper.spec.ts +++ b/packages/libraries/main/test/utils/helper.spec.ts @@ -5,8 +5,8 @@ import { translate, mod, buildRankedDictionary, -} from '../src/helper' -import { LooseObject } from '../src/types' +} from '../../src/utils/helper' +import { LooseObject } from '../../src/types' describe('utils matching', () => { describe('empty', () => { diff --git a/packages/libraries/main/test/levenshtein.spec.ts b/packages/libraries/main/test/utils/levenshtein.spec.ts similarity index 78% rename from packages/libraries/main/test/levenshtein.spec.ts rename to packages/libraries/main/test/utils/levenshtein.spec.ts index ac5b2e80..550c2bb1 100644 --- a/packages/libraries/main/test/levenshtein.spec.ts +++ b/packages/libraries/main/test/utils/levenshtein.spec.ts @@ -1,9 +1,9 @@ -import * as zxcvbnCommonPackage from '../../../languages/common/src' -import * as zxcvbnEnPackage from '../../../languages/en/src' -import { zxcvbn, zxcvbnOptions } from '../src' +import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common/src' +import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en/src' +import { ZxcvbnFactory } from '../../src' describe('levenshtein', () => { - zxcvbnOptions.setOptions({ + const zxcvbn = new ZxcvbnFactory({ dictionary: { ...zxcvbnCommonPackage.dictionary, ...zxcvbnEnPackage.dictionary, @@ -14,7 +14,9 @@ describe('levenshtein', () => { }) it('should find levensteindistance', () => { - const result = zxcvbn('ishduehlduod83h4mfs8', ['ishduehgldueod83h4mfis8']) + const result = zxcvbn.check('ishduehlduod83h4mfs8', [ + 'ishduehgldueod83h4mfis8', + ]) expect(result.calcTime).toBeDefined() result.calcTime = 0 expect(result).toEqual({ @@ -63,7 +65,7 @@ describe('levenshtein', () => { }) it('should recognize a mistyped common English word', () => { - const result = zxcvbn('alaphant') + const result = zxcvbn.check('alaphant') expect(result.calcTime).toBeDefined() result.calcTime = 0 expect( @@ -117,10 +119,17 @@ describe('levenshtein', () => { }) it('should respect threshold which is lower than the default 2', () => { - zxcvbnOptions.setOptions({ + const customZxcvbn = new ZxcvbnFactory({ + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations, + useLevenshteinDistance: true, levenshteinThreshold: 1, }) - const result = zxcvbn('eeleephaant') + const result = customZxcvbn.check('eeleephaant') expect( result.sequence.find( (sequenceItem) => sequenceItem.levenshteinDistance !== undefined, @@ -129,10 +138,17 @@ describe('levenshtein', () => { }) it('should respect threshold which is higher than the default 2', () => { - zxcvbnOptions.setOptions({ + const customZxcvbn = new ZxcvbnFactory({ + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations, + useLevenshteinDistance: true, levenshteinThreshold: 3, }) - const result = zxcvbn('eeleephaant') + const result = customZxcvbn.check('eeleephaant') expect(result.sequence.length).toStrictEqual(1) expect(result.sequence[0].levenshteinDistance).toBeDefined() }) diff --git a/packages/libraries/pwned/README.md b/packages/libraries/pwned/README.md index b9b721f8..f80e54b7 100644 --- a/packages/libraries/pwned/README.md +++ b/packages/libraries/pwned/README.md @@ -16,16 +16,19 @@ The pwned matcher is an async matcher that will make a k-anonymity password requ ## Setup ```js -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core' +import { ZxcvbnFactory } from '@zxcvbn-ts/core' import { matcherPwnedFactory } from '@zxcvbn-ts/matcher-pwned' -const password = 'somePassword' +const matcherPwned = matcherPwnedFactory(fetch) +const customMatcher = { + pwned: matcherPwned +} -const matcherPwned = matcherPwnedFactory(fetch, zxcvbnOptions) -zxcvbnOptions.addMatcher('pwned', matcherPwned) +const zxcvbn = new ZxcvbnFactory(options, customMatcher) +const password = 'somePassword' // @zxcvbn-ts/matcher-pwned is async so zxcvbn will return a promise -zxcvbn(password).then((result) => { +zxcvbn.checkAsync(password).then((result) => { }) ``` diff --git a/packages/libraries/pwned/src/feedback.ts b/packages/libraries/pwned/src/feedback.ts index 513341d5..62b3fcce 100644 --- a/packages/libraries/pwned/src/feedback.ts +++ b/packages/libraries/pwned/src/feedback.ts @@ -1,11 +1,11 @@ // @ts-ignore -import { Options } from '@zxcvbn-ts/core' +import { DefaultFeedbackFunction } from '@zxcvbn-ts/core' -export default (options: Options) => { - return () => { - return { - warning: options.translations.warnings.pwned, - suggestions: [options.translations.suggestions.pwned], - } +const pwnedFeedback: DefaultFeedbackFunction = (options) => { + return { + warning: options.translations.warnings.pwned, + suggestions: [options.translations.suggestions.pwned], } } + +export default pwnedFeedback diff --git a/packages/libraries/pwned/src/index.ts b/packages/libraries/pwned/src/index.ts index c94fc238..767deb80 100644 --- a/packages/libraries/pwned/src/index.ts +++ b/packages/libraries/pwned/src/index.ts @@ -1,5 +1,5 @@ // @ts-ignore -import { Matcher, Options } from '@zxcvbn-ts/core' +import { Matcher } from '@zxcvbn-ts/core' import MatchPwned from './matching' import scoring from './scoring' import FeedbackFactory from './feedback' @@ -8,12 +8,11 @@ import { FetchApi, MatcherPwnedFactoryConfig } from './types' export const matcherPwnedFactory = ( universalFetch: FetchApi, - options: Options, config: MatcherPwnedFactoryConfig = {}, ): Matcher => { return { Matching: MatchPwned(universalFetch, config), - feedback: FeedbackFactory(options), + feedback: FeedbackFactory, scoring, } } diff --git a/packages/libraries/pwned/test/index.spec.ts b/packages/libraries/pwned/test/index.spec.ts index 72bc0294..cb77c330 100644 --- a/packages/libraries/pwned/test/index.spec.ts +++ b/packages/libraries/pwned/test/index.spec.ts @@ -1,4 +1,4 @@ -import { zxcvbnAsync, zxcvbnOptions } from '../../main/src' +import { ZxcvbnFactory } from '../../main/src' import { matcherPwnedFactory } from '../src' describe('main', () => { @@ -8,9 +8,14 @@ describe('main', () => { return `008A205652858375D71117A63004CC75167:5\r\n3EA386688A0147AB736AABCEDE496610382:244` }, })) - // @ts-ignore - zxcvbnOptions.matchers.pwned = matcherPwnedFactory(fetch, zxcvbnOptions) - const result = await zxcvbnAsync('P4$$w0rd') + const zxcvbn = new ZxcvbnFactory( + {}, + { + // @ts-ignore + pwned: matcherPwnedFactory(fetch), + }, + ) + const result = await zxcvbn.checkAsync('P4$$w0rd') expect(result.calcTime).toBeDefined() result.calcTime = 0 @@ -54,9 +59,14 @@ describe('main', () => { const fetch = jest.fn(async () => { throw new Error('Some Network error') }) - zxcvbnOptions.matchers.pwned = matcherPwnedFactory(fetch, zxcvbnOptions) - const result = await zxcvbnAsync('P4$$w0rd') - + const zxcvbn = new ZxcvbnFactory( + {}, + { + // @ts-ignore + pwned: matcherPwnedFactory(fetch), + }, + ) + const result = await zxcvbn.checkAsync('P4$$w0rd') expect(result.calcTime).toBeDefined() result.calcTime = 0 expect(result).toEqual({ diff --git a/packages/libraries/pwned/test/matching.spec.ts b/packages/libraries/pwned/test/matching.spec.ts index a46b0cc5..b07e1801 100644 --- a/packages/libraries/pwned/test/matching.spec.ts +++ b/packages/libraries/pwned/test/matching.spec.ts @@ -1,5 +1,5 @@ -import { zxcvbnOptions } from '@zxcvbn-ts/core/src' import { matcherPwnedFactory } from '../src' +import Options from '../../main/src/Options' const fetch = jest.fn(async () => ({ text() { @@ -8,10 +8,12 @@ const fetch = jest.fn(async () => ({ })) describe('pwned matching', () => { + const options = new Options() // @ts-ignore - const matcherPwned = matcherPwnedFactory(fetch, zxcvbnOptions) + const matcherPwned = matcherPwnedFactory(fetch) it('should return a match', async () => { - const matchPwned = new matcherPwned.Matching() + // @ts-ignore + const matchPwned = new matcherPwned.Matching(options) // @ts-ignore const match = await matchPwned.match({ password: 'P4$$w0rd' }) expect(match).toEqual([ diff --git a/scripts/cli-password-tester.ts b/scripts/cli-password-tester.ts index a2c9858a..f1580e08 100644 --- a/scripts/cli-password-tester.ts +++ b/scripts/cli-password-tester.ts @@ -1,9 +1,9 @@ // eslint-disable-next-line import/no-relative-packages import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common/src/index' import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en/src/index' -import { zxcvbnAsync, zxcvbnOptions } from '@zxcvbn-ts/core/src/index' +import { ZxcvbnFactory } from '@zxcvbn-ts/core/src/index' -// eslint-disable-next-line prettier/prettier +// eslint-disable-next-line (async () => { const options = { dictionary: { @@ -14,8 +14,8 @@ import { zxcvbnAsync, zxcvbnOptions } from '@zxcvbn-ts/core/src/index' graphs: zxcvbnCommonPackage.adjacencyGraphs, useLevenshteinDistance: true, } - zxcvbnOptions.setOptions(options) - return zxcvbnAsync(process.argv[2], process.argv[3]?.split(';')) + const zxcvbn = new ZxcvbnFactory(options) + return zxcvbn.checkAsync(process.argv[2], process.argv[3]?.split(';')) })() .then((match) => { // eslint-disable-next-line no-console