├── .editorconfig ├── .gitattributes ├── .gitignore ├── GitVersion.yml ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── zxcvbn-core-list-builder ├── GlobalSuppressions.cs ├── ListBuilder.cs ├── Program.cs ├── RankDictionaryName.cs └── zxcvbn-core-list-builder.csproj ├── zxcvbn-core-test ├── Core │ └── CoreTests.cs ├── GlobalSuppressions.cs ├── Matcher │ ├── DateMatcherTests.cs │ ├── DictionaryMatcherTests.cs │ ├── L33tMatcherTests.cs │ ├── RegexMatcherTests.cs │ ├── RepeatMatcherTests.cs │ ├── ReverseDictionaryMatcherTests.cs │ ├── SequenceMatcherTests.cs │ └── SpatialMatcherTests.cs ├── Scoring │ ├── BruteForceScoringTests.cs │ ├── DateScoringTests.cs │ ├── DictionaryScoringTests.cs │ ├── RegexScoringTests.cs │ ├── RepeatScoringTests.cs │ ├── ScoringTests.cs │ ├── SequenceScoringTests.cs │ └── SpatialTests.cs ├── test_dictionary_1.txt ├── test_dictionary_2.txt ├── test_dictionary_3.txt ├── zxcvbn-core-test.csproj └── zxcvbn-strong-name-key.snk ├── zxcvbn-core ├── AttackTimes.cs ├── Core.cs ├── CrackTimes.cs ├── CrackTimesDisplay.cs ├── Data │ ├── english.lst │ ├── female_names.lst │ ├── male_names.lst │ ├── passwords.lst │ ├── surnames.lst │ └── us_tv_and_film.lst ├── DefaultMatcherFactory.cs ├── Dictionaries │ ├── english.lst │ ├── female_names.lst │ ├── male_names.lst │ ├── passwords.lst │ ├── surnames.lst │ └── us_tv_and_film.lst ├── Feedback.cs ├── FeedbackItem.cs ├── GlobalSuppressions.cs ├── Matcher │ ├── DateMatcher.cs │ ├── DictionaryMatcher.cs │ ├── IMatcher.cs │ ├── L33tMatcher.cs │ ├── LooseDate.cs │ ├── Matches │ │ ├── BruteForceMatch.cs │ │ ├── DateMatch.cs │ │ ├── DictionaryMatch.cs │ │ ├── Match.cs │ │ ├── RegexMatch.cs │ │ ├── RepeatMatch.cs │ │ ├── SequenceMatch.cs │ │ └── SpatialMatch.cs │ ├── Point.cs │ ├── RegexMatcher.cs │ ├── RepeatMatcher.cs │ ├── ReverseDictionaryMatcher.cs │ ├── SequenceMatcher.cs │ ├── SpatialGraph.cs │ └── SpatialMatcher.cs ├── MostGuessableMatchResult.cs ├── OptimalValues.cs ├── PasswordScoring.cs ├── Properties │ └── AssemblyInfo.cs ├── Result.cs ├── Scoring │ ├── BruteForceGuessesCalculator.cs │ ├── DateGuessesCalculator.cs │ ├── DictionaryGuessesCalculator.cs │ ├── RegexGuessesCalculator.cs │ ├── RepeatGuessesCalculator.cs │ ├── SequenceGuessesCalculator.cs │ └── SpatialGuessesCalculator.cs ├── TimeEstimates.cs ├── Utility.cs ├── zxcvbn-core.csproj ├── zxcvbn-core.xml └── zxcvbn-strong-name-key.snk └── zxcvbn-cs.sln /.editorconfig: -------------------------------------------------------------------------------- 1 | # Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\Users\trich\Source\Repos\trichards57\zxcvbn-cs codebase based on best match to current usage at 21/01/2022 2 | # You can modify the rules from these initially generated values to suit your own policies 3 | # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 4 | [*.cs] 5 | 6 | #Core editorconfig formatting - indentation 7 | 8 | #use soft tabs (spaces) for indentation 9 | indent_style = space 10 | 11 | #Formatting - indentation options 12 | 13 | #indent switch case contents. 14 | csharp_indent_case_contents = true 15 | #indent switch labels 16 | csharp_indent_switch_labels = true 17 | 18 | #Formatting - new line options 19 | 20 | #place else statements on a new line 21 | csharp_new_line_before_else = true 22 | #require members of object intializers to be on separate lines 23 | csharp_new_line_before_members_in_object_initializers = true 24 | #require braces to be on a new line for object_collection_array_initializers, methods, control_blocks, types, and lambdas (also known as "Allman" style) 25 | csharp_new_line_before_open_brace = object_collection_array_initializers, methods, control_blocks, types, lambdas 26 | 27 | #Formatting - organize using options 28 | 29 | #sort System.* using directives alphabetically, and place them before other usings 30 | dotnet_sort_system_directives_first = true 31 | 32 | #Formatting - spacing options 33 | 34 | #require NO space between a cast and the value 35 | csharp_space_after_cast = false 36 | #require a space before the colon for bases or interfaces in a type declaration 37 | csharp_space_after_colon_in_inheritance_clause = true 38 | #require a space after a keyword in a control flow statement such as a for loop 39 | csharp_space_after_keywords_in_control_flow_statements = true 40 | #require a space before the colon for bases or interfaces in a type declaration 41 | csharp_space_before_colon_in_inheritance_clause = true 42 | #remove space within empty argument list parentheses 43 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 44 | #remove space between method call name and opening parenthesis 45 | csharp_space_between_method_call_name_and_opening_parenthesis = false 46 | #do not place space characters after the opening parenthesis and before the closing parenthesis of a method call 47 | csharp_space_between_method_call_parameter_list_parentheses = false 48 | #remove space within empty parameter list parentheses for a method declaration 49 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 50 | #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. 51 | csharp_space_between_method_declaration_parameter_list_parentheses = false 52 | 53 | #Formatting - wrapping options 54 | 55 | #leave code block on single line 56 | csharp_preserve_single_line_blocks = true 57 | #leave statements and member declarations on the same line 58 | csharp_preserve_single_line_statements = true 59 | 60 | #Style - Code block preferences 61 | 62 | #prefer no curly braces if allowed 63 | csharp_prefer_braces = false:suggestion 64 | 65 | #Style - expression bodied member options 66 | 67 | #prefer block bodies for constructors 68 | csharp_style_expression_bodied_constructors = false:suggestion 69 | #prefer block bodies for methods 70 | csharp_style_expression_bodied_methods = false:suggestion 71 | 72 | #Style - expression level options 73 | 74 | #prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them 75 | dotnet_style_predefined_type_for_member_access = true:suggestion 76 | 77 | #Style - Expression-level preferences 78 | 79 | #prefer objects to be initialized using object initializers when possible 80 | dotnet_style_object_initializer = true:suggestion 81 | 82 | #Style - implicit and explicit types 83 | 84 | #prefer var over explicit type in all cases, unless overridden by another code style rule 85 | csharp_style_var_elsewhere = true:suggestion 86 | #prefer var is used to declare variables with built-in system types such as int 87 | csharp_style_var_for_built_in_types = true:suggestion 88 | #prefer var when the type is already mentioned on the right-hand side of a declaration expression 89 | csharp_style_var_when_type_is_apparent = true:suggestion 90 | 91 | #Style - language keyword and framework type options 92 | 93 | #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them 94 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 95 | 96 | #Style - Miscellaneous preferences 97 | 98 | #prefer local functions over anonymous functions 99 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 100 | 101 | #Style - modifier options 102 | 103 | #prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. 104 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 105 | 106 | #Style - Modifier preferences 107 | 108 | #when this rule is set to a list of modifiers, prefer the specified ordering. 109 | csharp_preferred_modifier_order = public,private,internal,static,readonly,virtual:suggestion 110 | 111 | #Style - qualification options 112 | 113 | #prefer fields not to be prefaced with this. or Me. in Visual Basic 114 | dotnet_style_qualification_for_field = false:suggestion 115 | #prefer methods not to be prefaced with this. or Me. in Visual Basic 116 | dotnet_style_qualification_for_method = false:suggestion 117 | #prefer properties not to be prefaced with this. or Me. in Visual Basic 118 | dotnet_style_qualification_for_property = false:suggestion 119 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # Visual Studio Trace Files 107 | *.e2e 108 | 109 | # TFS 2012 Local Workspace 110 | $tf/ 111 | 112 | # Guidance Automation Toolkit 113 | *.gpState 114 | 115 | # ReSharper is a .NET coding add-in 116 | _ReSharper*/ 117 | *.[Rr]e[Ss]harper 118 | *.DotSettings.user 119 | 120 | # JustCode is a .NET coding add-in 121 | .JustCode 122 | 123 | # TeamCity is a build add-in 124 | _TeamCity* 125 | 126 | # DotCover is a Code Coverage Tool 127 | *.dotCover 128 | 129 | # AxoCover is a Code Coverage Tool 130 | .axoCover/* 131 | !.axoCover/settings.json 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # Note: Comment the next line if you want to checkin your web deploy settings, 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/[Pp]ackages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/[Pp]ackages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/[Pp]ackages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | *.appx 204 | 205 | # Visual Studio cache files 206 | # files ending in .cache can be ignored 207 | *.[Cc]ache 208 | # but keep track of directories ending in .cache 209 | !*.[Cc]ache/ 210 | 211 | # Others 212 | ClientBin/ 213 | ~$* 214 | *~ 215 | *.dbmdl 216 | *.dbproj.schemaview 217 | *.jfm 218 | *.pfx 219 | *.publishsettings 220 | orleans.codegen.cs 221 | 222 | # Including strong name files can present a security risk 223 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 224 | #*.snk 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | ServiceFabricBackup/ 241 | 242 | # SQL Server files 243 | *.mdf 244 | *.ldf 245 | *.ndf 246 | 247 | # Business Intelligence projects 248 | *.rdl.data 249 | *.bim.layout 250 | *.bim_*.settings 251 | 252 | # Microsoft Fakes 253 | FakesAssemblies/ 254 | 255 | # GhostDoc plugin setting file 256 | *.GhostDoc.xml 257 | 258 | # Node.js Tools for Visual Studio 259 | .ntvs_analysis.dat 260 | node_modules/ 261 | 262 | # TypeScript v1 declaration files 263 | typings/ 264 | 265 | # Visual Studio 6 build log 266 | *.plg 267 | 268 | # Visual Studio 6 workspace options file 269 | *.opt 270 | 271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 272 | *.vbw 273 | 274 | # Visual Studio LightSwitch build output 275 | **/*.HTMLClient/GeneratedArtifacts 276 | **/*.DesktopClient/GeneratedArtifacts 277 | **/*.DesktopClient/ModelManifest.xml 278 | **/*.Server/GeneratedArtifacts 279 | **/*.Server/ModelManifest.xml 280 | _Pvt_Extensions 281 | 282 | # Paket dependency manager 283 | .paket/paket.exe 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | /reports 326 | /tools 327 | /nuget.exe 328 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | next-version: 8.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Dropbox, Inc. 2 | Copyright (c) 2020 Tony Richards 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Zxcvbn C#/.NET 2 | ============== 3 | 4 | [![Build status](https://ci.appveyor.com/api/projects/status/90iaonivnh97yfda?svg=true)](https://ci.appveyor.com/project/trichards57/zxcvbn-cs-62692) 5 | [![Coverage Status](https://coveralls.io/repos/github/trichards57/zxcvbn-cs/badge.svg)](https://coveralls.io/github/trichards57/zxcvbn-cs) 6 | [![NuGet](https://img.shields.io/nuget/v/zxcvbn-core.svg)](https://www.nuget.org/packages/zxcvbn-core) 7 | 8 | This is a port of the `Zxcvbn` JavaScript password strength estimation library at 9 | https://github.com/lowe/zxcvbn to .NET, written in C#. This fork moves the library 10 | to support .Net Core. 11 | 12 | From the `Zxcvbn` readme: 13 | 14 | > `zxcvbn`, named after a crappy password, is a JavaScript password strength 15 | > estimation library. Use it to implement a custom strength bar on a 16 | > signup form near you! 17 | > 18 | > `zxcvbn` attempts to give sound password advice through pattern matching 19 | > and conservative entropy calculations. It finds 10k common passwords, 20 | > common American names and surnames, common English words, and common 21 | > patterns like dates, repeats (aaa), sequences (abcd), and QWERTY 22 | > patterns. 23 | > 24 | > For full motivation, see: 25 | > 26 | > http://tech.dropbox.com/?p=165 27 | 28 | This port aims to produce comparable results with the Typescript version of `Zxcvbn` which I have also put out and is here https://github.com/trichards57/zxcvbn. 29 | The results structure that is returned can be interpreted in the same way as with JS `Zxcvbn` and this port has been tested with a variety of passwords to ensure 30 | that it return the same score as the JS version (some other details vary a little). 31 | 32 | I have tried to keep the implementation as close as possible, but there is still a chance of some small changes. Let me know if you find any differences 33 | and I can investigate. 34 | 35 | ### Using `Zxcvbn-cs` 36 | 37 | The included Visual Studio project will create a single assembly, Zxcvbn.dll, which is all that is 38 | required to be included in your project. 39 | 40 | To evaluate a password: 41 | 42 | ``` C# 43 | using Zxcvbn; 44 | 45 | //... 46 | 47 | var result = Zxcvbn.Core.EvaluatePassword("p@ssw0rd"); 48 | ``` 49 | 50 | `EvaluatePassword` takes an optional second parameter that contains an enumerable of 51 | user data strings to also match the password against. 52 | 53 | ### Interpreting Results 54 | 55 | The `Result` structure returned from password evaluation is interpreted the same way as with JS `Zxcvbn`. 56 | 57 | - `result.Score`: 0-4 indicating the estimated strength of the password. 58 | 59 | ### Licence 60 | 61 | Since `Zxcvbn-cs` is a port of the original `Zxcvbn` the original copyright and licensing applies. Cf. the LICENSE file. 62 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - feature/* 4 | - release/* 5 | 6 | pool: 7 | vmImage: 'windows-latest' 8 | 9 | variables: 10 | solution: '**/*.sln' 11 | buildPlatform: 'Any CPU' 12 | buildConfiguration: 'Release' 13 | GitVersion.SemVer: '' 14 | 15 | steps: 16 | 17 | - task: gitversion/setup@0 18 | inputs: 19 | versionSpec: '5.x' 20 | 21 | - task: gitversion/execute@0 22 | 23 | - task: DotNetCoreCLI@2 24 | inputs: 25 | command: 'restore' 26 | projects: '**/*.csproj' 27 | 28 | - task: DotNetCoreCLI@2 29 | inputs: 30 | command: 'build' 31 | arguments: '-p:Version=$(GitVersion.SemVer) -c Release' 32 | 33 | - task: DotNetCoreCLI@2 34 | inputs: 35 | command: 'pack' 36 | packagesToPack: '**/*.csproj' 37 | includesymbols: true 38 | includesource: true 39 | versioningScheme: 'byEnvVar' 40 | versionEnvVar: 'GitVersion.NuGetVersion' 41 | nobuild: true 42 | 43 | - task: DotNetCoreCLI@2 44 | inputs: 45 | command: 'test' 46 | nobuild: true 47 | arguments: '-c Release' 48 | 49 | - task: PublishPipelineArtifact@1 50 | inputs: 51 | targetPath: '$(Build.ArtifactStagingDirectory)' 52 | artifact: 'drop' 53 | publishLocation: 'pipeline' 54 | -------------------------------------------------------------------------------- /zxcvbn-core-list-builder/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "To match this project's coding style.")] 9 | [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "To match this project's coding style.")] 10 | [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "To match this project's coding style.")] 11 | [assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "This normalization isn't security critical.")] 12 | [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1208:System using directives should be placed before other using directives", Justification = "Using CodeMaid for this task.")] 13 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "This isn't actually publicly released.")] 14 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "No header required.")] 15 | -------------------------------------------------------------------------------- /zxcvbn-core-list-builder/ListBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Zxcvbn.ListBuilder 8 | { 9 | internal static class ListBuilder 10 | { 11 | /// 12 | /// Returns the maximum number of words for a dictionary. 13 | /// null means take everything. 14 | /// 15 | private static readonly ReadOnlyDictionary DictionaryLimits = new ReadOnlyDictionary(new Dictionary 16 | { 17 | { "us_tv_and_film", 30000 }, 18 | { "english", 30000 }, 19 | { "passwords", 30000 }, 20 | { "surnames", 10000 }, 21 | { "male_names", null }, 22 | { "female_names", null }, 23 | }); 24 | 25 | /// 26 | /// Filters the frequency lists to remove: 27 | /// - Tokens which are short and rare 28 | /// - Tokens that are already in another dictionary and a lower rank 29 | /// - Tokens that end up beyond the end of the limits in . 30 | /// 31 | /// The list to filter. 32 | /// 33 | /// The filtered output. 34 | /// 35 | public static Dictionary> FilterFrequencyLists(Dictionary> frequencyLists) 36 | { 37 | var filteredTokenAndRank = new Dictionary>(); 38 | var tokenCounts = new Dictionary(); 39 | 40 | foreach (var name in frequencyLists.Keys) 41 | { 42 | filteredTokenAndRank[name] = new Dictionary(); 43 | tokenCounts[name] = 0; 44 | } 45 | 46 | var minimumValues = new Dictionary(); 47 | 48 | foreach (var kvp in frequencyLists) 49 | { 50 | var name = kvp.Key; 51 | foreach (var tvp in kvp.Value) 52 | { 53 | var token = tvp.Key; 54 | var rank = tvp.Value; 55 | 56 | if (!minimumValues.ContainsKey(token)) 57 | { 58 | minimumValues[token] = new RankDictionaryName 59 | { 60 | Name = name, 61 | Rank = rank, 62 | }; 63 | } 64 | else 65 | { 66 | if (rank < minimumValues[token].Rank) 67 | { 68 | minimumValues[token] = new RankDictionaryName 69 | { 70 | Rank = rank, 71 | Name = name, 72 | }; 73 | } 74 | } 75 | } 76 | } 77 | 78 | foreach (var kvp in frequencyLists) 79 | { 80 | var name = kvp.Key; 81 | foreach (var tvp in kvp.Value) 82 | { 83 | var token = tvp.Key; 84 | var rank = tvp.Value; 85 | 86 | if (token == "o") 87 | { 88 | Console.Write(string.Empty); 89 | } 90 | 91 | if (minimumValues[token].Name != name) 92 | continue; 93 | if (IsRareAndShort(token, rank) || HasCommaOrDoubleQuote(token)) 94 | continue; 95 | filteredTokenAndRank[name][token] = rank; 96 | tokenCounts[name]++; 97 | } 98 | } 99 | 100 | var result = new Dictionary>(); 101 | 102 | foreach (var kvp in filteredTokenAndRank) 103 | { 104 | var name = kvp.Key; 105 | var values = kvp.Value; 106 | var res = values.OrderBy(s => s.Value).Select(kvp => kvp.Key); 107 | if (DictionaryLimits[name].HasValue) 108 | res = res.Take(DictionaryLimits[name].Value); 109 | result[name] = res.ToList(); 110 | } 111 | 112 | return result; 113 | } 114 | 115 | public static Dictionary> ParseFrequencyLists(string directory) 116 | { 117 | var result = new Dictionary>(); 118 | 119 | foreach (var file in Directory.GetFiles(directory)) 120 | { 121 | var name = Path.GetFileNameWithoutExtension(file); 122 | if (!DictionaryLimits.ContainsKey(name)) 123 | { 124 | Console.WriteLine($"Warning: {name} is in directory {directory} but was not expected. Skipped."); 125 | continue; 126 | } 127 | 128 | var tokenToRank = new Dictionary(); 129 | var rank = 0; 130 | 131 | foreach (var line in File.ReadAllLines(file).Where(l => !string.IsNullOrWhiteSpace(l))) 132 | { 133 | rank++; 134 | var token = line.Split(" ")[0]; 135 | tokenToRank[token] = rank; 136 | } 137 | 138 | result[name] = tokenToRank; 139 | } 140 | 141 | foreach (var expectedKey in DictionaryLimits.Keys) 142 | { 143 | if (!result.ContainsKey(expectedKey)) 144 | { 145 | Console.WriteLine($"Warning: {expectedKey} is not in directory {directory} but was expected."); 146 | } 147 | } 148 | 149 | return result; 150 | } 151 | 152 | private static bool HasCommaOrDoubleQuote(string token) 153 | { 154 | return token.Contains(",") || token.Contains("\""); 155 | } 156 | 157 | private static bool IsRareAndShort(string token, int rank) 158 | { 159 | return rank >= Math.Pow(10, token.Length); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /zxcvbn-core-list-builder/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | namespace Zxcvbn.ListBuilder 6 | { 7 | internal class Program 8 | { 9 | private static void Main(string[] args) 10 | { 11 | if (args.Length != 2) 12 | { 13 | PrintUsage(Assembly.GetExecutingAssembly().GetName().Name); 14 | return; 15 | } 16 | 17 | var dataDir = args[0]; 18 | var outputDir = args[1]; 19 | 20 | var unfilteredLists = ListBuilder.ParseFrequencyLists(dataDir); 21 | var filteredLists = ListBuilder.FilterFrequencyLists(unfilteredLists); 22 | 23 | foreach (var kvp in filteredLists) 24 | { 25 | var name = kvp.Key; 26 | var list = kvp.Value; 27 | 28 | File.WriteAllLines(Path.Combine(outputDir, $"{name}.lst"), list); 29 | } 30 | } 31 | 32 | private static void PrintUsage(string name) 33 | { 34 | Console.WriteLine("Usage:"); 35 | Console.WriteLine($" {name} dataDirectory outputDirectory"); 36 | Console.WriteLine(); 37 | Console.WriteLine("Converts the raw data used by zxcvbn's build_frequency_lists scripts into input files for embedding into the assembly."); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /zxcvbn-core-list-builder/RankDictionaryName.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn.ListBuilder 2 | { 3 | internal struct RankDictionaryName 4 | { 5 | public string Name { get; set; } 6 | 7 | public int Rank { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /zxcvbn-core-list-builder/zxcvbn-core-list-builder.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | Zxcvbn.ListBuilder 7 | false 8 | False 9 | 10 | 11 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Core/CoreTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | 4 | namespace Zxcvbn.Tests.Core 5 | { 6 | public class CoreTests 7 | { 8 | [Fact] 9 | public void GoodScoreShallHaveNoSuggestions() 10 | { 11 | var result = Zxcvbn.Core.EvaluatePassword("turtledogspicemagic"); 12 | 13 | result.Score.Should().BeGreaterOrEqualTo(3); 14 | result.Feedback.Suggestions.Count.Should().Be(0); 15 | } 16 | 17 | [Fact] 18 | public void GoodScoreShallHaveNoWarning() 19 | { 20 | var result = Zxcvbn.Core.EvaluatePassword("turtleturtledogspicemagic"); 21 | result.Score.Should().BeGreaterOrEqualTo(3); 22 | result.Feedback.Warning.Should().Be(string.Empty); 23 | } 24 | 25 | [Fact] 26 | public void EmptyPasswordShallHaveZeroScore() 27 | { 28 | var result = Zxcvbn.Core.EvaluatePassword(string.Empty); 29 | result.Score.Should().Be(0); 30 | } 31 | 32 | [Fact] 33 | public void EmptyPasswordShallYieldDefaultSuggestions() 34 | { 35 | var result = Zxcvbn.Core.EvaluatePassword(string.Empty); 36 | var defaultFeedback = new[] 37 | { 38 | "Use a few words, avoid common phrases", 39 | "No need for symbols, digits, or uppercase letters", 40 | }; 41 | result.Feedback.Suggestions.Should().BeEquivalentTo(defaultFeedback); 42 | } 43 | 44 | [Fact] 45 | public void EnglishNounsShallBeRecognisedAsSuch() 46 | { 47 | var result = Zxcvbn.Core.EvaluatePassword("geologic"); 48 | var warning = "A word by itself is easy to guess"; 49 | result.Feedback.Warning.Should().BeEquivalentTo(warning); 50 | } 51 | 52 | [Fact] 53 | public void SurnamesShallBeRecognisedAsSuch() 54 | { 55 | var result = Zxcvbn.Core.EvaluatePassword("grajeda"); 56 | var warning = "Names and surnames by themselves are easy to guess"; 57 | result.Feedback.Warning.Should().BeEquivalentTo(warning); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /zxcvbn-core-test/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | using Xunit; 8 | 9 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "To match this project's coding style.")] 10 | [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "To match this project's coding style.")] 11 | [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "To match this project's coding style.")] 12 | [assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "This normalization isn't security critical.")] 13 | [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1208:System using directives should be placed before other using directives", Justification = "Using CodeMaid for this task.")] 14 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "This isn't actually publicly released.")] 15 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "No header required.")] 16 | [assembly: CollectionBehavior(DisableTestParallelization = true)] 17 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Matcher/DateMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Linq; 3 | using Xunit; 4 | using Zxcvbn.Matcher; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Tests.Matcher 8 | { 9 | public class DateMatcherTests 10 | { 11 | [Fact] 12 | public void DateMatcherMatchesDatesPaddedByNonAmbiguousDigits() 13 | { 14 | const string password = "912/20/919"; 15 | 16 | var expected = new[] 17 | { 18 | new DateMatch 19 | { 20 | i = 1, 21 | j = 8, 22 | Token = "12/20/91", 23 | Day = 20, 24 | Month = 12, 25 | Year = 1991, 26 | Separator = "/", 27 | }, 28 | }; 29 | 30 | var matcher = new DateMatcher(); 31 | 32 | var matches = matcher.MatchPassword(password).ToList(); 33 | 34 | matches.Should().BeEquivalentTo(expected); 35 | } 36 | 37 | [Theory(DisplayName = "DateMatcher.DifferentSeperator")] 38 | [InlineData("")] 39 | [InlineData(" ")] 40 | [InlineData("-")] 41 | [InlineData("/")] 42 | [InlineData("\\")] 43 | [InlineData("_")] 44 | [InlineData(".")] 45 | public void DateMatcherMatchesDatesWithVariedSeperators(string seperator) 46 | { 47 | var password = $"13{seperator}2{seperator}1921"; 48 | 49 | var expected = new[] 50 | { 51 | new DateMatch 52 | { 53 | i = 0, 54 | j = password.Length - 1, 55 | Token = password, 56 | Day = 13, 57 | Month = 2, 58 | Year = 1921, 59 | Separator = seperator, 60 | }, 61 | }; 62 | 63 | var matcher = new DateMatcher(); 64 | 65 | var matches = matcher.MatchPassword(password).ToList(); 66 | 67 | matches.Should().BeEquivalentTo(expected); 68 | } 69 | 70 | [Theory(DisplayName = "DateMatcher.DifferentOrders")] 71 | [InlineData("mdy")] 72 | [InlineData("dmy")] 73 | [InlineData("ymd")] 74 | [InlineData("ydm")] 75 | public void DateMatcherMatchesDateWithVariedElementOrder(string order) 76 | { 77 | const int day = 22; 78 | const int month = 12; 79 | const int year = 1935; 80 | 81 | var password = order.Replace("d", day.ToString()) 82 | .Replace("m", month.ToString()).Replace("y", year.ToString()); 83 | 84 | var expected = new[] 85 | { 86 | new DateMatch 87 | { 88 | i = 0, 89 | j = password.Length - 1, 90 | Token = password, 91 | Day = day, 92 | Month = month, 93 | Year = year, 94 | Separator = string.Empty, 95 | }, 96 | }; 97 | 98 | var matcher = new DateMatcher(); 99 | 100 | var matches = matcher.MatchPassword(password).ToList(); 101 | 102 | matches.Should().BeEquivalentTo(expected); 103 | } 104 | 105 | [Theory(DisplayName = "DateMatcher.PrefixSuffix")] 106 | [InlineData("a", "")] 107 | [InlineData("ab", "")] 108 | [InlineData("", "!")] 109 | [InlineData("a", "!")] 110 | [InlineData("ab", "!")] 111 | public void DateMatcherMatchesEmbeddedDates(string prefix, string suffix) 112 | { 113 | var pattern = "1/1/91"; 114 | var password = $"{prefix}{pattern}{suffix}"; 115 | 116 | var expected = new[] 117 | { 118 | new DateMatch 119 | { 120 | i = prefix.Length, 121 | j = prefix.Length + pattern.Length - 1, 122 | Token = "1/1/91", 123 | Day = 1, 124 | Month = 1, 125 | Year = 1991, 126 | Separator = "/", 127 | }, 128 | }; 129 | 130 | var matcher = new DateMatcher(); 131 | 132 | var matches = matcher.MatchPassword(password).ToList(); 133 | 134 | matches.Should().BeEquivalentTo(expected); 135 | } 136 | 137 | [Fact] 138 | public void DateMatcherMatchesOverlappingDates() 139 | { 140 | const string password = "12/20/1991.12.20"; 141 | 142 | var expected = new[] 143 | { 144 | new DateMatch 145 | { 146 | i = 0, 147 | j = 9, 148 | Token = "12/20/1991", 149 | Day = 20, 150 | Month = 12, 151 | Year = 1991, 152 | Separator = "/", 153 | }, 154 | new DateMatch 155 | { 156 | i = 6, 157 | j = 15, 158 | Token = "1991.12.20", 159 | Day = 20, 160 | Month = 12, 161 | Year = 1991, 162 | Separator = ".", 163 | }, 164 | }; 165 | 166 | var matcher = new DateMatcher(); 167 | 168 | var matches = matcher.MatchPassword(password).ToList(); 169 | 170 | matches.Should().BeEquivalentTo(expected); 171 | } 172 | 173 | [Fact] 174 | public void DateMatcherMatchesTheDateWithYearClosestToReferenceYear() 175 | { 176 | const string password = "111504"; 177 | 178 | var expected = new[] 179 | { 180 | new DateMatch 181 | { 182 | i = 0, 183 | j = password.Length - 1, 184 | Token = password, 185 | Day = 15, 186 | Month = 11, 187 | Year = 2004, 188 | Separator = string.Empty, 189 | }, 190 | }; 191 | 192 | var matcher = new DateMatcher(); 193 | 194 | var matches = matcher.MatchPassword(password).ToList(); 195 | matches.Should().BeEquivalentTo(expected); 196 | } 197 | 198 | [Theory] 199 | [InlineData(1, 1, 1999)] 200 | [InlineData(11, 8, 2000)] 201 | [InlineData(9, 12, 2005)] 202 | [InlineData(22, 11, 1551)] 203 | public void DateMatcherMatchesVariousDates(int day, int month, int year) 204 | { 205 | var password = $"{year}{month}{day}"; 206 | 207 | var matcher = new DateMatcher(); 208 | 209 | var matches = matcher.MatchPassword(password).ToList(); 210 | 211 | matches.Should().HaveCountGreaterOrEqualTo(1); 212 | matches.Count(m => m is DateMatch).Should().Be(1); 213 | 214 | var match = matches.OfType().Single(); 215 | match.Pattern.Should().Be("date"); 216 | match.Token.Should().Be(password); 217 | match.i.Should().Be(0); 218 | match.j.Should().Be(password.Length - 1); 219 | match.Separator.Should().Be(string.Empty); 220 | match.Year.Should().Be(year); 221 | 222 | var dottedPassword = $"{year}.{month}.{day}"; 223 | 224 | matches = matcher.MatchPassword(dottedPassword).ToList(); 225 | 226 | matches.Should().HaveCountGreaterOrEqualTo(1); 227 | matches.Count(m => m is DateMatch).Should().Be(1); 228 | 229 | match = matches.OfType().Single(); 230 | match.Pattern.Should().Be("date"); 231 | match.Token.Should().Be(dottedPassword); 232 | match.i.Should().Be(0); 233 | match.j.Should().Be(dottedPassword.Length - 1); 234 | match.Separator.Should().Be("."); 235 | match.Year.Should().Be(year); 236 | } 237 | 238 | [Fact] 239 | public void DateMatchesMatchesZeroPaddedDates() 240 | { 241 | const string password = "02/02/02"; 242 | 243 | var expected = new[] 244 | { 245 | new DateMatch 246 | { 247 | i = 0, 248 | j = password.Length - 1, 249 | Token = password, 250 | Day = 2, 251 | Month = 2, 252 | Year = 2002, 253 | Separator = "/", 254 | }, 255 | }; 256 | 257 | var matcher = new DateMatcher(); 258 | 259 | var matches = matcher.MatchPassword(password).ToList(); 260 | 261 | matches.Should().BeEquivalentTo(expected); 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Matcher/DictionaryMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Xunit; 5 | using Zxcvbn.Matcher; 6 | using Zxcvbn.Matcher.Matches; 7 | 8 | namespace Zxcvbn.Tests.Matcher 9 | { 10 | public class DictionaryMatcherTests 11 | { 12 | private readonly DictionaryMatcher matcher1 = new DictionaryMatcher("d1", "test_dictionary_1.txt"); 13 | private readonly DictionaryMatcher matcher2 = new DictionaryMatcher("d2", "test_dictionary_2.txt"); 14 | private readonly DictionaryMatcher matcherTv = new DictionaryMatcher("us_tv_and_film", "us_tv_and_film.lst"); 15 | 16 | [Theory] 17 | [InlineData("q", "%")] 18 | [InlineData("q", "qq")] 19 | [InlineData("%%", "%")] 20 | [InlineData("%%", "qq")] 21 | public void IdentifiesWordsSurroundedByNonWords(string prefix, string suffix) 22 | { 23 | var word = "asdf1234&*"; 24 | var password = $"{prefix}{word}{suffix}"; 25 | var result = RunMatches(password); 26 | 27 | var expected = new[] 28 | { 29 | new DictionaryMatch 30 | { 31 | DictionaryName = "d2", 32 | i = prefix.Length, 33 | j = prefix.Length + word.Length - 1, 34 | MatchedWord = word, 35 | Rank = 5, 36 | Reversed = false, 37 | L33t = false, 38 | Token = word, 39 | }, 40 | }; 41 | result.Should().BeEquivalentTo(expected); 42 | } 43 | 44 | [Fact] 45 | public void IgnoresUppercasing() 46 | { 47 | var result = RunMatches("BoaRdZ"); 48 | 49 | var expected = new[] 50 | { 51 | new DictionaryMatch 52 | { 53 | DictionaryName = "d1", 54 | i = 0, 55 | j = 4, 56 | MatchedWord = "board", 57 | Rank = 3, 58 | Reversed = false, 59 | L33t = false, 60 | Token = "BoaRd", 61 | }, 62 | new DictionaryMatch 63 | { 64 | DictionaryName = "d2", 65 | i = 5, 66 | j = 5, 67 | MatchedWord = "z", 68 | Rank = 1, 69 | Reversed = false, 70 | L33t = false, 71 | Token = "Z", 72 | }, 73 | }; 74 | result.Should().BeEquivalentTo(expected); 75 | } 76 | 77 | [Theory] 78 | [InlineData("mother", 2, "d1")] 79 | [InlineData("board", 3, "d1")] 80 | [InlineData("abcd", 4, "d1")] 81 | [InlineData("cdef", 5, "d1")] 82 | [InlineData("z", 1, "d2")] 83 | [InlineData("8", 2, "d2")] 84 | [InlineData("99", 3, "d2")] 85 | [InlineData("$", 4, "d2")] 86 | [InlineData("asdf1234&*", 5, "d2")] 87 | public void MatchesAgainstAllWordsInProvidedDictionaries(string word, int rank, string dictionary) 88 | { 89 | var result = RunMatches(word); 90 | 91 | var expected = new[] 92 | { 93 | new DictionaryMatch 94 | { 95 | DictionaryName = dictionary, 96 | i = 0, 97 | j = word.Length - 1, 98 | MatchedWord = word, 99 | Rank = rank, 100 | Reversed = false, 101 | L33t = false, 102 | Token = word, 103 | }, 104 | }; 105 | result.Should().BeEquivalentTo(expected); 106 | } 107 | 108 | [Fact] 109 | public void MatchesMultipleOverlappingWords() 110 | { 111 | var result = RunMatches("abcdef"); 112 | 113 | var expected = new[] 114 | { 115 | new DictionaryMatch 116 | { 117 | DictionaryName = "d1", 118 | i = 0, 119 | j = 3, 120 | MatchedWord = "abcd", 121 | Rank = 4, 122 | Reversed = false, 123 | L33t = false, 124 | Token = "abcd", 125 | }, 126 | new DictionaryMatch 127 | { 128 | DictionaryName = "d1", 129 | i = 2, 130 | j = 5, 131 | MatchedWord = "cdef", 132 | Rank = 5, 133 | Reversed = false, 134 | L33t = false, 135 | Token = "cdef", 136 | }, 137 | }; 138 | result.Should().BeEquivalentTo(expected); 139 | } 140 | 141 | [Fact] 142 | public void MatchesWithProvidedUserInputDictionary() 143 | { 144 | var matcher = new DictionaryMatcher("user", new[] { "foo", "bar" }); 145 | var result = matcher.MatchPassword("foobar").OfType().Where(m => m.DictionaryName == "user").ToList(); 146 | 147 | result.Should().HaveCount(2); 148 | 149 | result[0].Token.Should().Be("foo"); 150 | result[0].MatchedWord.Should().Be("foo"); 151 | result[0].Rank.Should().Be(1); 152 | result[0].i.Should().Be(0); 153 | result[0].j.Should().Be(2); 154 | 155 | result[1].Token.Should().Be("bar"); 156 | result[1].MatchedWord.Should().Be("bar"); 157 | result[1].Rank.Should().Be(2); 158 | result[1].i.Should().Be(3); 159 | result[1].j.Should().Be(5); 160 | } 161 | 162 | [Fact] 163 | public void MatchesWordsThatContainOtherWords() 164 | { 165 | var result = RunMatches("motherboard"); 166 | 167 | var expected = new[] 168 | { 169 | new DictionaryMatch 170 | { 171 | DictionaryName = "d1", 172 | i = 0, 173 | j = 5, 174 | MatchedWord = "mother", 175 | Rank = 2, 176 | Reversed = false, 177 | L33t = false, 178 | Token = "mother", 179 | }, 180 | new DictionaryMatch 181 | { 182 | DictionaryName = "d1", 183 | i = 0, 184 | j = 10, 185 | MatchedWord = "motherboard", 186 | Rank = 1, 187 | Reversed = false, 188 | L33t = false, 189 | Token = "motherboard", 190 | }, 191 | new DictionaryMatch 192 | { 193 | DictionaryName = "d1", 194 | i = 6, 195 | j = 10, 196 | MatchedWord = "board", 197 | Rank = 3, 198 | Reversed = false, 199 | L33t = false, 200 | Token = "board", 201 | }, 202 | }; 203 | 204 | result.Should().BeEquivalentTo(expected); 205 | } 206 | 207 | [Fact] 208 | public void UsesTheDefaultDictionaries() 209 | { 210 | var result = matcherTv.MatchPassword("wow").OfType().ToList(); 211 | 212 | var expected = new[] 213 | { 214 | new DictionaryMatch 215 | { 216 | DictionaryName = "us_tv_and_film", 217 | i = 0, 218 | j = 2, 219 | MatchedWord = "wow", 220 | Rank = 322, 221 | Reversed = false, 222 | L33t = false, 223 | Token = "wow", 224 | }, 225 | }; 226 | result.Should().BeEquivalentTo(expected); 227 | } 228 | 229 | [Fact] 230 | public void UsesTheUserInputDictionary() 231 | { 232 | var matcher = new DictionaryMatcher("user_inputs", new[] { "foo", "bar" }); 233 | 234 | var result = matcher.MatchPassword("foobar").OfType().ToList(); 235 | 236 | var expected = new[] 237 | { 238 | new DictionaryMatch 239 | { 240 | DictionaryName = "user_inputs", 241 | i = 0, 242 | j = 2, 243 | MatchedWord = "foo", 244 | Rank = 1, 245 | Reversed = false, 246 | L33t = false, 247 | Token = "foo", 248 | }, 249 | new DictionaryMatch 250 | { 251 | DictionaryName = "user_inputs", 252 | i = 3, 253 | j = 5, 254 | MatchedWord = "bar", 255 | Rank = 2, 256 | Reversed = false, 257 | L33t = false, 258 | Token = "bar", 259 | }, 260 | }; 261 | 262 | result.Should().BeEquivalentTo(expected); 263 | } 264 | 265 | private List RunMatches(string word) 266 | { 267 | var result = matcher1.MatchPassword(word).Concat(matcher2.MatchPassword(word)); 268 | 269 | return result.OfType().ToList(); 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Matcher/L33tMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using Xunit; 6 | using Zxcvbn.Matcher; 7 | using Zxcvbn.Matcher.Matches; 8 | 9 | namespace Zxcvbn.Tests.Matcher 10 | { 11 | public class L33tMatcherTests 12 | { 13 | private static readonly DictionaryMatcher TestDictionary1 = new DictionaryMatcher("words", new[] { "aac", string.Empty, "password", "paassword", "asdf0" }); 14 | 15 | private static readonly DictionaryMatcher TestDictionary2 = new DictionaryMatcher("words2", new[] { "cgo" }); 16 | 17 | private static readonly ReadOnlyDictionary TestL33tTable = new ReadOnlyDictionary(new Dictionary 18 | { 19 | ['a'] = new[] { '4', '@' }, 20 | ['c'] = new[] { '(', '{', '[', '<' }, 21 | ['g'] = new[] { '6', '9' }, 22 | ['o'] = new[] { '0' }, 23 | }); 24 | 25 | [Fact] 26 | public void DoesNotMatchEmptyString() 27 | { 28 | L33tMatcher.L33tTable = TestL33tTable; 29 | var matcher = new L33tMatcher(new List { TestDictionary1, TestDictionary2 }); 30 | 31 | var result = matcher.MatchPassword(string.Empty); 32 | result.Should().BeEmpty(); 33 | } 34 | 35 | [Fact] 36 | public void DoesNotMatchNonL33tWords() 37 | { 38 | L33tMatcher.L33tTable = TestL33tTable; 39 | var matcher = new L33tMatcher(new List { TestDictionary1, TestDictionary2 }); 40 | 41 | var result = matcher.MatchPassword("password"); 42 | result.Should().BeEmpty(); 43 | } 44 | 45 | [Fact] 46 | public void DoesNotMatchSingleCharacterL33tedWords() 47 | { 48 | L33tMatcher.L33tTable = TestL33tTable; 49 | var matcher = new L33tMatcher(new List { TestDictionary1, TestDictionary2 }); 50 | 51 | var result = matcher.MatchPassword("4 1 @"); 52 | result.Should().BeEmpty(); 53 | } 54 | 55 | [Fact] 56 | public void DoesNotMatchWhenMultipleSubstitutionsAreNeededForTheSameLetter() 57 | { 58 | L33tMatcher.L33tTable = TestL33tTable; 59 | var matcher = new L33tMatcher(new List { TestDictionary1, TestDictionary2 }); 60 | 61 | var result = matcher.MatchPassword("p4@ssword"); 62 | result.Should().BeEmpty(); 63 | } 64 | 65 | [Fact] 66 | public void DoesNotMatchWithSubsetsOfPossibleL33tCombinations() 67 | { 68 | L33tMatcher.L33tTable = TestL33tTable; 69 | var matcher = new L33tMatcher(new List { TestDictionary1, TestDictionary2 }); 70 | 71 | var result = matcher.MatchPassword("4sdf0"); 72 | result.Should().BeEmpty(); 73 | } 74 | 75 | [Fact] 76 | public void EnumerateL33tSubsGetsTheSetOfSubstitutionsForAPassword() 77 | { 78 | L33tMatcher.L33tTable = TestL33tTable; 79 | 80 | var actual = L33tMatcher.EnumerateSubtitutions(new ReadOnlyDictionary(new Dictionary())); 81 | actual.Single().Should().BeEmpty(); 82 | 83 | actual = L33tMatcher.EnumerateSubtitutions(new ReadOnlyDictionary(new Dictionary 84 | { 85 | { 'a', new[] { '@' } }, 86 | })); 87 | var expected = new List>() 88 | { 89 | new Dictionary() 90 | { 91 | { '@', 'a' }, 92 | }, 93 | }; 94 | actual.Should().BeEquivalentTo(expected); 95 | 96 | actual = L33tMatcher.EnumerateSubtitutions(new ReadOnlyDictionary(new Dictionary 97 | { 98 | { 'a', new[] { '@', '4' } }, 99 | })); 100 | expected = new List>() 101 | { 102 | new Dictionary() 103 | { 104 | { '@', 'a' }, 105 | }, 106 | new Dictionary() 107 | { 108 | { '4', 'a' }, 109 | }, 110 | }; 111 | actual.Should().BeEquivalentTo(expected); 112 | 113 | actual = L33tMatcher.EnumerateSubtitutions(new ReadOnlyDictionary(new Dictionary 114 | { 115 | { 'a', new[] { '@', '4' } }, 116 | { 'c', new[] { '(' } }, 117 | })); 118 | expected = new List>() 119 | { 120 | new Dictionary() 121 | { 122 | { '@', 'a' }, 123 | { '(', 'c' }, 124 | }, 125 | new Dictionary() 126 | { 127 | { '4', 'a' }, 128 | { '(', 'c' }, 129 | }, 130 | }; 131 | actual.Should().BeEquivalentTo(expected); 132 | } 133 | 134 | [Fact] 135 | public void MatchesCommonL33tSubstitutions() 136 | { 137 | L33tMatcher.L33tTable = TestL33tTable; 138 | var matcher = new L33tMatcher(new List { TestDictionary1, TestDictionary2 }); 139 | 140 | var expected = new List 141 | { 142 | new DictionaryMatch() 143 | { 144 | DictionaryName = "words", 145 | i = 0, 146 | j = 7, 147 | MatchedWord = "password", 148 | Rank = 3, 149 | Reversed = false, 150 | L33t = true, 151 | Token = "p4ssword", 152 | L33tSubs = new Dictionary 153 | { 154 | { '4', 'a' }, 155 | }, 156 | }, 157 | }; 158 | var result = matcher.MatchPassword("p4ssword"); 159 | result.Should().BeEquivalentTo(expected); 160 | 161 | expected = new List 162 | { 163 | new DictionaryMatch() 164 | { 165 | DictionaryName = "words", 166 | i = 0, 167 | j = 7, 168 | MatchedWord = "password", 169 | Rank = 3, 170 | Reversed = false, 171 | L33t = true, 172 | Token = "p@ssw0rd", 173 | L33tSubs = new Dictionary 174 | { 175 | { '@', 'a' }, { '0', 'o' }, 176 | }, 177 | }, 178 | }; 179 | result = matcher.MatchPassword("p@ssw0rd"); 180 | result.Should().BeEquivalentTo(expected); 181 | 182 | expected = new List 183 | { 184 | new DictionaryMatch() 185 | { 186 | DictionaryName = "words2", 187 | i = 5, 188 | j = 7, 189 | MatchedWord = "cgo", 190 | Rank = 1, 191 | Reversed = false, 192 | L33t = true, 193 | Token = "{G0", 194 | L33tSubs = new Dictionary 195 | { 196 | { '{', 'c' }, { '0', 'o' }, 197 | }, 198 | }, 199 | }; 200 | result = matcher.MatchPassword("aSdfO{G0asDfO"); 201 | result.Should().BeEquivalentTo(expected); 202 | } 203 | 204 | [Fact] 205 | public void MatchesOveralppingL33tPatterns() 206 | { 207 | L33tMatcher.L33tTable = TestL33tTable; 208 | var matcher = new L33tMatcher(new List { TestDictionary1, TestDictionary2 }); 209 | 210 | var expected = new List 211 | { 212 | new DictionaryMatch() 213 | { 214 | DictionaryName = "words", 215 | i = 0, 216 | j = 2, 217 | MatchedWord = "aac", 218 | Rank = 1, 219 | Reversed = false, 220 | L33t = true, 221 | Token = "@a(", 222 | L33tSubs = new Dictionary 223 | { 224 | { '@', 'a' }, 225 | { '(', 'c' }, 226 | }, 227 | }, 228 | new DictionaryMatch() 229 | { 230 | DictionaryName = "words2", 231 | i = 2, 232 | j = 4, 233 | MatchedWord = "cgo", 234 | Rank = 1, 235 | Reversed = false, 236 | L33t = true, 237 | Token = "(go", 238 | L33tSubs = new Dictionary 239 | { 240 | { '(', 'c' }, 241 | }, 242 | }, 243 | new DictionaryMatch() 244 | { 245 | DictionaryName = "words2", 246 | i = 5, 247 | j = 7, 248 | MatchedWord = "cgo", 249 | Rank = 1, 250 | Reversed = false, 251 | L33t = true, 252 | Token = "{G0", 253 | L33tSubs = new Dictionary 254 | { 255 | { '{', 'c' }, { '0', 'o' }, 256 | }, 257 | }, 258 | }; 259 | var result = matcher.MatchPassword("@a(go{G0"); 260 | result.Should().BeEquivalentTo(expected); 261 | } 262 | 263 | [Fact] 264 | public void RelevantL33tTableReducesSubstitutions() 265 | { 266 | L33tMatcher.L33tTable = TestL33tTable; 267 | 268 | var actual = L33tMatcher.RelevantL33tSubtable(string.Empty); 269 | actual.Should().BeEmpty(); 270 | 271 | actual = L33tMatcher.RelevantL33tSubtable("abcdefgo123578!#$&*)]}>"); 272 | actual.Should().BeEmpty(); 273 | 274 | actual = L33tMatcher.RelevantL33tSubtable("a"); 275 | actual.Should().BeEmpty(); 276 | 277 | var expected = new Dictionary 278 | { 279 | { 'a', new[] { '4' } }, 280 | }; 281 | actual = L33tMatcher.RelevantL33tSubtable("4"); 282 | actual.Should().BeEquivalentTo(expected); 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Matcher/RegexMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Linq; 3 | using Xunit; 4 | using Zxcvbn.Matcher; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Tests.Matcher 8 | { 9 | public class RegexMatcherTests 10 | { 11 | [Theory] 12 | [InlineData("1922", "recent_year")] 13 | [InlineData("2017", "recent_year")] 14 | public void MatchesPattern(string pattern, string name) 15 | { 16 | var re = new RegexMatcher(pattern, name); 17 | 18 | var result = re.MatchPassword(pattern).OfType().ToList(); 19 | 20 | var expected = new[] 21 | { 22 | new RegexMatch 23 | { 24 | i = 0, 25 | j = pattern.Length - 1, 26 | RegexName = name, 27 | Token = pattern, 28 | }, 29 | }; 30 | result.Should().BeEquivalentTo(expected); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Matcher/ReverseDictionaryMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Linq; 3 | using Xunit; 4 | using Zxcvbn.Matcher; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Tests.Matcher 8 | { 9 | public class ReverseDictionaryMatcherTests 10 | { 11 | private readonly ReverseDictionaryMatcher matcher = new ReverseDictionaryMatcher("d1", "test_dictionary_3.txt"); 12 | 13 | [Fact] 14 | public void MatchesAganstReversedWords() 15 | { 16 | var result = matcher.MatchPassword("0123456789").OfType().ToList(); 17 | 18 | var expected = new[] 19 | { 20 | new DictionaryMatch 21 | { 22 | DictionaryName = "d1", 23 | i = 1, 24 | j = 3, 25 | MatchedWord = "321", 26 | Rank = 2, 27 | Reversed = true, 28 | L33t = false, 29 | Token = "123", 30 | }, 31 | new DictionaryMatch 32 | { 33 | DictionaryName = "d1", 34 | i = 4, 35 | j = 6, 36 | MatchedWord = "654", 37 | Rank = 4, 38 | Reversed = true, 39 | L33t = false, 40 | Token = "456", 41 | }, 42 | }; 43 | result.Should().BeEquivalentTo(expected); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Matcher/SequenceMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Linq; 3 | using Xunit; 4 | using Zxcvbn.Matcher; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Tests.Matcher 8 | { 9 | public class SequenceMatcherTests 10 | { 11 | private readonly SequenceMatcher matcher = new SequenceMatcher(); 12 | 13 | [Theory] 14 | [InlineData("")] 15 | [InlineData("a")] 16 | [InlineData("1")] 17 | public void DoesntMatchVeryEmptyOrOneLengthSequences(string password) 18 | { 19 | var res = matcher.MatchPassword(password); 20 | res.Should().BeEmpty(); 21 | } 22 | 23 | [Theory] 24 | [InlineData("", "!")] 25 | [InlineData("", "22")] 26 | [InlineData("!", "!")] 27 | [InlineData("!", "22")] 28 | [InlineData("22", "!")] 29 | [InlineData("22", "22")] 30 | public void MatchesEmbeddedSequencePatterns(string prefix, string suffix) 31 | { 32 | const string pattern = "jihg"; 33 | 34 | var password = prefix + pattern + suffix; 35 | var i = prefix.Length; 36 | var j = i + pattern.Length - 1; 37 | 38 | var result = matcher.MatchPassword(password).OfType().ToList(); 39 | 40 | var expected = new[] 41 | { 42 | new SequenceMatch 43 | { 44 | Ascending = false, 45 | i = i, 46 | j = j, 47 | SequenceName = "lower", 48 | SequenceSpace = 26, 49 | Token = pattern, 50 | }, 51 | }; 52 | result.Should().BeEquivalentTo(expected); 53 | } 54 | 55 | [Theory] 56 | [InlineData("ABC", "upper", true, 26)] 57 | [InlineData("CBA", "upper", false, 26)] 58 | [InlineData("PQR", "upper", true, 26)] 59 | [InlineData("RQP", "upper", false, 26)] 60 | [InlineData("XYZ", "upper", true, 26)] 61 | [InlineData("ZYX", "upper", false, 26)] 62 | [InlineData("abcd", "lower", true, 26)] 63 | [InlineData("dcba", "lower", false, 26)] 64 | [InlineData("jihg", "lower", false, 26)] 65 | [InlineData("wxyz", "lower", true, 26)] 66 | [InlineData("zxvt", "lower", false, 26)] 67 | [InlineData("0369", "digits", true, 10)] 68 | [InlineData("97531", "digits", false, 10)] 69 | public void MatchesGeneralSequence(string password, string name, bool ascending, int space) 70 | { 71 | var result = matcher.MatchPassword(password).OfType().ToList(); 72 | 73 | var expected = new[] 74 | { 75 | new SequenceMatch 76 | { 77 | Ascending = ascending, 78 | i = 0, 79 | j = password.Length - 1, 80 | SequenceName = name, 81 | SequenceSpace = space, 82 | Token = password, 83 | }, 84 | }; 85 | result.Should().BeEquivalentTo(expected); 86 | } 87 | 88 | [Fact] 89 | public void MatchesOverlappingPatterns() 90 | { 91 | var result = matcher.MatchPassword("abcbabc").OfType().ToList(); 92 | 93 | var expected = new[] 94 | { 95 | new SequenceMatch 96 | { 97 | Ascending = true, 98 | i = 0, 99 | j = 2, 100 | SequenceName = "lower", 101 | SequenceSpace = 26, 102 | Token = "abc", 103 | }, 104 | new SequenceMatch 105 | { 106 | Ascending = false, 107 | i = 2, 108 | j = 4, 109 | SequenceName = "lower", 110 | SequenceSpace = 26, 111 | Token = "cba", 112 | }, 113 | new SequenceMatch 114 | { 115 | Ascending = true, 116 | i = 4, 117 | j = 6, 118 | SequenceName = "lower", 119 | SequenceSpace = 26, 120 | Token = "abc", 121 | }, 122 | }; 123 | result.Should().BeEquivalentTo(expected); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Matcher/SpatialMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Linq; 3 | using Xunit; 4 | using Zxcvbn.Matcher; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Tests.Matcher 8 | { 9 | public class SpatialMatcherTests 10 | { 11 | private readonly SpatialMatcher matcher = new SpatialMatcher(); 12 | 13 | [Theory] 14 | [InlineData("")] 15 | [InlineData("/")] 16 | [InlineData("qw")] 17 | [InlineData("*/")] 18 | public void DoesNotmatch1And2CharacterSpatialPatterns(string password) 19 | { 20 | var matches = matcher.MatchPassword(password); 21 | 22 | matches.Should().BeEmpty(); 23 | } 24 | 25 | [Theory] 26 | [InlineData("12345", "qwerty", 1, 0)] 27 | [InlineData("@WSX", "qwerty", 1, 4)] 28 | [InlineData("6tfGHJ", "qwerty", 2, 3)] 29 | [InlineData("hGFd", "qwerty", 1, 2)] 30 | [InlineData("/;p09876yhn", "qwerty", 3, 0)] 31 | [InlineData("Xdr%", "qwerty", 1, 2)] 32 | [InlineData("159-", "keypad", 1, 0)] 33 | [InlineData("*84", "keypad", 1, 0)] 34 | [InlineData("/8520", "keypad", 1, 0)] 35 | [InlineData("369", "keypad", 1, 0)] 36 | [InlineData("/963.", "mac_keypad", 1, 0)] 37 | [InlineData("*-632.0214", "mac_keypad", 9, 0)] 38 | [InlineData("aoEP%yIxkjq:", "dvorak", 4, 5)] 39 | [InlineData(";qoaOQ:Aoq;a", "dvorak", 11, 4)] 40 | public void MatchesGeneralPattern(string pattern, string keyboard, int turns, int shifts) 41 | { 42 | var result = matcher.MatchPassword(pattern).OfType().Where(m => m.Graph == keyboard).ToList(); 43 | var expected = new[] 44 | { 45 | new SpatialMatch 46 | { 47 | Graph = keyboard, 48 | Turns = turns, 49 | ShiftedCount = shifts, 50 | i = 0, 51 | j = pattern.Length - 1, 52 | Token = pattern, 53 | }, 54 | }; 55 | result.Should().BeEquivalentTo(expected); 56 | } 57 | 58 | [Fact] 59 | public void MatchesSpatialPatternSurroundedByNonSpatialPatterns() 60 | { 61 | var matcher = new SpatialMatcher(); 62 | var pattern = "6tfGHJ"; 63 | var password = $"rz!{pattern}%z"; 64 | var result = matcher.MatchPassword(password).OfType().Where(m => m.Graph == "qwerty").ToList(); 65 | 66 | var expected = new[] 67 | { 68 | new SpatialMatch 69 | { 70 | Graph = "qwerty", 71 | Turns = 2, 72 | ShiftedCount = 3, 73 | i = 3, 74 | j = 3 + pattern.Length - 1, 75 | Token = pattern, 76 | }, 77 | }; 78 | result.Should().BeEquivalentTo(expected); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Scoring/BruteForceScoringTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Xunit; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Tests.Scoring 8 | { 9 | public class BruteForceScoringTests 10 | { 11 | [Fact] 12 | public void MostGuessableMatchSequenceChoosesMatchWithFewestGuessesIfTheyMatchTheSameSpan() 13 | { 14 | var password = "0123456789"; 15 | var worseMatch = CreateTestMatch(0, 9, 1); 16 | var bestMatch = CreateTestMatch(0, 9, 2); 17 | 18 | var expected = new List 19 | { 20 | worseMatch, 21 | }; 22 | 23 | var result = PasswordScoring.MostGuessableMatchSequence(password, new List { worseMatch, bestMatch }, true); 24 | result.Sequence.Should().BeEquivalentTo(expected); 25 | 26 | result = PasswordScoring.MostGuessableMatchSequence(password, new List { bestMatch, worseMatch }, true); 27 | result.Sequence.Should().BeEquivalentTo(expected); 28 | } 29 | 30 | [Fact] 31 | public void MostGuessableMatchSequenceChoosesOptimalMatches() 32 | { 33 | var password = "0123456789"; 34 | var m0 = CreateTestMatch(0, 9, 3); 35 | var m1 = CreateTestMatch(0, 3, 2); 36 | var m2 = CreateTestMatch(4, 9, 1); 37 | 38 | var expected = new List 39 | { 40 | m0, 41 | }; 42 | 43 | var result = PasswordScoring.MostGuessableMatchSequence(password, new List { m0, m1, m2 }, true); 44 | result.Sequence.Should().BeEquivalentTo(expected); 45 | 46 | m0.Guesses = 5; 47 | 48 | expected = new List 49 | { 50 | m1, m2, 51 | }; 52 | 53 | result = PasswordScoring.MostGuessableMatchSequence(password, new List { m0, m1, m2 }, true); 54 | result.Sequence.Should().BeEquivalentTo(expected); 55 | } 56 | 57 | [Fact] 58 | public void MostGuessableMatchSequenceReturnsBruteForceThenMatchThenBruteForceWhenMatchCoversAnInfixOfPassword() 59 | { 60 | var password = "0123456789"; 61 | var existingMatch = CreateTestMatch(1, 8, 1); 62 | 63 | var expected = new List 64 | { 65 | new BruteForceMatch 66 | { 67 | i = 0, 68 | j = 0, 69 | Token = "0", 70 | Guesses = 11, 71 | }, 72 | existingMatch, 73 | new BruteForceMatch 74 | { 75 | i = 9, 76 | j = 9, 77 | Token = "9", 78 | Guesses = 11, 79 | }, 80 | }; 81 | 82 | var result = PasswordScoring.MostGuessableMatchSequence(password, new List { existingMatch }, true); 83 | result.Sequence.Should().BeEquivalentTo(expected); 84 | } 85 | 86 | [Fact] 87 | public void MostGuessableMatchSequenceReturnsBruteForceThenMatchWhenMatchCoversASuffixOfPassword() 88 | { 89 | var password = "0123456789"; 90 | var existingMatch = CreateTestMatch(3, 9, 1); 91 | 92 | var expected = new List 93 | { 94 | new BruteForceMatch 95 | { 96 | i = 0, 97 | j = 2, 98 | Token = "012", 99 | Guesses = 1000, 100 | }, 101 | existingMatch, 102 | }; 103 | 104 | var result = PasswordScoring.MostGuessableMatchSequence(password, new List { existingMatch }, true); 105 | result.Sequence.Should().BeEquivalentTo(expected); 106 | } 107 | 108 | [Fact] 109 | public void MostGuessableMatchSequenceReturnsMatchThenBruteForceWhenMatchCoversAPrefixOfPassword() 110 | { 111 | var password = "0123456789"; 112 | var existingMatch = CreateTestMatch(0, 5, 1); 113 | 114 | var expected = new List 115 | { 116 | existingMatch, 117 | new BruteForceMatch 118 | { 119 | i = 6, 120 | j = 9, 121 | Token = "6789", 122 | Guesses = 10000, 123 | }, 124 | }; 125 | 126 | var result = PasswordScoring.MostGuessableMatchSequence(password, new List { existingMatch }, true); 127 | result.Sequence.Should().BeEquivalentTo(expected); 128 | } 129 | 130 | [Fact] 131 | public void MostGuessableMatchSequenceRetursnOneMatchForEmptySequence() 132 | { 133 | var password = "0123456789"; 134 | var expected = new List 135 | { 136 | new BruteForceMatch 137 | { 138 | i = 0, 139 | j = 9, 140 | Token = password, 141 | Guesses = 10000000000, 142 | }, 143 | }; 144 | 145 | var result = PasswordScoring.MostGuessableMatchSequence(password, Enumerable.Empty()); 146 | result.Sequence.Should().BeEquivalentTo(expected); 147 | } 148 | 149 | private Match CreateTestMatch(int i, int j, double guesses) 150 | { 151 | return new DateMatch 152 | { 153 | i = i, 154 | j = j, 155 | Guesses = guesses, 156 | Token = "abc", 157 | Day = 1, 158 | Month = 2, 159 | Separator = "/", 160 | Year = 3, 161 | }; 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Scoring/DateScoringTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | using Zxcvbn.Matcher; 4 | using Zxcvbn.Matcher.Matches; 5 | using Zxcvbn.Scoring; 6 | 7 | namespace Zxcvbn.Tests.Scoring 8 | { 9 | public class DateScoringTests 10 | { 11 | [Fact] 12 | public void AssumesMinYearSpaceForRecentYears() 13 | { 14 | var match = new DateMatch 15 | { 16 | Token = "2010", 17 | Separator = string.Empty, 18 | Year = 2010, 19 | Month = 1, 20 | Day = 1, 21 | i = 1, 22 | j = 2, 23 | }; 24 | 25 | var actual = DateGuessesCalculator.CalculateGuesses(match); 26 | var expected = 365 * DateGuessesCalculator.MinimumYearSpace; 27 | 28 | actual.Should().Be(expected); 29 | } 30 | 31 | [Fact] 32 | public void CalculatesBasedOnReferenceYear() 33 | { 34 | var match = new DateMatch 35 | { 36 | Token = "1923", 37 | Separator = string.Empty, 38 | Year = 1923, 39 | Month = 1, 40 | Day = 1, 41 | i = 1, 42 | j = 2, 43 | }; 44 | 45 | var actual = DateGuessesCalculator.CalculateGuesses(match); 46 | var expected = 365 * (DateMatcher.ReferenceYear - match.Year); 47 | 48 | actual.Should().Be(expected); 49 | } 50 | 51 | [Fact] 52 | public void IsDelegatedToBeEstimateGuesses() 53 | { 54 | var match = new DateMatch 55 | { 56 | Token = "1923", 57 | Separator = string.Empty, 58 | Year = 1923, 59 | Month = 1, 60 | Day = 1, 61 | i = 1, 62 | j = 2, 63 | }; 64 | 65 | var actual = PasswordScoring.EstimateGuesses(match, "1923"); 66 | var expected = 365 * (DateMatcher.ReferenceYear - match.Year); 67 | 68 | actual.Should().Be(expected); 69 | } 70 | 71 | [Fact] 72 | public void MultipliesByFourForSeparators() 73 | { 74 | var match = new DateMatch 75 | { 76 | Token = "1923", 77 | Separator = "/", 78 | Year = 1923, 79 | Month = 1, 80 | Day = 1, 81 | i = 1, 82 | j = 2, 83 | }; 84 | 85 | var actual = DateGuessesCalculator.CalculateGuesses(match); 86 | var expected = 365 * (DateMatcher.ReferenceYear - match.Year) * 4; 87 | 88 | actual.Should().Be(expected); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Scoring/RegexScoringTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | using Zxcvbn.Matcher; 4 | using Zxcvbn.Matcher.Matches; 5 | 6 | namespace Zxcvbn.Tests.Scoring 7 | { 8 | public class RegexScoringTests 9 | { 10 | [Fact] 11 | public void CalculatesWithReferenceYear() 12 | { 13 | var match = new RegexMatch 14 | { 15 | Token = "1972", 16 | RegexName = "recent_year", 17 | i = 1, 18 | j = 2, 19 | }; 20 | 21 | var result = PasswordScoring.EstimateGuesses(match, "1972"); 22 | result.Should().Be(DateMatcher.ReferenceYear - 1972); 23 | } 24 | 25 | [Fact] 26 | public void IsDelegatedToByEstimateGuesses() 27 | { 28 | var match = new RegexMatch 29 | { 30 | Token = "1972", 31 | RegexName = "recent_year", 32 | i = 1, 33 | j = 2, 34 | }; 35 | 36 | var result = PasswordScoring.EstimateGuesses(match, "1972"); 37 | result.Should().Be(DateMatcher.ReferenceYear - 1972); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Scoring/RepeatScoringTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Collections.Generic; 3 | using Xunit; 4 | using Zxcvbn.Matcher.Matches; 5 | using Zxcvbn.Scoring; 6 | 7 | namespace Zxcvbn.Tests.Scoring 8 | { 9 | public class RepeatScoringTests 10 | { 11 | [Theory] 12 | [InlineData("aa", "a", 2)] 13 | [InlineData("999", "9", 3)] 14 | [InlineData("$$$$", "$", 4)] 15 | [InlineData("abab", "ab", 2)] 16 | [InlineData("batterystaplebatterystaplebatterystaple", "batterystaple", 3)] 17 | public void CalculatesTheRightNumberOfGuesses(string token, string baseToken, int expectedRepeats) 18 | { 19 | var baseGuesses = PasswordScoring.MostGuessableMatchSequence(baseToken, Zxcvbn.Core.GetAllMatches(baseToken)).Guesses; 20 | 21 | var match = new RepeatMatch 22 | { 23 | Token = token, 24 | BaseToken = baseToken, 25 | BaseGuesses = baseGuesses, 26 | RepeatCount = expectedRepeats, 27 | BaseMatchItems = new List(), 28 | i = 1, 29 | j = 2, 30 | }; 31 | 32 | var expected = baseGuesses * expectedRepeats; 33 | 34 | var actual = RepeatGuessesCalculator.CalculateGuesses(match); 35 | actual.Should().Be(expected); 36 | } 37 | 38 | [Fact] 39 | public void IsDelegatedToByEstimateGuesses() 40 | { 41 | var match = new RepeatMatch 42 | { 43 | Token = "aa", 44 | BaseToken = "a", 45 | BaseGuesses = PasswordScoring.MostGuessableMatchSequence("a", Zxcvbn.Core.GetAllMatches("a")).Guesses, 46 | BaseMatchItems = new List(), 47 | RepeatCount = 2, 48 | i = 1, 49 | j = 2, 50 | }; 51 | 52 | var expected = RepeatGuessesCalculator.CalculateGuesses(match); 53 | var actual = PasswordScoring.EstimateGuesses(match, "aa"); 54 | 55 | actual.Should().Be(expected); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Scoring/ScoringTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn.Tests.Scoring 6 | { 7 | public class ScoringTests 8 | { 9 | [Fact] 10 | public void ReturnsCachedGuessesIfAvailable() 11 | { 12 | var match = new DateMatch 13 | { 14 | Guesses = 1, 15 | Token = "1977", 16 | Year = 1977, 17 | Month = 8, 18 | Day = 14, 19 | Separator = "/", 20 | i = 1, 21 | j = 2, 22 | }; 23 | 24 | var actual = PasswordScoring.EstimateGuesses(match, string.Empty); 25 | actual.Should().Be(1); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Scoring/SequenceScoringTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | using Zxcvbn.Matcher.Matches; 4 | using Zxcvbn.Scoring; 5 | 6 | namespace Zxcvbn.Tests.Scoring 7 | { 8 | public class SequenceScoringTests 9 | { 10 | [Theory] 11 | [InlineData("ab", true, 8)] 12 | [InlineData("XYZ", true, 78)] 13 | [InlineData("4567", true, 40)] 14 | [InlineData("7654", false, 80)] 15 | [InlineData("ZYX", false, 24)] 16 | public void CalculatesTheCorrectNumberOfGuesses(string token, bool ascending, int expected) 17 | { 18 | var match = new SequenceMatch 19 | { 20 | Token = token, 21 | Ascending = ascending, 22 | i = 1, 23 | j = 2, 24 | SequenceName = "abc", 25 | SequenceSpace = 1, 26 | }; 27 | 28 | var actual = SequenceGuessesCalculator.CalculateGuesses(match); 29 | actual.Should().Be(expected); 30 | } 31 | 32 | [Fact] 33 | public void IsDelegatedToByEstimateGuesses() 34 | { 35 | var match = new SequenceMatch 36 | { 37 | Token = "ab", 38 | Ascending = true, 39 | i = 1, 40 | j = 2, 41 | SequenceName = "abc", 42 | SequenceSpace = 1, 43 | }; 44 | 45 | var actual = SequenceGuessesCalculator.CalculateGuesses(match); 46 | actual.Should().Be(8); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /zxcvbn-core-test/Scoring/SpatialTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using Xunit; 4 | using Zxcvbn.Matcher.Matches; 5 | using Zxcvbn.Scoring; 6 | 7 | namespace Zxcvbn.Tests.Scoring 8 | { 9 | public class SpatialTests 10 | { 11 | [Fact] 12 | public void AccountsForTurnPositionsDirectionsAndStartingKey() 13 | { 14 | var match = new SpatialMatch 15 | { 16 | Token = "zxcvbn", 17 | Graph = "qwerty", 18 | Turns = 3, 19 | ShiftedCount = 0, 20 | i = 1, 21 | j = 2, 22 | }; 23 | 24 | var l = match.Token.Length; 25 | var s = SpatialGuessesCalculator.KeyboardStartingPositions; 26 | var d = SpatialGuessesCalculator.KeyboardAverageDegree; 27 | var expected = 0.0; 28 | 29 | for (var i = 2; i <= l; i++) 30 | { 31 | for (var j = 1; j <= Math.Min(match.Turns, i - 1); j++) 32 | { 33 | expected += PasswordScoring.Binomial(i - 1, j - 1) * s * Math.Pow(d, j); 34 | } 35 | } 36 | 37 | var actual = SpatialGuessesCalculator.CalculateGuesses(match); 38 | 39 | actual.Should().Be(expected); 40 | } 41 | 42 | [Fact] 43 | public void AddsToTheGuessesForShiftedKeys() 44 | { 45 | var match = new SpatialMatch 46 | { 47 | Token = "zxcvbn", 48 | Graph = "qwerty", 49 | Turns = 1, 50 | ShiftedCount = 2, 51 | i = 1, 52 | j = 2, 53 | }; 54 | 55 | var expected = SpatialGuessesCalculator.KeyboardStartingPositions * SpatialGuessesCalculator.KeyboardAverageDegree * (match.Token.Length - 1) * 21; 56 | 57 | var actual = SpatialGuessesCalculator.CalculateGuesses(match); 58 | 59 | actual.Should().Be(expected); 60 | } 61 | 62 | [Fact] 63 | public void DoublesGuessesIfEverythingIsShifted() 64 | { 65 | var match = new SpatialMatch 66 | { 67 | Token = "zxcvbn", 68 | Graph = "qwerty", 69 | Turns = 1, 70 | ShiftedCount = 6, 71 | i = 1, 72 | j = 2, 73 | }; 74 | 75 | var expected = SpatialGuessesCalculator.KeyboardStartingPositions * SpatialGuessesCalculator.KeyboardAverageDegree * (match.Token.Length - 1) * 2; 76 | 77 | var actual = SpatialGuessesCalculator.CalculateGuesses(match); 78 | 79 | actual.Should().Be(expected); 80 | } 81 | 82 | [Fact] 83 | public void GuessesWhenThereAreNoTurns() 84 | { 85 | var match = new SpatialMatch 86 | { 87 | Token = "zxcvbn", 88 | Graph = "qwerty", 89 | Turns = 1, 90 | ShiftedCount = 0, 91 | i = 1, 92 | j = 2, 93 | }; 94 | 95 | var expected = SpatialGuessesCalculator.KeyboardStartingPositions * SpatialGuessesCalculator.KeyboardAverageDegree * (match.Token.Length - 1); 96 | 97 | var actual = SpatialGuessesCalculator.CalculateGuesses(match); 98 | 99 | actual.Should().Be(expected); 100 | } 101 | 102 | [Fact] 103 | public void IsDelegatedToByEstimateGuesses() 104 | { 105 | var match = new SpatialMatch 106 | { 107 | Token = "zxcvbn", 108 | Graph = "qwerty", 109 | Turns = 1, 110 | ShiftedCount = 0, 111 | i = 1, 112 | j = 2, 113 | }; 114 | 115 | var expected = SpatialGuessesCalculator.KeyboardStartingPositions * SpatialGuessesCalculator.KeyboardAverageDegree * (match.Token.Length - 1); 116 | 117 | var actual = PasswordScoring.EstimateGuesses(match, match.Token); 118 | 119 | actual.Should().Be(expected); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /zxcvbn-core-test/test_dictionary_1.txt: -------------------------------------------------------------------------------- 1 | motherboard 2 | mother 3 | board 4 | abcd 5 | cdef -------------------------------------------------------------------------------- /zxcvbn-core-test/test_dictionary_2.txt: -------------------------------------------------------------------------------- 1 | z 2 | 8 3 | 99 4 | $ 5 | asdf1234&* -------------------------------------------------------------------------------- /zxcvbn-core-test/test_dictionary_3.txt: -------------------------------------------------------------------------------- 1 | 123 2 | 321 3 | 456 4 | 654 -------------------------------------------------------------------------------- /zxcvbn-core-test/zxcvbn-core-test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net5.0 4 | false 5 | zxcvbn-core-test 6 | Zxcvbn.Tests 7 | true 8 | zxcvbn-strong-name-key.snk 9 | False 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Always 34 | 35 | 36 | Always 37 | 38 | 39 | Always 40 | 41 | 42 | -------------------------------------------------------------------------------- /zxcvbn-core-test/zxcvbn-strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trichards57/zxcvbn-cs/2e680287ca64bde8ee5366a17ac0d2af7a11d8ea/zxcvbn-core-test/zxcvbn-strong-name-key.snk -------------------------------------------------------------------------------- /zxcvbn-core/AttackTimes.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn 2 | { 3 | /// 4 | /// A summary of the attack times for the password. 5 | /// 6 | internal class AttackTimes 7 | { 8 | /// 9 | /// Gets or sets the display version of the crack times. 10 | /// 11 | public CrackTimesDisplay CrackTimesDisplay { get; set; } 12 | 13 | /// 14 | /// Gets or sets the numerical version of the crack times. 15 | /// 16 | public CrackTimes CrackTimesSeconds { get; set; } 17 | 18 | /// 19 | /// Gets or sets the overall score. 20 | /// 21 | public int Score { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /zxcvbn-core/Core.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn 6 | { 7 | /// 8 | /// Estimates the strength of passwords. 9 | /// 10 | public static class Core 11 | { 12 | /// 13 | /// Perform the password matching on the given password and user inputs. 14 | /// 15 | /// The password to assess. 16 | /// Optionally, an enumarable of user data. 17 | /// Result for the password. 18 | public static Result EvaluatePassword(string password, IEnumerable userInputs = null) 19 | { 20 | password = password ?? string.Empty; 21 | userInputs = userInputs ?? Enumerable.Empty(); 22 | 23 | var timer = System.Diagnostics.Stopwatch.StartNew(); 24 | 25 | var matches = GetAllMatches(password, userInputs); 26 | var result = PasswordScoring.MostGuessableMatchSequence(password, matches); 27 | timer.Stop(); 28 | 29 | var attackTimes = TimeEstimates.EstimateAttackTimes(result.Guesses); 30 | result.Score = attackTimes.Score; 31 | var feedback = Feedback.GetFeedback(result.Score, result.Sequence); 32 | 33 | var finalResult = new Result 34 | { 35 | CalcTime = timer.ElapsedMilliseconds, 36 | CrackTime = attackTimes.CrackTimesSeconds, 37 | CrackTimeDisplay = attackTimes.CrackTimesDisplay, 38 | Score = attackTimes.Score, 39 | MatchSequence = result.Sequence, 40 | Guesses = result.Guesses, 41 | Password = result.Password, 42 | Feedback = feedback, 43 | }; 44 | 45 | return finalResult; 46 | } 47 | 48 | /// 49 | /// Gets all matches associated with a password. 50 | /// 51 | /// The password to assess. 52 | /// The user input dictionary. 53 | /// An enumerable of relevant matches. 54 | internal static IEnumerable GetAllMatches(string token, IEnumerable userInputs = null) 55 | { 56 | userInputs = userInputs ?? Enumerable.Empty(); 57 | 58 | return DefaultMatcherFactory.CreateMatchers(userInputs).SelectMany(matcher => matcher.MatchPassword(token)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /zxcvbn-core/CrackTimes.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn 2 | { 3 | /// 4 | /// Represents the time needed to crack the password under different conditions. 5 | /// 6 | public class CrackTimes 7 | { 8 | /// 9 | /// Gets the time if offline hashing at 1e10 per second. 10 | /// 11 | public double OfflineFastHashing1e10PerSecond { get; internal set; } 12 | 13 | /// 14 | /// Gets the time if offline hashing at 1e4 per second. 15 | /// 16 | public double OfflineSlowHashing1e4PerSecond { get; internal set; } 17 | 18 | /// 19 | /// Gets the time if online attempting the password at 10 per second. 20 | /// 21 | public double OnlineNoThrottling10PerSecond { get; internal set; } 22 | 23 | /// 24 | /// Gets the time if online attempting the password at 100 per hour. 25 | /// 26 | public double OnlineThrottling100PerHour { get; internal set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /zxcvbn-core/CrackTimesDisplay.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn 2 | { 3 | /// 4 | /// Represents the time needed to crack the password under different conditions. 5 | /// 6 | public class CrackTimesDisplay 7 | { 8 | /// 9 | /// Gets the time if offline hashing at 1e10 per second. 10 | /// 11 | public string OfflineFastHashing1e10PerSecond { get; internal set; } 12 | 13 | /// 14 | /// Gets the time if offline hashing at 1e4 per second. 15 | /// 16 | public string OfflineSlowHashing1e4PerSecond { get; internal set; } 17 | 18 | /// 19 | /// Gets the time if online attempting the password at 10 per second. 20 | /// 21 | public string OnlineNoThrottling10PerSecond { get; internal set; } 22 | 23 | /// 24 | /// Gets the time if online attempting the password at 100 per hour. 25 | /// 26 | public string OnlineThrottling100PerHour { get; internal set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /zxcvbn-core/DefaultMatcherFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Zxcvbn.Matcher; 3 | 4 | namespace Zxcvbn 5 | { 6 | /// 7 | /// Creates the default matchers. 8 | /// 9 | internal static class DefaultMatcherFactory 10 | { 11 | private static readonly IEnumerable BuiltInMatchers = BuildBuiltInMatchers(); 12 | 13 | /// 14 | /// Gets all of the built in matchers, as well as matchers for the custom dictionaries. 15 | /// 16 | /// Enumerable of user information. 17 | /// Enumerable of matchers to use. 18 | public static IEnumerable CreateMatchers(IEnumerable userInputs) 19 | { 20 | var userInputDict = new DictionaryMatcher("user_inputs", userInputs); 21 | var leetUser = new L33tMatcher(userInputDict); 22 | 23 | return new List(BuiltInMatchers) { userInputDict, leetUser }; 24 | } 25 | 26 | private static IEnumerable BuildBuiltInMatchers() 27 | { 28 | var dictionaryMatchers = new List 29 | { 30 | new DictionaryMatcher("passwords", "passwords.lst"), 31 | new DictionaryMatcher("english", "english.lst"), 32 | new DictionaryMatcher("male_names", "male_names.lst"), 33 | new DictionaryMatcher("female_names", "female_names.lst"), 34 | new DictionaryMatcher("surnames", "surnames.lst"), 35 | new DictionaryMatcher("us_tv_and_film", "us_tv_and_film.lst"), 36 | new ReverseDictionaryMatcher("passwords", "passwords.lst"), 37 | new ReverseDictionaryMatcher("english", "english.lst"), 38 | new ReverseDictionaryMatcher("male_names", "male_names.lst"), 39 | new ReverseDictionaryMatcher("female_names", "female_names.lst"), 40 | new ReverseDictionaryMatcher("surnames", "surnames.lst"), 41 | new ReverseDictionaryMatcher("us_tv_and_film", "us_tv_and_film.lst"), 42 | }; 43 | 44 | return new List(dictionaryMatchers) 45 | { 46 | new RepeatMatcher(), 47 | new SequenceMatcher(), 48 | new RegexMatcher("19\\d\\d|200\\d|201\\d", "recent_year"), 49 | new DateMatcher(), 50 | new SpatialMatcher(), 51 | new L33tMatcher(dictionaryMatchers), 52 | }; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /zxcvbn-core/Dictionaries/male_names.lst: -------------------------------------------------------------------------------- 1 | james 2 | john 3 | robert 4 | michael 5 | william 6 | david 7 | richard 8 | charles 9 | joseph 10 | thomas 11 | christopher 12 | daniel 13 | paul 14 | mark 15 | donald 16 | george 17 | kenneth 18 | steven 19 | edward 20 | brian 21 | ronald 22 | anthony 23 | kevin 24 | jason 25 | matthew 26 | gary 27 | timothy 28 | jose 29 | larry 30 | jeffrey 31 | frank 32 | scott 33 | eric 34 | stephen 35 | andrew 36 | raymond 37 | gregory 38 | joshua 39 | jerry 40 | dennis 41 | walter 42 | patrick 43 | peter 44 | harold 45 | douglas 46 | henry 47 | carl 48 | arthur 49 | ryan 50 | roger 51 | joe 52 | juan 53 | jack 54 | albert 55 | jonathan 56 | justin 57 | terry 58 | gerald 59 | keith 60 | samuel 61 | willie 62 | ralph 63 | lawrence 64 | nicholas 65 | roy 66 | benjamin 67 | bruce 68 | brandon 69 | adam 70 | harry 71 | fred 72 | wayne 73 | billy 74 | steve 75 | louis 76 | jeremy 77 | aaron 78 | randy 79 | eugene 80 | carlos 81 | russell 82 | bobby 83 | victor 84 | ernest 85 | phillip 86 | todd 87 | jesse 88 | craig 89 | alan 90 | shawn 91 | clarence 92 | sean 93 | philip 94 | chris 95 | johnny 96 | earl 97 | jimmy 98 | antonio 99 | danny 100 | bryan 101 | tony 102 | luis 103 | mike 104 | stanley 105 | leonard 106 | nathan 107 | dale 108 | manuel 109 | rodney 110 | curtis 111 | norman 112 | marvin 113 | vincent 114 | glenn 115 | jeffery 116 | travis 117 | jeff 118 | chad 119 | jacob 120 | melvin 121 | alfred 122 | kyle 123 | francis 124 | bradley 125 | jesus 126 | herbert 127 | frederick 128 | ray 129 | joel 130 | edwin 131 | don 132 | eddie 133 | ricky 134 | troy 135 | randall 136 | barry 137 | bernard 138 | mario 139 | leroy 140 | francisco 141 | marcus 142 | micheal 143 | theodore 144 | clifford 145 | miguel 146 | oscar 147 | jay 148 | jim 149 | tom 150 | calvin 151 | alex 152 | jon 153 | ronnie 154 | bill 155 | lloyd 156 | tommy 157 | leon 158 | derek 159 | darrell 160 | jerome 161 | floyd 162 | leo 163 | alvin 164 | tim 165 | wesley 166 | dean 167 | greg 168 | jorge 169 | dustin 170 | pedro 171 | derrick 172 | dan 173 | zachary 174 | corey 175 | herman 176 | maurice 177 | vernon 178 | roberto 179 | clyde 180 | glen 181 | hector 182 | shane 183 | ricardo 184 | sam 185 | rick 186 | lester 187 | brent 188 | ramon 189 | tyler 190 | gilbert 191 | gene 192 | marc 193 | reginald 194 | ruben 195 | brett 196 | nathaniel 197 | rafael 198 | edgar 199 | milton 200 | raul 201 | ben 202 | cecil 203 | duane 204 | andre 205 | elmer 206 | brad 207 | gabriel 208 | ron 209 | roland 210 | harvey 211 | jared 212 | adrian 213 | karl 214 | cory 215 | claude 216 | erik 217 | darryl 218 | neil 219 | christian 220 | javier 221 | fernando 222 | clinton 223 | ted 224 | mathew 225 | tyrone 226 | darren 227 | lonnie 228 | lance 229 | cody 230 | julio 231 | kurt 232 | allan 233 | clayton 234 | hugh 235 | max 236 | dwayne 237 | dwight 238 | armando 239 | felix 240 | jimmie 241 | everett 242 | ian 243 | ken 244 | bob 245 | jaime 246 | casey 247 | alfredo 248 | alberto 249 | dave 250 | ivan 251 | johnnie 252 | sidney 253 | byron 254 | julian 255 | isaac 256 | clifton 257 | willard 258 | daryl 259 | virgil 260 | andy 261 | salvador 262 | kirk 263 | sergio 264 | seth 265 | kent 266 | terrance 267 | rene 268 | eduardo 269 | terrence 270 | enrique 271 | freddie 272 | stuart 273 | fredrick 274 | arturo 275 | alejandro 276 | joey 277 | nick 278 | luther 279 | wendell 280 | jeremiah 281 | evan 282 | julius 283 | donnie 284 | otis 285 | trevor 286 | luke 287 | homer 288 | gerard 289 | doug 290 | kenny 291 | hubert 292 | angelo 293 | shaun 294 | lyle 295 | matt 296 | alfonso 297 | orlando 298 | rex 299 | carlton 300 | ernesto 301 | pablo 302 | lorenzo 303 | omar 304 | wilbur 305 | blake 306 | horace 307 | roderick 308 | kerry 309 | abraham 310 | rickey 311 | ira 312 | andres 313 | cesar 314 | johnathan 315 | malcolm 316 | rudolph 317 | damon 318 | kelvin 319 | rudy 320 | preston 321 | alton 322 | archie 323 | marco 324 | pete 325 | randolph 326 | garry 327 | geoffrey 328 | jonathon 329 | felipe 330 | bennie 331 | gerardo 332 | dominic 333 | loren 334 | delbert 335 | colin 336 | guillermo 337 | earnest 338 | benny 339 | noel 340 | rodolfo 341 | myron 342 | edmund 343 | salvatore 344 | cedric 345 | lowell 346 | gregg 347 | sherman 348 | devin 349 | sylvester 350 | roosevelt 351 | israel 352 | jermaine 353 | forrest 354 | wilbert 355 | leland 356 | simon 357 | irving 358 | owen 359 | rufus 360 | woodrow 361 | sammy 362 | kristopher 363 | levi 364 | marcos 365 | gustavo 366 | jake 367 | lionel 368 | marty 369 | gilberto 370 | clint 371 | nicolas 372 | laurence 373 | ismael 374 | orville 375 | drew 376 | ervin 377 | dewey 378 | wilfred 379 | josh 380 | hugo 381 | ignacio 382 | caleb 383 | tomas 384 | sheldon 385 | erick 386 | frankie 387 | darrel 388 | rogelio 389 | terence 390 | alonzo 391 | elias 392 | bert 393 | elbert 394 | ramiro 395 | conrad 396 | noah 397 | grady 398 | phil 399 | cornelius 400 | lamar 401 | rolando 402 | clay 403 | percy 404 | bradford 405 | merle 406 | darin 407 | amos 408 | terrell 409 | moses 410 | irvin 411 | saul 412 | roman 413 | darnell 414 | randal 415 | tommie 416 | timmy 417 | darrin 418 | brendan 419 | toby 420 | van 421 | abel 422 | dominick 423 | emilio 424 | elijah 425 | cary 426 | domingo 427 | aubrey 428 | emmett 429 | marlon 430 | emanuel 431 | jerald 432 | edmond 433 | emil 434 | dewayne 435 | otto 436 | teddy 437 | reynaldo 438 | bret 439 | jess 440 | trent 441 | humberto 442 | emmanuel 443 | stephan 444 | louie 445 | vicente 446 | lamont 447 | garland 448 | micah 449 | efrain 450 | heath 451 | rodger 452 | demetrius 453 | ethan 454 | eldon 455 | rocky 456 | pierre 457 | eli 458 | bryce 459 | antoine 460 | robbie 461 | kendall 462 | royce 463 | sterling 464 | grover 465 | elton 466 | cleveland 467 | dylan 468 | chuck 469 | damian 470 | reuben 471 | stan 472 | leonardo 473 | russel 474 | erwin 475 | benito 476 | hans 477 | monte 478 | blaine 479 | ernie 480 | curt 481 | quentin 482 | agustin 483 | jamal 484 | devon 485 | adolfo 486 | tyson 487 | wilfredo 488 | bart 489 | jarrod 490 | vance 491 | denis 492 | damien 493 | joaquin 494 | harlan 495 | desmond 496 | elliot 497 | darwin 498 | gregorio 499 | kermit 500 | roscoe 501 | esteban 502 | anton 503 | solomon 504 | norbert 505 | elvin 506 | nolan 507 | carey 508 | rod 509 | quinton 510 | hal 511 | brain 512 | rob 513 | elwood 514 | kendrick 515 | darius 516 | moises 517 | marlin 518 | fidel 519 | thaddeus 520 | cliff 521 | marcel 522 | ali 523 | raphael 524 | bryon 525 | armand 526 | alvaro 527 | jeffry 528 | dane 529 | joesph 530 | thurman 531 | ned 532 | sammie 533 | rusty 534 | michel 535 | monty 536 | rory 537 | fabian 538 | reggie 539 | kris 540 | isaiah 541 | gus 542 | avery 543 | loyd 544 | diego 545 | adolph 546 | millard 547 | rocco 548 | gonzalo 549 | derick 550 | rodrigo 551 | gerry 552 | rigoberto 553 | alphonso 554 | rickie 555 | noe 556 | vern 557 | elvis 558 | bernardo 559 | mauricio 560 | hiram 561 | donovan 562 | basil 563 | nickolas 564 | scot 565 | vince 566 | quincy 567 | eddy 568 | sebastian 569 | federico 570 | ulysses 571 | heriberto 572 | donnell 573 | denny 574 | gavin 575 | emery 576 | romeo 577 | jayson 578 | dion 579 | dante 580 | clement 581 | coy 582 | odell 583 | jarvis 584 | bruno 585 | issac 586 | dudley 587 | sanford 588 | colby 589 | carmelo 590 | nestor 591 | hollis 592 | stefan 593 | donny 594 | linwood 595 | beau 596 | weldon 597 | galen 598 | isidro 599 | truman 600 | delmar 601 | johnathon 602 | silas 603 | frederic 604 | irwin 605 | merrill 606 | charley 607 | marcelino 608 | carlo 609 | trenton 610 | kurtis 611 | aurelio 612 | winfred 613 | vito 614 | collin 615 | denver 616 | leonel 617 | emory 618 | pasquale 619 | mohammad 620 | mariano 621 | danial 622 | landon 623 | dirk 624 | branden 625 | adan 626 | numbers 627 | clair 628 | buford 629 | bernie 630 | wilmer 631 | emerson 632 | zachery 633 | jacques 634 | errol 635 | josue 636 | edwardo 637 | wilford 638 | theron 639 | raymundo 640 | daren 641 | tristan 642 | robby 643 | lincoln 644 | jame 645 | genaro 646 | octavio 647 | cornell 648 | hung 649 | arron 650 | antony 651 | herschel 652 | alva 653 | giovanni 654 | garth 655 | cyrus 656 | cyril 657 | ronny 658 | stevie 659 | lon 660 | kennith 661 | carmine 662 | augustine 663 | erich 664 | chadwick 665 | wilburn 666 | russ 667 | myles 668 | jonas 669 | mitchel 670 | mervin 671 | zane 672 | jamel 673 | lazaro 674 | alphonse 675 | randell 676 | johnie 677 | jarrett 678 | ariel 679 | abdul 680 | dusty 681 | luciano 682 | seymour 683 | scottie 684 | eugenio 685 | mohammed 686 | arnulfo 687 | lucien 688 | ferdinand 689 | thad 690 | ezra 691 | aldo 692 | rubin 693 | mitch 694 | earle 695 | abe 696 | marquis 697 | lanny 698 | kareem 699 | jamar 700 | boris 701 | isiah 702 | emile 703 | elmo 704 | aron 705 | leopoldo 706 | everette 707 | josef 708 | eloy 709 | dorian 710 | rodrick 711 | reinaldo 712 | lucio 713 | jerrod 714 | weston 715 | hershel 716 | lemuel 717 | lavern 718 | burt 719 | jules 720 | gil 721 | eliseo 722 | ahmad 723 | nigel 724 | efren 725 | antwan 726 | alden 727 | margarito 728 | refugio 729 | dino 730 | osvaldo 731 | les 732 | deandre 733 | normand 734 | kieth 735 | ivory 736 | trey 737 | norberto 738 | napoleon 739 | jerold 740 | fritz 741 | rosendo 742 | milford 743 | sang 744 | deon 745 | christoper 746 | alfonzo 747 | lyman 748 | josiah 749 | brant 750 | wilton 751 | rico 752 | jamaal 753 | dewitt 754 | brenton 755 | yong 756 | olin 757 | faustino 758 | claudio 759 | judson 760 | gino 761 | edgardo 762 | alec 763 | jarred 764 | donn 765 | trinidad 766 | tad 767 | porfirio 768 | odis 769 | lenard 770 | chauncey 771 | tod 772 | mel 773 | marcelo 774 | kory 775 | augustus 776 | keven 777 | hilario 778 | bud 779 | sal 780 | orval 781 | mauro 782 | dannie 783 | zachariah 784 | olen 785 | anibal 786 | milo 787 | jed 788 | thanh 789 | amado 790 | lenny 791 | tory 792 | richie 793 | horacio 794 | brice 795 | mohamed 796 | delmer 797 | dario 798 | mac 799 | jonah 800 | jerrold 801 | robt 802 | hank 803 | sung 804 | rupert 805 | rolland 806 | kenton 807 | damion 808 | chi 809 | antone 810 | waldo 811 | fredric 812 | bradly 813 | kip 814 | burl 815 | tyree 816 | jefferey 817 | ahmed 818 | willy 819 | stanford 820 | oren 821 | moshe 822 | mikel 823 | enoch 824 | brendon 825 | quintin 826 | jamison 827 | florencio 828 | darrick 829 | tobias 830 | minh 831 | hassan 832 | giuseppe 833 | demarcus 834 | cletus 835 | tyrell 836 | lyndon 837 | keenan 838 | werner 839 | theo 840 | geraldo 841 | columbus 842 | chet 843 | bertram 844 | markus 845 | huey 846 | hilton 847 | dwain 848 | donte 849 | tyron 850 | omer 851 | isaias 852 | hipolito 853 | fermin 854 | chung 855 | adalberto 856 | jamey 857 | teodoro 858 | mckinley 859 | maximo 860 | raleigh 861 | lawerence 862 | abram 863 | rashad 864 | emmitt 865 | daron 866 | chong 867 | samual 868 | otha 869 | miquel 870 | eusebio 871 | dong 872 | domenic 873 | darron 874 | wilber 875 | renato 876 | hoyt 877 | haywood 878 | ezekiel 879 | chas 880 | florentino 881 | elroy 882 | clemente 883 | arden 884 | neville 885 | edison 886 | deshawn 887 | carrol 888 | shayne 889 | nathanial 890 | jordon 891 | danilo 892 | claud 893 | sherwood 894 | raymon 895 | rayford 896 | cristobal 897 | ambrose 898 | titus 899 | hyman 900 | felton 901 | ezequiel 902 | erasmo 903 | lonny 904 | milan 905 | lino 906 | jarod 907 | herb 908 | andreas 909 | rhett 910 | jude 911 | douglass 912 | cordell 913 | oswaldo 914 | ellsworth 915 | virgilio 916 | toney 917 | nathanael 918 | benedict 919 | mose 920 | hong 921 | isreal 922 | garret 923 | fausto 924 | arlen 925 | zack 926 | modesto 927 | francesco 928 | manual 929 | gaylord 930 | gaston 931 | filiberto 932 | deangelo 933 | michale 934 | granville 935 | malik 936 | zackary 937 | tuan 938 | nicky 939 | cristopher 940 | antione 941 | malcom 942 | korey 943 | jospeh 944 | colton 945 | waylon 946 | hosea 947 | shad 948 | santo 949 | rudolf 950 | rolf 951 | renaldo 952 | marcellus 953 | lucius 954 | kristofer 955 | harland 956 | arnoldo 957 | rueben 958 | leandro 959 | kraig 960 | jerrell 961 | jeromy 962 | hobert 963 | cedrick 964 | arlie 965 | winford 966 | wally 967 | luigi 968 | keneth 969 | jacinto 970 | graig 971 | franklyn 972 | edmundo 973 | leif 974 | jeramy 975 | willian 976 | vincenzo 977 | shon 978 | michal 979 | lynwood 980 | jere 981 | elden 982 | darell 983 | broderick 984 | alonso 985 | -------------------------------------------------------------------------------- /zxcvbn-core/Feedback.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn 6 | { 7 | /// 8 | /// Generates feedback based on the match results. 9 | /// 10 | internal static class Feedback 11 | { 12 | private static readonly FeedbackItem DefaultFeedback = new FeedbackItem 13 | { 14 | Warning = string.Empty, 15 | Suggestions = new[] 16 | { 17 | "Use a few words, avoid common phrases", 18 | "No need for symbols, digits, or uppercase letters", 19 | }, 20 | }; 21 | 22 | /// 23 | /// Gets feedback based on the provided score and matches. 24 | /// 25 | /// The score to assess. 26 | /// The sequence of matches to assess. 27 | /// Any warnings and suggestiongs about the password matches. 28 | public static FeedbackItem GetFeedback(int score, IEnumerable sequence) 29 | { 30 | if (!sequence.Any()) 31 | return DefaultFeedback; 32 | 33 | if (score > 2) 34 | { 35 | return new FeedbackItem 36 | { 37 | Warning = string.Empty, 38 | Suggestions = new List(), 39 | }; 40 | } 41 | 42 | var longestMatch = sequence.OrderBy(c => c.Token.Length).Last(); 43 | 44 | var feedback = GetMatchFeedback(longestMatch, sequence.Count() == 1); 45 | var extraFeedback = "Add another word or two. Uncommon words are better."; 46 | 47 | if (feedback != null) 48 | { 49 | feedback.Suggestions.Insert(0, extraFeedback); 50 | } 51 | else 52 | { 53 | feedback = new FeedbackItem 54 | { 55 | Warning = string.Empty, 56 | Suggestions = new List { extraFeedback }, 57 | }; 58 | } 59 | 60 | return feedback; 61 | } 62 | 63 | private static FeedbackItem GetDictionaryMatchFeedback(DictionaryMatch match, bool isSoleMatch) 64 | { 65 | var warning = string.Empty; 66 | 67 | if (match.DictionaryName == "passwords") 68 | { 69 | if (isSoleMatch && !match.L33t && !match.Reversed) 70 | { 71 | if (match.Rank <= 10) 72 | warning = "This is a top-10 common password"; 73 | else if (match.Rank <= 100) 74 | warning = "This is a top-100 common password"; 75 | else 76 | warning = "This is a very common password"; 77 | } 78 | else if (match.GuessesLog10 <= 4) 79 | { 80 | warning = "This is similar to a commonly used password"; 81 | } 82 | } 83 | else if (match.DictionaryName == "english" && isSoleMatch) 84 | { 85 | warning = "A word by itself is easy to guess"; 86 | } 87 | else if (match.DictionaryName == "surnames" || match.DictionaryName == "male_names" || match.DictionaryName == "female_names") 88 | { 89 | if (isSoleMatch) 90 | warning = "Names and surnames by themselves are easy to guess"; 91 | else 92 | warning = "Common names and surnames are easy to guess"; 93 | } 94 | 95 | var suggestions = new List(); 96 | var word = match.Token; 97 | if (char.IsUpper(word[0])) 98 | suggestions.Add("Capitalization doesn't help very much"); 99 | else if (word.All(c => char.IsUpper(c)) && word.ToLower() != word) 100 | suggestions.Add("All-uppercase is almost as easy to guess as all-lowercase"); 101 | 102 | if (match.Reversed && match.Token.Length >= 4) 103 | suggestions.Add("Reversed words aren't much harder to guess"); 104 | if (match.L33t) 105 | suggestions.Add("Predictable substitutions like '@' instead of 'a' don't help very much"); 106 | 107 | return new FeedbackItem 108 | { 109 | Suggestions = suggestions, 110 | Warning = warning, 111 | }; 112 | } 113 | 114 | private static FeedbackItem GetMatchFeedback(Match match, bool isSoleMatch) 115 | { 116 | switch (match.Pattern) 117 | { 118 | case "dictionary": 119 | return GetDictionaryMatchFeedback(match as DictionaryMatch, isSoleMatch); 120 | 121 | case "spatial": 122 | return new FeedbackItem 123 | { 124 | Warning = (match as SpatialMatch).Turns == 1 ? "Straight rows of keys are easy to guess" : "Short keyboard patterns are easy to guess", 125 | Suggestions = new List 126 | { 127 | "Use a longer keyboard pattern with more turns", 128 | }, 129 | }; 130 | 131 | case "repeat": 132 | return new FeedbackItem 133 | { 134 | Warning = (match as RepeatMatch).BaseToken.Length == 1 ? "Repeats like 'aaa' are easy to guess" : "Repeats like 'abcabcabc' are only slightly harder to guess than 'abc'", 135 | Suggestions = new List 136 | { 137 | "Avoid repeated words and characters", 138 | }, 139 | }; 140 | 141 | case "regex": 142 | if ((match as RegexMatch).RegexName == "recent_year") 143 | { 144 | return new FeedbackItem 145 | { 146 | Warning = "Recent years are easy to guess", 147 | Suggestions = new List 148 | { 149 | "Avoid recent years", 150 | "Avoid years that are associated with you", 151 | }, 152 | }; 153 | } 154 | 155 | break; 156 | 157 | case "date": 158 | return new FeedbackItem 159 | { 160 | Warning = "Dates are often easy to guess", 161 | Suggestions = new List 162 | { 163 | "Avoid dates and years that are associated with you", 164 | }, 165 | }; 166 | } 167 | 168 | return null; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /zxcvbn-core/FeedbackItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Zxcvbn 4 | { 5 | /// 6 | /// Represents feedback that can be presented to the user. 7 | /// 8 | public class FeedbackItem 9 | { 10 | /// 11 | /// Gets the list of suggestions that can be presented. 12 | /// 13 | public IList Suggestions { get; internal set; } 14 | 15 | /// 16 | /// Gets the warning that should be the headline for the user. 17 | /// 18 | public string Warning { get; internal set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /zxcvbn-core/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "To match this project's coding style.")] 9 | [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "To match this project's coding style.")] 10 | [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "To match this project's coding style.")] 11 | [assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "This normalization isn't security critical")] 12 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "No header required.")] 13 | [assembly: SuppressMessage("Globalization", "CA1304:Specify CultureInfo", Justification = "Not supported in all the desired target libraries.")] 14 | [assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "Not supported in all the desired target libraries.")] 15 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/DateMatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | using Zxcvbn.Matcher.Matches; 8 | 9 | namespace Zxcvbn.Matcher 10 | { 11 | /// 12 | /// Attempts to match a string with a date. 13 | /// 14 | /// 15 | /// A date is recognised if it is: 16 | /// 17 | /// Any 3 tuple that starts or ends with a 2- or 4- digit year 18 | /// With 2 or 0 separator characters 19 | /// May be zero padded 20 | /// Has a month between 1 and 12 21 | /// Has a day between 1 and 31 22 | /// 23 | /// 24 | /// This isn't true date parsing. Invalid dates like 31 February will be allowed. 25 | /// 26 | internal class DateMatcher : IMatcher 27 | { 28 | private const int MaxYear = 2050; 29 | private const int MinYear = 1000; 30 | 31 | private static readonly ReadOnlyDictionary DateSplits = new ReadOnlyDictionary(new Dictionary 32 | { 33 | [4] = new[] 34 | { 35 | new[] { 1, 2 }, // 1 1 91 36 | new[] { 2, 3 }, // 91 1 1 37 | }, 38 | [5] = new[] 39 | { 40 | new[] { 1, 3 }, // 1 11 91 41 | new[] { 2, 3 }, // 11 1 91 42 | }, 43 | [6] = new[] 44 | { 45 | new[] { 1, 2 }, // 1 1 1991 46 | new[] { 2, 4 }, // 11 11 91 47 | new[] { 4, 5 }, // 1991 1 1 48 | }, 49 | [7] = new[] 50 | { 51 | new[] { 1, 3 }, // 1 11 1991 52 | new[] { 2, 3 }, // 11 1 1991 53 | new[] { 4, 5 }, // 1991 1 11 54 | new[] { 4, 6 }, // 1991 11 1 55 | }, 56 | [8] = new[] 57 | { 58 | new[] { 2, 4 }, // 11 11 1991 59 | new[] { 4, 6 }, // 1991 11 11 60 | }, 61 | }); 62 | 63 | private readonly Regex dateWithNoSeperater = new Regex("^\\d{4,8}$", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); 64 | 65 | private readonly Regex dateWithSeperator = new Regex( 66 | @"^( \d{1,4} ) # day or month 67 | ( [\s/\\_.-] ) # separator 68 | ( \d{1,2} ) # month or day 69 | \2 # same separator 70 | ( \d{1,4} ) # year 71 | $", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); 72 | 73 | /// 74 | /// Gets the reference year used to check for recent dates. 75 | /// 76 | public static int ReferenceYear { get; } = DateTime.Now.Year; 77 | 78 | /// 79 | /// Find date matches in . 80 | /// 81 | /// The passsord to check. 82 | /// An enumerable of date matches. 83 | public IEnumerable MatchPassword(string password) 84 | { 85 | var matches = new List(); 86 | 87 | for (var i = 0; i <= password.Length - 4; i++) 88 | { 89 | for (var j = 4; i + j <= password.Length; j++) 90 | { 91 | var dateMatch = dateWithNoSeperater.Match(password); // Slashless dates 92 | if (!dateMatch.Success) 93 | continue; 94 | 95 | var candidates = new List(); 96 | 97 | foreach (var split in DateSplits[dateMatch.Length]) 98 | { 99 | var l = split[0]; 100 | var m = split[1]; 101 | var kLength = l; 102 | var lLength = m - l; 103 | 104 | var date = MapIntsToDate(new[] 105 | { 106 | int.Parse(dateMatch.Value.Substring(0, kLength), CultureInfo.InvariantCulture), 107 | int.Parse(dateMatch.Value.Substring(l, lLength), CultureInfo.InvariantCulture), 108 | int.Parse(dateMatch.Value.Substring(m), CultureInfo.InvariantCulture), 109 | }); 110 | 111 | if (date != null) 112 | candidates.Add(date.Value); 113 | } 114 | 115 | if (candidates.Count == 0) 116 | continue; 117 | 118 | var bestCandidate = candidates[0]; 119 | 120 | int Metric(LooseDate c) => Math.Abs(c.Year - ReferenceYear); 121 | 122 | var minDistance = Metric(bestCandidate); 123 | 124 | foreach (var candidate in candidates.Skip(1)) 125 | { 126 | var distance = Metric(candidate); 127 | if (distance < minDistance) 128 | { 129 | minDistance = distance; 130 | bestCandidate = candidate; 131 | } 132 | } 133 | 134 | matches.Add(new DateMatch 135 | { 136 | Token = dateMatch.Value, 137 | i = i, 138 | j = j + i - 1, 139 | Separator = string.Empty, 140 | Year = bestCandidate.Year, 141 | Month = bestCandidate.Month, 142 | Day = bestCandidate.Day, 143 | }); 144 | } 145 | } 146 | 147 | for (var i = 0; i <= password.Length - 6; i++) 148 | { 149 | for (var j = 6; i + j <= password.Length; j++) 150 | { 151 | var token = password.Substring(i, j); 152 | var match = dateWithSeperator.Match(token); 153 | 154 | if (!match.Success) 155 | continue; 156 | 157 | var date = MapIntsToDate(new[] 158 | { 159 | int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture), 160 | int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture), 161 | int.Parse(match.Groups[4].Value, CultureInfo.InvariantCulture), 162 | }); 163 | 164 | if (date == null) 165 | continue; 166 | 167 | var m = new DateMatch 168 | { 169 | Token = token, 170 | i = i, 171 | j = j + i - 1, 172 | Separator = match.Groups[2].Value, 173 | Year = date.Value.Year, 174 | Month = date.Value.Month, 175 | Day = date.Value.Day, 176 | }; 177 | 178 | matches.Add(m); 179 | } 180 | } 181 | 182 | var filteredMatches = matches.Where(m => 183 | { 184 | foreach (var n in matches) 185 | { 186 | if (m == n) 187 | continue; 188 | if (n.i <= m.i && n.j >= m.j) 189 | return false; 190 | } 191 | 192 | return true; 193 | }); 194 | 195 | return filteredMatches; 196 | } 197 | 198 | private static LooseDate? MapIntsToDate(IList vals) 199 | { 200 | if (vals[1] > 31 || vals[1] < 1) 201 | return null; 202 | 203 | var over12 = 0; 204 | var over31 = 0; 205 | var under1 = 0; 206 | 207 | foreach (var i in vals) 208 | { 209 | if ((i > 99 && i < MinYear) || i > MaxYear) 210 | return null; 211 | 212 | if (i > 31) 213 | over31++; 214 | if (i > 12) 215 | over12++; 216 | if (i < 1) 217 | under1++; 218 | } 219 | 220 | if (over31 >= 2 || over12 == 3 || under1 >= 2) 221 | return null; 222 | 223 | var possibleSplits = new[] 224 | { 225 | new[] { vals[2], vals[0], vals[1] }, 226 | new[] { vals[0], vals[1], vals[2] }, 227 | }; 228 | 229 | foreach (var possibleSplit in possibleSplits) 230 | { 231 | if (possibleSplit[0] < MinYear || possibleSplit[0] > MaxYear) 232 | continue; 233 | 234 | var dayMonth = MapIntsToDayMonth(new[] { possibleSplit[1], possibleSplit[2] }); 235 | if (dayMonth != null) 236 | return new LooseDate(possibleSplit[0], dayMonth.Value.Month, dayMonth.Value.Day); 237 | return null; 238 | } 239 | 240 | foreach (var possibleSplit in possibleSplits) 241 | { 242 | var dayMonth = MapIntsToDayMonth(new[] { possibleSplit[1], possibleSplit[2] }); 243 | if (dayMonth == null) continue; 244 | var year = TwoToFourDigitYear(possibleSplit[0]); 245 | return new LooseDate(year, dayMonth.Value.Month, dayMonth.Value.Day); 246 | } 247 | 248 | return null; 249 | } 250 | 251 | private static LooseDate? MapIntsToDayMonth(IList vals) 252 | { 253 | var day = vals[0]; 254 | var month = vals[1]; 255 | 256 | if (day >= 1 && day <= 31 && month >= 1 && month <= 12) 257 | return new LooseDate(0, month, day); 258 | 259 | day = vals[1]; 260 | month = vals[0]; 261 | 262 | if (day >= 1 && day <= 31 && month >= 1 && month <= 12) 263 | return new LooseDate(0, month, day); 264 | 265 | return null; 266 | } 267 | 268 | private static int TwoToFourDigitYear(int year) 269 | { 270 | if (year > 99) 271 | return year; 272 | if (year > 50) 273 | return year + 1900; 274 | return year + 2000; 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/DictionaryMatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Matcher 8 | { 9 | /// 10 | /// Attempts to match a string with a list of words. 11 | /// 12 | /// 13 | /// This matcher reads in a list of words (in frequency order) either from a built in resource, an external file 14 | /// or a list of strings. These must be in decreasing frequency order and contain one word per line with no additional information. 15 | /// 16 | internal class DictionaryMatcher : IMatcher 17 | { 18 | private readonly string dictionaryName; 19 | private readonly Dictionary rankedDictionary; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// The name provided to the dictionary used. 25 | /// The filename of the dictionary (full or relative path) or name of built-in dictionary. 26 | public DictionaryMatcher(string name, string wordListPath) 27 | { 28 | dictionaryName = name; 29 | rankedDictionary = BuildRankedDictionary(wordListPath); 30 | } 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// Creates a new dictionary matcher from the passed in word list. If there is any frequency order then they should be in 35 | /// decreasing frequency order. 36 | /// 37 | /// The name provided to the dictionary used. 38 | /// The words to add to the dictionary. 39 | public DictionaryMatcher(string name, IEnumerable wordList) 40 | { 41 | dictionaryName = name; 42 | 43 | // Must ensure that the dictionary is using lowercase words only 44 | rankedDictionary = BuildRankedDictionary(wordList.Select(w => w.ToLower())); 45 | } 46 | 47 | /// 48 | /// Find dictionary matches in . 49 | /// 50 | /// The password to check. 51 | /// An enumerable of dictionary matches. 52 | public virtual IEnumerable MatchPassword(string password) 53 | { 54 | var passwordLower = password.ToLower(); 55 | var length = passwordLower.Length; 56 | var matches = new List(); 57 | 58 | for (var i = 0; i < length; i++) 59 | { 60 | for (var j = i; j < length; j++) 61 | { 62 | var passwordSub = passwordLower.Substring(i, j - i + 1); 63 | if (rankedDictionary.ContainsKey(passwordSub)) 64 | { 65 | var match = new DictionaryMatch 66 | { 67 | i = i, 68 | j = j, 69 | Token = password.Substring(i, j - i + 1), 70 | MatchedWord = passwordSub, 71 | Rank = rankedDictionary[passwordSub], 72 | DictionaryName = dictionaryName, 73 | Reversed = false, 74 | L33t = false, 75 | }; 76 | 77 | matches.Add(match); 78 | } 79 | } 80 | } 81 | 82 | return matches.OrderBy(m => m.i).ThenBy(m => m.j); 83 | } 84 | 85 | /// 86 | /// Loads the file . 87 | /// 88 | /// 89 | /// If the file is embedded in the assembly, this is loaded, otherwise is treated as a file path. 90 | /// Path to word list. 91 | /// A dictionary of the words and their associated rank. 92 | private static Dictionary BuildRankedDictionary(string wordListFile) 93 | { 94 | var lines = Utility.GetEmbeddedResourceLines($"Zxcvbn.Dictionaries.{wordListFile}") ?? File.ReadAllLines(wordListFile); 95 | 96 | return BuildRankedDictionary(lines); 97 | } 98 | 99 | private static Dictionary BuildRankedDictionary(IEnumerable wordList) 100 | { 101 | var dict = new Dictionary(); 102 | 103 | var i = 1; 104 | foreach (var word in wordList) 105 | { 106 | var actualWord = word; 107 | if (actualWord.Contains(" ")) 108 | actualWord = actualWord.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries)[0]; 109 | 110 | // The word list is assumed to be in increasing frequency order 111 | dict[actualWord] = i++; 112 | } 113 | 114 | return dict; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/IMatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Zxcvbn.Matcher.Matches; 3 | 4 | namespace Zxcvbn.Matcher 5 | { 6 | /// 7 | /// Represents a class that can look for matches in a password. 8 | /// 9 | internal interface IMatcher 10 | { 11 | /// 12 | /// Find matches in . 13 | /// 14 | /// The password to check. 15 | /// An enumerable of the matches. 16 | IEnumerable MatchPassword(string password); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/L33tMatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Matcher 8 | { 9 | /// 10 | /// Attempts to match a string with a list of words, considering common l33t substitutions. 11 | /// 12 | internal class L33tMatcher : IMatcher 13 | { 14 | private readonly IEnumerable dictionaryMatchers; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// Create a l33t matcher that applies substitutions and then matches agains the passed in list of dictionary matchers. 19 | /// 20 | /// The list of dictionary matchers to check transformed passwords against. 21 | public L33tMatcher(IEnumerable dictionaryMatchers) 22 | { 23 | this.dictionaryMatchers = dictionaryMatchers; 24 | } 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// Create a l33t matcher that applies substitutions and then matches agains a single dictionary matcher. 29 | /// 30 | /// The dictionary matcher to check transformed passwords against. 31 | public L33tMatcher(DictionaryMatcher dictionaryMatcher) 32 | : this(new List { dictionaryMatcher }) 33 | { 34 | } 35 | 36 | /// 37 | /// Gets or sets the table of l33t transforms. 38 | /// 39 | internal static ReadOnlyDictionary L33tTable { get; set; } = new ReadOnlyDictionary(new Dictionary 40 | { 41 | ['a'] = new[] { '4', '@' }, 42 | ['b'] = new[] { '8' }, 43 | ['c'] = new[] { '(', '{', '[', '<' }, 44 | ['e'] = new[] { '3' }, 45 | ['g'] = new[] { '6', '9' }, 46 | ['i'] = new[] { '1', '!', '|' }, 47 | ['l'] = new[] { '1', '|', '7' }, 48 | ['o'] = new[] { '0' }, 49 | ['s'] = new[] { '$', '5' }, 50 | ['t'] = new[] { '+', '7' }, 51 | ['x'] = new[] { '%' }, 52 | ['z'] = new[] { '2' }, 53 | }); 54 | 55 | /// 56 | /// Find l33t dictionary matches in . 57 | /// 58 | /// The password to check. 59 | /// An enumerable of dictionary matches. 60 | public IEnumerable MatchPassword(string password) 61 | { 62 | var result = new List(); 63 | 64 | foreach (var sub in EnumerateSubtitutions(RelevantL33tSubtable(password))) 65 | { 66 | if (!sub.Any()) 67 | break; 68 | 69 | var subbedPassword = TranslateString(sub, password); 70 | 71 | foreach (var matcher in dictionaryMatchers) 72 | { 73 | foreach (DictionaryMatch match in matcher.MatchPassword(subbedPassword)) 74 | { 75 | var token = password.Substring(match.i, match.j - match.i + 1); 76 | if (token.ToLower().Equals(match.MatchedWord.ToLower())) 77 | continue; 78 | 79 | var matchSub = new Dictionary(); 80 | 81 | foreach (var subbedChar in sub.Keys) 82 | { 83 | var chr = sub[subbedChar]; 84 | if (token.Contains(subbedChar)) 85 | matchSub[subbedChar] = chr; 86 | } 87 | 88 | match.L33t = true; 89 | match.Token = token; 90 | match.L33tSubs.Clear(); 91 | foreach (var key in matchSub.Keys) 92 | match.L33tSubs[key] = matchSub[key]; 93 | 94 | result.Add(match); 95 | } 96 | } 97 | } 98 | 99 | return result.Where(m => m.Token.Length > 1).OrderBy(m => m.i).ThenBy(m => m.j); 100 | } 101 | 102 | /// 103 | /// Enumerates the subtitutions in the provided table. 104 | /// 105 | /// The table to get the enumerations from. 106 | /// The enumeration of possible substitutions. 107 | internal static IEnumerable> EnumerateSubtitutions(ReadOnlyDictionary table) 108 | { 109 | return Helper(table.Keys, table).Select(s => 110 | { 111 | var subDictionary = new Dictionary(); 112 | foreach (var item in s) 113 | { 114 | var l33tChar = item.Item1; 115 | var chr = item.Item2; 116 | subDictionary[l33tChar] = chr; 117 | } 118 | 119 | return subDictionary; 120 | }); 121 | } 122 | 123 | /// 124 | /// Prunes the L33T subtable to only the relevant bits. 125 | /// 126 | /// The password to consider. 127 | /// The pruned l33t table. 128 | internal static ReadOnlyDictionary RelevantL33tSubtable(string password) 129 | { 130 | var subtable = new Dictionary(); 131 | 132 | foreach (var c in L33tTable.Keys) 133 | { 134 | var relevantSubs = L33tTable[c].Where(s => password.Contains(s)); 135 | if (relevantSubs.Any()) 136 | subtable[c] = relevantSubs.ToArray(); 137 | } 138 | 139 | return new ReadOnlyDictionary(subtable); 140 | } 141 | 142 | private static List>> Deduplicate(List>> subs) 143 | { 144 | var result = new List>>(); 145 | var members = new HashSet(); 146 | 147 | foreach (var sub in subs) 148 | { 149 | var label = string.Join("-", sub 150 | .Select((kvp, i) => new Tuple, int>(kvp, i)) 151 | .OrderBy(i => i.ToString()) 152 | .Select((kvp, i) => kvp.ToString())); 153 | 154 | if (!members.Contains(label)) 155 | { 156 | members.Add(label); 157 | result.Add(sub); 158 | } 159 | } 160 | 161 | return result; 162 | } 163 | 164 | private static List>> Helper(IEnumerable keys, ReadOnlyDictionary table, List>> subs = null) 165 | { 166 | if (subs == null) 167 | { 168 | subs = new List>> 169 | { 170 | new List>(), 171 | }; 172 | } 173 | 174 | if (!keys.Any()) 175 | return subs; 176 | 177 | var firstKey = keys.First(); 178 | var restOfKeys = keys.Skip(1); 179 | 180 | var nextSubs = new List>>(); 181 | 182 | foreach (var l33tChr in table[firstKey]) 183 | { 184 | foreach (var sub in subs) 185 | { 186 | var dupL33tIndex = sub.FindIndex(s => s.Item1 == l33tChr); 187 | 188 | if (dupL33tIndex != -1) 189 | nextSubs.Add(sub); 190 | 191 | nextSubs.Add(new List>(sub) 192 | { 193 | new Tuple(l33tChr, firstKey), 194 | }); 195 | } 196 | } 197 | 198 | subs = Deduplicate(nextSubs); 199 | return Helper(restOfKeys, table, subs); 200 | } 201 | 202 | private static string TranslateString(IReadOnlyDictionary charMap, string str) 203 | { 204 | // Make substitutions from the character map wherever possible 205 | return new string(str.Select(c => charMap.ContainsKey(c) ? charMap[c] : c).ToArray()); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/LooseDate.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn.Matcher 2 | { 3 | /// 4 | /// Represents a possible date without the limitations of DateTime. 5 | /// 6 | internal struct LooseDate 7 | { 8 | /// 9 | /// Initializes a new instance of the struct. 10 | /// 11 | /// The year. 12 | /// The month. 13 | /// The day. 14 | public LooseDate(int year, int month, int day) 15 | { 16 | Year = year; 17 | Month = month; 18 | Day = day; 19 | } 20 | 21 | /// 22 | /// Gets the day in the date. 23 | /// 24 | public int Day { get; } 25 | 26 | /// 27 | /// Gets the month in the date. 28 | /// 29 | public int Month { get; } 30 | 31 | /// 32 | /// Gets the year in the date. 33 | /// 34 | public int Year { get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Matches/BruteForceMatch.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn.Matcher.Matches 2 | { 3 | /// 4 | /// A match used to evaluate the brute-force strength of a password. 5 | /// 6 | /// 7 | public class BruteForceMatch : Match 8 | { 9 | /// 10 | /// Gets the name of the pattern matcher used to generate this match. 11 | /// 12 | public override string Pattern => "bruteforce"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Matches/DateMatch.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn.Matcher.Matches 2 | { 3 | /// 4 | /// A match identified as a possible date. 5 | /// 6 | /// 7 | public class DateMatch : Match 8 | { 9 | /// 10 | /// Gets the detected day. 11 | /// 12 | public int Day { get; internal set; } 13 | 14 | /// 15 | /// Gets the detected month. 16 | /// 17 | public int Month { get; internal set; } 18 | 19 | /// 20 | /// Gets the name of the pattern matcher used to generate this match. 21 | /// 22 | public override string Pattern => "date"; 23 | 24 | /// 25 | /// Gets the separator detected in the date. 26 | /// 27 | /// 28 | /// If there is no separator then this will be an empty string. 29 | /// 30 | public string Separator { get; internal set; } 31 | 32 | /// 33 | /// Gets the detected year. 34 | /// 35 | public int Year { get; internal set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Matches/DictionaryMatch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace Zxcvbn.Matcher.Matches 5 | { 6 | /// 7 | /// A match identified as being in one of the provided dictionaries. 8 | /// 9 | /// 10 | public class DictionaryMatch : Match 11 | { 12 | /// 13 | /// Gets the base guesses associated with the matched word. 14 | /// 15 | public double BaseGuesses { get; internal set; } 16 | 17 | /// 18 | /// Gets the name of the dictionary containing the matched word. 19 | /// 20 | public string DictionaryName { get; internal set; } 21 | 22 | /// 23 | /// Gets a value indicating whether the matched word was found with l33t spelling. 24 | /// 25 | public bool L33t { get; internal set; } 26 | 27 | /// 28 | /// Gets the number of L33T variations associated with this match. 29 | /// 30 | public double L33tVariations { get; internal set; } 31 | 32 | /// 33 | /// Gets the dictionary word matched to. 34 | /// 35 | public string MatchedWord { get; internal set; } 36 | 37 | /// 38 | /// Gets the name of the pattern matcher used to generate this match. 39 | /// 40 | public override string Pattern => "dictionary"; 41 | 42 | /// 43 | /// Gets the rank of the matched word in the dictionary. 44 | /// 45 | /// 46 | /// The most frequent word is has a rank of 1, with less frequent words having higher ranks. 47 | /// 48 | public int Rank { get; internal set; } 49 | 50 | /// 51 | /// Gets a value indicating whether the matched word was reversed. 52 | /// 53 | public bool Reversed { get; internal set; } 54 | 55 | /// 56 | /// Gets the l33t character mappings that are in use for this match. 57 | /// 58 | public IReadOnlyDictionary Sub => new ReadOnlyDictionary(L33tSubs); 59 | 60 | /// 61 | /// Gets the number of uppercase variations associated with this match. 62 | /// 63 | public double UppercaseVariations { get; internal set; } 64 | 65 | /// 66 | /// Gets or sets the l33t character mappings that are in use for this match. 67 | /// 68 | /// 69 | /// Modifiable version of Sub. 70 | /// 71 | internal Dictionary L33tSubs { get; set; } = new Dictionary(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Matches/Match.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Zxcvbn.Matcher.Matches 4 | { 5 | /// 6 | /// A match identified by zxcvbn. 7 | /// 8 | public abstract class Match 9 | { 10 | /// 11 | /// Gets the number of guesses associated with this match. 12 | /// 13 | public double Guesses { get; internal set; } 14 | 15 | /// 16 | /// Gets log10(number of guesses) associated with this match. 17 | /// 18 | public double GuessesLog10 => Math.Log10(Guesses); 19 | 20 | #pragma warning disable IDE1006 // Naming Styles 21 | #pragma warning disable SA1300 // Element should begin with upper-case letter 22 | 23 | /// 24 | /// Gets the start index in the password string of the matched token. 25 | /// 26 | public int i { get; internal set; } 27 | 28 | /// 29 | /// Gets the end index in the password string of the matched token. 30 | /// 31 | public int j { get; internal set; } 32 | 33 | #pragma warning restore SA1300 // Element should begin with upper-case letter 34 | #pragma warning restore IDE1006 // Naming Styles 35 | 36 | /// 37 | /// Gets the name of the pattern matcher used to generate this match. 38 | /// 39 | public abstract string Pattern { get; } 40 | 41 | /// 42 | /// Gets the portion of the password that was matched. 43 | /// 44 | public string Token { get; internal set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Matches/RegexMatch.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn.Matcher.Matches 2 | { 3 | /// 4 | /// A match identified as matching one of the provided regular expressions. 5 | /// 6 | public class RegexMatch : Match 7 | { 8 | /// 9 | /// Gets the name of the pattern matcher used to generate this match. 10 | /// 11 | public override string Pattern => "regex"; 12 | 13 | /// 14 | /// Gets the name of the regex that matched. 15 | /// 16 | public string RegexName { get; internal set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Matches/RepeatMatch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Zxcvbn.Matcher.Matches 4 | { 5 | /// 6 | /// A match identified as containing some repeated details. 7 | /// 8 | public class RepeatMatch : Match 9 | { 10 | /// 11 | /// Gets the base number guesses associated with the base matches. 12 | /// 13 | public double BaseGuesses { get; internal set; } 14 | 15 | /// 16 | /// Gets the base matches that are repeated. 17 | /// 18 | public IReadOnlyList BaseMatches => BaseMatchItems.AsReadOnly(); 19 | 20 | /// 21 | /// Gets the base repeated token. 22 | /// 23 | public string BaseToken { get; internal set; } 24 | 25 | /// 26 | /// Gets the name of the pattern matcher used to generate this match. 27 | /// 28 | public override string Pattern => "repeat"; 29 | 30 | /// 31 | /// Gets the number of times the base token is repeated. 32 | /// 33 | public int RepeatCount { get; internal set; } 34 | 35 | /// 36 | /// Gets or sets the base matches that are repeated. 37 | /// 38 | /// 39 | /// The editable backing of BaseMatches. 40 | /// 41 | internal List BaseMatchItems { get; set; } = new List(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Matches/SequenceMatch.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn.Matcher.Matches 2 | { 3 | /// 4 | /// A match identified as a predictable sequence of characters. 5 | /// 6 | public class SequenceMatch : Match 7 | { 8 | /// 9 | /// Gets a value indicating whether the match was found in ascending order (cdefg) or not (zyxw). 10 | /// 11 | public bool Ascending { get; internal set; } 12 | 13 | /// 14 | /// Gets the name of the pattern matcher used to generate this match. 15 | /// 16 | public override string Pattern => "sequence"; 17 | 18 | /// 19 | /// Gets the name of the sequence that the match was found in. 20 | /// 21 | public string SequenceName { get; internal set; } 22 | 23 | /// 24 | /// Gets the size of the sequence the pattern was found in. 25 | /// 26 | public int SequenceSpace { get; internal set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Matches/SpatialMatch.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn.Matcher.Matches 2 | { 3 | /// 4 | /// A match identified as a sequence of keys on a recognised keyboard. 5 | /// 6 | public class SpatialMatch : Match 7 | { 8 | /// 9 | /// Gets The name of the keyboard layout used to make the spatial match. 10 | /// 11 | public string Graph { get; internal set; } 12 | 13 | /// 14 | /// Gets the name of the pattern matcher used to generate this match. 15 | /// 16 | public override string Pattern => "spatial"; 17 | 18 | /// 19 | /// Gets the number of shifted characters matched in the pattern. 20 | /// 21 | public int ShiftedCount { get; internal set; } 22 | 23 | /// 24 | /// Gets the number of turns made. 25 | /// 26 | public int Turns { get; internal set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/Point.cs: -------------------------------------------------------------------------------- 1 | namespace Zxcvbn.Matcher 2 | { 3 | /// 4 | /// A local implementation of Point to avoid referring to the graphical libraries. 5 | /// 6 | internal struct Point 7 | { 8 | /// 9 | /// Initializes a new instance of the struct. 10 | /// 11 | /// The x coordinate. 12 | /// The y coordinate. 13 | public Point(int x, int y) 14 | { 15 | X = x; 16 | Y = y; 17 | } 18 | 19 | /// 20 | /// Gets the x coordinate of the point. 21 | /// 22 | public int X { get; } 23 | 24 | /// 25 | /// Gets the y coordinate of the point. 26 | /// 27 | public int Y { get; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/RegexMatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn.Matcher 6 | { 7 | /// 8 | /// Attempts to match a string with a pre-defined regular expressions. 9 | /// 10 | internal class RegexMatcher : IMatcher 11 | { 12 | private readonly string matcherName; 13 | private readonly Regex matchRegex; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// Create a new regex pattern matcher. 18 | /// 19 | /// The regex pattern to match. 20 | /// The name to give this matcher ('pattern' in resulting matches). 21 | public RegexMatcher(string pattern, string matcherName = "regex") 22 | : this(new Regex(pattern), matcherName) 23 | { 24 | } 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// Create a new regex pattern matcher. 29 | /// 30 | /// The regex object used to perform matching. 31 | /// The name to give this matcher ('pattern' in resulting matches). 32 | public RegexMatcher(Regex matchRegex, string matcherName = "regex") 33 | { 34 | this.matchRegex = matchRegex; 35 | this.matcherName = matcherName; 36 | } 37 | 38 | /// 39 | /// Find regex matches in . 40 | /// 41 | /// The password to check. 42 | /// An enumerable of regex matches. 43 | public IEnumerable MatchPassword(string password) 44 | { 45 | var reMatches = matchRegex.Matches(password); 46 | 47 | var pwMatches = new List(); 48 | 49 | foreach (System.Text.RegularExpressions.Match rem in reMatches) 50 | { 51 | pwMatches.Add(new RegexMatch 52 | { 53 | RegexName = matcherName, 54 | i = rem.Index, 55 | j = rem.Index + rem.Length - 1, 56 | Token = password.Substring(rem.Index, rem.Length), 57 | }); 58 | } 59 | 60 | return pwMatches; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/RepeatMatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn.Matcher 6 | { 7 | /// 8 | /// Attempts to match repeated characters in the string. 9 | /// 10 | /// 11 | /// Repeats must be more than two characters long to count. 12 | /// 13 | internal class RepeatMatcher : IMatcher 14 | { 15 | /// 16 | /// Find repeat matches in . 17 | /// 18 | /// The password to check. 19 | /// An enumerable of repeat matches. 20 | public IEnumerable MatchPassword(string password) 21 | { 22 | var matches = new List(); 23 | var greedy = "(.+)\\1+"; 24 | var lazy = "(.+?)\\1+"; 25 | var lazyAnchored = "^(.+?)\\1+$"; 26 | var lastIndex = 0; 27 | 28 | while (lastIndex < password.Length) 29 | { 30 | var greedyMatch = Regex.Match(password.Substring(lastIndex), greedy); 31 | var lazyMatch = Regex.Match(password.Substring(lastIndex), lazy); 32 | 33 | if (!greedyMatch.Success) break; 34 | 35 | System.Text.RegularExpressions.Match match; 36 | string baseToken; 37 | 38 | if (greedyMatch.Length > lazyMatch.Length) 39 | { 40 | match = greedyMatch; 41 | baseToken = Regex.Match(match.Value, lazyAnchored).Groups[1].Value; 42 | } 43 | else 44 | { 45 | match = lazyMatch; 46 | baseToken = match.Groups[1].Value; 47 | } 48 | 49 | var i = lastIndex + match.Index; 50 | var j = lastIndex + match.Index + match.Length - 1; 51 | 52 | var baseAnalysis = 53 | PasswordScoring.MostGuessableMatchSequence(baseToken, Core.GetAllMatches(baseToken)); 54 | 55 | var baseMatches = baseAnalysis.Sequence; 56 | var baseGuesses = baseAnalysis.Guesses; 57 | 58 | var m = new RepeatMatch 59 | { 60 | i = i, 61 | j = j, 62 | Token = match.Value, 63 | BaseToken = baseToken, 64 | BaseGuesses = baseGuesses, 65 | RepeatCount = match.Length / baseToken.Length, 66 | }; 67 | m.BaseMatchItems.AddRange(baseMatches); 68 | 69 | matches.Add(m); 70 | 71 | lastIndex = j + 1; 72 | } 73 | 74 | return matches; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/ReverseDictionaryMatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn.Matcher 6 | { 7 | /// 8 | /// Attempts to match a reversed string with a list of words. 9 | /// 10 | /// 11 | internal class ReverseDictionaryMatcher : DictionaryMatcher 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The name provided to the dictionary used. 17 | /// The filename of the dictionary (full or relative path) or name of built-in dictionary. 18 | public ReverseDictionaryMatcher(string name, string wordListPath) 19 | : base(name, wordListPath) 20 | { 21 | } 22 | 23 | /// 24 | /// Find reversed dictionary matches in . 25 | /// 26 | /// The password to check. 27 | /// An enumerable of dictionary matches. 28 | public override IEnumerable MatchPassword(string password) 29 | { 30 | var matches = base.MatchPassword(password.StringReverse()).ToList(); 31 | 32 | foreach (var m in matches) 33 | { 34 | if (!(m is DictionaryMatch ma)) 35 | continue; 36 | 37 | var i = ma.i; 38 | var j = ma.j; 39 | 40 | ma.Token = m.Token.StringReverse(); 41 | ma.Reversed = true; 42 | ma.i = password.Length - 1 - j; 43 | ma.j = password.Length - 1 - i; 44 | } 45 | 46 | return matches.OrderBy(m => m.i).ThenBy(m => m.j); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/SequenceMatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using Zxcvbn.Matcher.Matches; 6 | 7 | namespace Zxcvbn.Matcher 8 | { 9 | /// 10 | /// Attempts to match a string with a sequence of characters. 11 | /// 12 | internal class SequenceMatcher : IMatcher 13 | { 14 | private const int MaxDelta = 5; 15 | 16 | /// 17 | /// Find sequence matches in . 18 | /// 19 | /// The password to check. 20 | /// An enumerable of sequence matches. 21 | public IEnumerable MatchPassword(string password) 22 | { 23 | if (password.Length <= 1) 24 | return Enumerable.Empty(); 25 | 26 | var result = new List(); 27 | 28 | void Update(int i, int j, int delta) 29 | { 30 | if (j - i > 1 || Math.Abs(delta) == 1) 31 | { 32 | if (Math.Abs(delta) > 0 && Math.Abs(delta) <= MaxDelta) 33 | { 34 | var token = password.Substring(i, j - i + 1); 35 | string sequenceName; 36 | int sequenceSpace; 37 | 38 | if (Regex.IsMatch(token, "^[a-z]+$")) 39 | { 40 | sequenceName = "lower"; 41 | sequenceSpace = 26; 42 | } 43 | else if (Regex.IsMatch(token, "^[A-Z]+$")) 44 | { 45 | sequenceName = "upper"; 46 | sequenceSpace = 26; 47 | } 48 | else if (Regex.IsMatch(token, "^\\d+$")) 49 | { 50 | sequenceName = "digits"; 51 | sequenceSpace = 10; 52 | } 53 | else 54 | { 55 | sequenceName = "unicode"; 56 | sequenceSpace = 26; 57 | } 58 | 59 | result.Add(new SequenceMatch 60 | { 61 | i = i, 62 | j = j, 63 | Token = token, 64 | SequenceName = sequenceName, 65 | SequenceSpace = sequenceSpace, 66 | Ascending = delta > 0, 67 | }); 68 | } 69 | } 70 | } 71 | 72 | var iIn = 0; 73 | int? lastDelta = null; 74 | 75 | for (var k = 1; k < password.Length; k++) 76 | { 77 | var deltaIn = password[k] - password[k - 1]; 78 | if (lastDelta == null) 79 | lastDelta = deltaIn; 80 | if (deltaIn == lastDelta) 81 | continue; 82 | 83 | var jIn = k - 1; 84 | Update(iIn, jIn, lastDelta.Value); 85 | iIn = jIn; 86 | lastDelta = deltaIn; 87 | } 88 | 89 | Update(iIn, password.Length - 1, lastDelta.Value); 90 | return result; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/SpatialGraph.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | 6 | namespace Zxcvbn.Matcher 7 | { 8 | /// 9 | /// A graph detailing how keys are arranged on a typical keyboard. 10 | /// 11 | internal class SpatialGraph 12 | { 13 | /// 14 | /// Initializes a new instance of the class with the given name 15 | /// and based on the given layout. 16 | /// 17 | /// The name. 18 | /// The layout. 19 | /// if set to true the keys are slanted. 20 | public SpatialGraph(string name, string layout, bool slanted) 21 | { 22 | Name = name; 23 | BuildGraph(layout, slanted); 24 | } 25 | 26 | /// 27 | /// Gets the generated adjacency graph. 28 | /// 29 | public ReadOnlyDictionary> AdjacencyGraph { get; private set; } 30 | 31 | /// 32 | /// Gets the name of the generated graph. 33 | /// 34 | public string Name { get; } 35 | 36 | private static Point[] GetAlignedAdjacent(Point c) 37 | { 38 | var x = c.X; 39 | var y = c.Y; 40 | 41 | return new[] { new Point(x - 1, y), new Point(x - 1, y - 1), new Point(x, y - 1), new Point(x + 1, y - 1), new Point(x + 1, y), new Point(x + 1, y + 1), new Point(x, y + 1), new Point(x - 1, y + 1) }; 42 | } 43 | 44 | private static Point[] GetSlantedAdjacent(Point c) 45 | { 46 | var x = c.X; 47 | var y = c.Y; 48 | 49 | return new[] { new Point(x - 1, y), new Point(x, y - 1), new Point(x + 1, y - 1), new Point(x + 1, y), new Point(x, y + 1), new Point(x - 1, y + 1) }; 50 | } 51 | 52 | private void BuildGraph(string layout, bool slanted) 53 | { 54 | var tokens = layout.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); 55 | var tokenSize = tokens[0].Length; 56 | 57 | // Put the characters in each keyboard cell into the map agains t their coordinates 58 | var positionTable = new Dictionary(); 59 | var lines = layout.Split("\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); 60 | for (var y = 0; y < lines.Length; ++y) 61 | { 62 | var line = lines[y]; 63 | var slant = slanted ? y - 1 : 0; 64 | 65 | foreach (var token in line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries)) 66 | { 67 | var x = (line.IndexOf(token, StringComparison.Ordinal) - slant) / (tokenSize + 1); 68 | var p = new Point(x, y); 69 | positionTable[p] = token; 70 | } 71 | } 72 | 73 | var adjacencyGraph = new Dictionary>(); 74 | foreach (var pair in positionTable) 75 | { 76 | var p = pair.Key; 77 | foreach (var c in pair.Value) 78 | { 79 | adjacencyGraph[c] = new List(); 80 | var adjacentPoints = slanted ? GetSlantedAdjacent(p) : GetAlignedAdjacent(p); 81 | 82 | foreach (var adjacent in adjacentPoints) 83 | { 84 | // We want to include nulls so that direction is correspondent with index in the list 85 | adjacencyGraph[c].Add(positionTable.ContainsKey(adjacent) ? positionTable[adjacent] : null); 86 | } 87 | } 88 | } 89 | 90 | AdjacencyGraph = new ReadOnlyDictionary>(adjacencyGraph.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.AsReadOnly())); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /zxcvbn-core/Matcher/SpatialMatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using Zxcvbn.Matcher.Matches; 7 | 8 | namespace Zxcvbn.Matcher 9 | { 10 | /// 11 | /// Attempts to match a string against common keyboard layout patterns, considering shifted characters and changes in direction. 12 | /// 13 | internal class SpatialMatcher : IMatcher 14 | { 15 | private const string ShiftedRegex = "[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:\"ZXCVBNM<>?]"; 16 | 17 | /// 18 | /// Gets the spatial graphs to match against. 19 | /// 20 | internal static ReadOnlyCollection SpatialGraphs { get; } = GenerateSpatialGraphs(); 21 | 22 | /// 23 | /// Find spatial matches in . 24 | /// 25 | /// The password to check. 26 | /// An enumerable of spatial matches. 27 | public IEnumerable MatchPassword(string password) 28 | { 29 | return SpatialGraphs.SelectMany(g => SpatialMatch(g, password)); 30 | } 31 | 32 | private static ReadOnlyCollection GenerateSpatialGraphs() 33 | { 34 | const string qwerty = @" 35 | `~ 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) -_ =+ 36 | qQ wW eE rR tT yY uU iI oO pP [{ ]} \| 37 | aA sS dD fF gG hH jJ kK lL ;: '"" 38 | zZ xX cC vV bB nN mM ,< .> /? 39 | "; 40 | 41 | const string dvorak = @" 42 | `~ 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) [{ ]} 43 | '"" ,< .> pP yY fF gG cC rR lL /? =+ \| 44 | aA oO eE uU iI dD hH tT nN sS -_ 45 | ;: qQ jJ kK xX bB mM wW vV zZ 46 | "; 47 | 48 | const string keypad = @" 49 | / * - 50 | 7 8 9 + 51 | 4 5 6 52 | 1 2 3 53 | 0 . 54 | "; 55 | 56 | const string macKeypad = @" 57 | = / * 58 | 7 8 9 - 59 | 4 5 6 + 60 | 1 2 3 61 | 0 . 62 | "; 63 | 64 | return new List 65 | { 66 | new SpatialGraph("qwerty", qwerty, true), 67 | new SpatialGraph("dvorak", dvorak, true), 68 | new SpatialGraph("keypad", keypad, false), 69 | new SpatialGraph("mac_keypad", macKeypad, false), 70 | }.AsReadOnly(); 71 | } 72 | 73 | private static IEnumerable SpatialMatch(SpatialGraph graph, string password) 74 | { 75 | var matches = new List(); 76 | 77 | var i = 0; 78 | while (i < password.Length - 1) 79 | { 80 | var turns = 0; 81 | var shiftedCount = 0; 82 | int? lastDirection = null; 83 | 84 | var j = i + 1; 85 | 86 | if ((graph.Name == "qwerty" || graph.Name == "dvorak") && Regex.IsMatch(password[i].ToString(), ShiftedRegex)) 87 | { 88 | shiftedCount = 1; 89 | } 90 | 91 | while (true) 92 | { 93 | var prevChar = password[j - 1]; 94 | var found = false; 95 | var currentDirection = -1; 96 | var adjacents = graph.AdjacencyGraph.ContainsKey(prevChar) ? graph.AdjacencyGraph[prevChar] : Enumerable.Empty(); 97 | 98 | if (j < password.Length) 99 | { 100 | var curChar = password[j].ToString(); 101 | foreach (var adjacent in adjacents) 102 | { 103 | currentDirection++; 104 | 105 | if (adjacent == null) 106 | continue; 107 | 108 | if (adjacent.Contains(curChar)) 109 | { 110 | found = true; 111 | var foundDirection = currentDirection; 112 | if (adjacent.IndexOf(curChar, StringComparison.Ordinal) == 1) 113 | { 114 | shiftedCount++; 115 | } 116 | 117 | if (lastDirection != foundDirection) 118 | { 119 | turns++; 120 | lastDirection = foundDirection; 121 | } 122 | 123 | break; 124 | } 125 | } 126 | } 127 | 128 | if (found) 129 | { 130 | j++; 131 | } 132 | else 133 | { 134 | if (j - i > 2) 135 | { 136 | matches.Add(new SpatialMatch() 137 | { 138 | i = i, 139 | j = j - 1, 140 | Token = password.Substring(i, j - i), 141 | Graph = graph.Name, 142 | Turns = turns, 143 | ShiftedCount = shiftedCount, 144 | }); 145 | } 146 | 147 | i = j; 148 | break; 149 | } 150 | } 151 | } 152 | 153 | return matches; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /zxcvbn-core/MostGuessableMatchResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Zxcvbn.Matcher.Matches; 3 | 4 | namespace Zxcvbn 5 | { 6 | /// 7 | /// Represents the most guessable match provided. 8 | /// 9 | internal class MostGuessableMatchResult 10 | { 11 | /// 12 | /// Gets or sets the guesses the match is estimated to need. 13 | /// 14 | public double Guesses { get; set; } 15 | 16 | /// 17 | /// Gets or sets the password being assessed. 18 | /// 19 | public string Password { get; set; } 20 | 21 | /// 22 | /// Gets or sets the score associated with the most guessable match. 23 | /// 24 | public int Score { get; set; } 25 | 26 | /// 27 | /// Gets or sets the matches identified. 28 | /// 29 | public IEnumerable Sequence { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /zxcvbn-core/OptimalValues.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Zxcvbn.Matcher.Matches; 3 | 4 | namespace Zxcvbn 5 | { 6 | /// 7 | /// Represents the optimal value when assessing the most guessible. 8 | /// 9 | internal class OptimalValues 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The length of the password being assessed. 15 | public OptimalValues(int length) 16 | { 17 | for (var i = 0; i < length; i++) 18 | { 19 | G.Add(new Dictionary()); 20 | Pi.Add(new Dictionary()); 21 | M.Add(new Dictionary()); 22 | } 23 | } 24 | 25 | /// 26 | /// Gets or sets the overall metric for the best guess. 27 | /// 28 | public List> G { get; set; } = new List>(); 29 | 30 | /// 31 | /// Gets or sets the best match at a given length. 32 | /// 33 | public List> M { get; set; } = new List>(); 34 | 35 | /// 36 | /// Gets or sets the the product term of the metric for the best guess. 37 | /// 38 | public List> Pi { get; set; } = new List>(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /zxcvbn-core/PasswordScoring.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Zxcvbn.Matcher.Matches; 5 | using Zxcvbn.Scoring; 6 | 7 | namespace Zxcvbn 8 | { 9 | /// 10 | /// Some useful shared functions used for evaluating passwords. 11 | /// 12 | internal static class PasswordScoring 13 | { 14 | private const int MinimumGuessesBeforeGrowingSequence = 10000; 15 | 16 | /// 17 | /// Caclulate binomial coefficient (i.e. nCk). 18 | /// 19 | /// n. 20 | /// k. 21 | /// Binomial coefficient. 22 | public static double Binomial(int n, int k) 23 | { 24 | if (k > n) return 0; 25 | if (k == 0) return 1; 26 | 27 | var r = 1.0; 28 | for (var d = 1; d <= k; ++d) 29 | { 30 | r *= n; 31 | r /= d; 32 | n -= 1; 33 | } 34 | 35 | return r; 36 | } 37 | 38 | /// 39 | /// Estimates the attempts required to guess the password. 40 | /// 41 | /// The match. 42 | /// The actual password. 43 | /// The guesses estimate. 44 | public static double EstimateGuesses(Match match, string password) 45 | { 46 | if (match.Guesses != 0) 47 | return match.Guesses; 48 | 49 | var minGuesses = 1.0; 50 | if (match.Token.Length < password.Length) 51 | { 52 | minGuesses = match.Token.Length == 1 ? BruteForceGuessesCalculator.MinSubmatchGuessesSingleCharacter : BruteForceGuessesCalculator.MinSubmatchGuessesMultiCharacter; 53 | } 54 | 55 | var guesses = 0.0; 56 | 57 | switch (match.Pattern) 58 | { 59 | case "bruteforce": 60 | guesses = BruteForceGuessesCalculator.CalculateGuesses(match as BruteForceMatch); 61 | break; 62 | 63 | case "date": 64 | guesses = DateGuessesCalculator.CalculateGuesses(match as DateMatch); 65 | break; 66 | 67 | case "dictionary": 68 | guesses = DictionaryGuessesCalculator.CalculateGuesses(match as DictionaryMatch); 69 | break; 70 | 71 | case "regex": 72 | guesses = RegexGuessesCalculator.CalculateGuesses(match as RegexMatch); 73 | break; 74 | 75 | case "repeat": 76 | guesses = RepeatGuessesCalculator.CalculateGuesses(match as RepeatMatch); 77 | break; 78 | 79 | case "sequence": 80 | guesses = SequenceGuessesCalculator.CalculateGuesses(match as SequenceMatch); 81 | break; 82 | 83 | case "spatial": 84 | guesses = SpatialGuessesCalculator.CalculateGuesses(match as SpatialMatch); 85 | break; 86 | } 87 | 88 | match.Guesses = Math.Max(guesses, minGuesses); 89 | return match.Guesses; 90 | } 91 | 92 | /// 93 | /// Identifies the most guessable match in the sequence. 94 | /// 95 | /// The password. 96 | /// The matches. 97 | /// if set to true, will exclude additive matches (for unit testing only). 98 | /// A summary on the most testable match. 99 | public static MostGuessableMatchResult MostGuessableMatchSequence(string password, IEnumerable matches, bool excludeAdditive = false) 100 | { 101 | var matchesByJ = Enumerable.Range(0, password.Length).Select(i => new List()).ToList(); 102 | foreach (var m in matches) 103 | matchesByJ[m.j].Add(m); 104 | 105 | var optimal = new OptimalValues(password.Length); 106 | 107 | for (var k = 0; k < password.Length; k++) 108 | { 109 | foreach (var m in matchesByJ[k]) 110 | { 111 | if (m.i > 0) 112 | { 113 | foreach (var l in optimal.M[m.i - 1].Keys) 114 | { 115 | Update(password, optimal, m, l + 1, excludeAdditive); 116 | } 117 | } 118 | else 119 | { 120 | Update(password, optimal, m, 1, excludeAdditive); 121 | } 122 | } 123 | 124 | BruteforceUpdate(password, optimal, k, excludeAdditive); 125 | } 126 | 127 | var optimalMatchSequence = Unwind(optimal, password.Length); 128 | var optimalL = optimalMatchSequence.Count; 129 | 130 | double guesses; 131 | 132 | if (password.Length == 0) 133 | guesses = 1; 134 | else 135 | guesses = optimal.G[password.Length - 1][optimalL]; 136 | 137 | return new MostGuessableMatchResult 138 | { 139 | Guesses = guesses, 140 | Password = password, 141 | Sequence = optimalMatchSequence, 142 | Score = 0, 143 | }; 144 | } 145 | 146 | private static void BruteforceUpdate(string password, OptimalValues optimal, int k, bool excludeAdditive) 147 | { 148 | Update(password, optimal, MakeBruteforceMatch(password, 0, k), 1, excludeAdditive); 149 | 150 | for (var i = 1; i <= k; i++) 151 | { 152 | var m = MakeBruteforceMatch(password, i, k); 153 | var obj = optimal.M[i - 1]; 154 | 155 | foreach (var l in obj.Keys) 156 | { 157 | var lastM = obj[l]; 158 | if (lastM.Pattern == "bruteforce") 159 | continue; 160 | Update(password, optimal, m, l + 1, excludeAdditive); 161 | } 162 | } 163 | } 164 | 165 | private static double Factorial(double n) 166 | { 167 | if (n < 2) 168 | return 1; 169 | var f = 1.0; 170 | 171 | for (var i = 2; i <= n; i++) 172 | f *= i; 173 | 174 | return f; 175 | } 176 | 177 | private static BruteForceMatch MakeBruteforceMatch(string password, int i, int j) 178 | { 179 | return new BruteForceMatch 180 | { 181 | Token = password.Substring(i, j - i + 1), 182 | i = i, 183 | j = j, 184 | }; 185 | } 186 | 187 | private static List Unwind(OptimalValues optimal, int n) 188 | { 189 | var optimalMatchSequence = new List(); 190 | var k = n - 1; 191 | var l = -1; 192 | var g = double.PositiveInfinity; 193 | if (k < 0) 194 | { 195 | return optimalMatchSequence; 196 | } 197 | 198 | foreach (var candidateL in optimal.G[k].Keys) 199 | { 200 | var candidateG = optimal.G[k][candidateL]; 201 | 202 | if (candidateG < g) 203 | { 204 | l = candidateL; 205 | g = candidateG; 206 | } 207 | } 208 | 209 | while (k >= 0) 210 | { 211 | var m = optimal.M[k][l]; 212 | optimalMatchSequence.Insert(0, m); 213 | k = m.i - 1; 214 | l--; 215 | } 216 | 217 | return optimalMatchSequence; 218 | } 219 | 220 | private static void Update(string password, OptimalValues optimal, Match m, int l, bool excludeAdditive) 221 | { 222 | var k = m.j; 223 | var pi = EstimateGuesses(m, password); 224 | if (l > 1) 225 | pi *= optimal.Pi[m.i - 1][l - 1]; 226 | 227 | var g = Factorial(l) * pi; 228 | if (!excludeAdditive) 229 | g += Math.Pow(MinimumGuessesBeforeGrowingSequence, l - 1); 230 | 231 | foreach (var competingL in optimal.G[k].Keys) 232 | { 233 | var competingG = optimal.G[k][competingL]; 234 | if (competingL > l) 235 | continue; 236 | if (competingG <= g) 237 | return; 238 | } 239 | 240 | optimal.G[k][l] = g; 241 | optimal.M[k][l] = m; 242 | optimal.Pi[k][l] = pi; 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /zxcvbn-core/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("zxcvbn-core-test, PublicKey=00240000048000009400000006020000002400005253413100040000010001000d095cca1c805b0a5d66873a225ccc3d721948e40f123ba80ecf6b89e3be35dacea2a1341505eb45fb10f549f7b0c8f9e2000e01e2105929949bdcf23170010103c02d087fecfc67bb7e6d0be6a861ea3390a2bd3a7f3a09409e81a5378463b54fec11263ddeafc6b2685d94e93cf249dd02be648470aac661bd4a115b3d7cd7")] 4 | -------------------------------------------------------------------------------- /zxcvbn-core/Result.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn 6 | { 7 | /// 8 | /// The results of zxcvbn's password analysis. 9 | /// 10 | public class Result 11 | { 12 | /// 13 | /// Gets the number of milliseconds that zxcvbn took to calculate results for this password. 14 | /// 15 | public long CalcTime { get; internal set; } 16 | 17 | /// 18 | /// Gets An estimation of the crack times for this password in seconds. 19 | /// 20 | public CrackTimes CrackTime { get; internal set; } 21 | 22 | /// 23 | /// Gets a set of string for the crack times. 24 | /// 25 | public CrackTimesDisplay CrackTimeDisplay { get; internal set; } 26 | 27 | /// 28 | /// Gets the feedback for the user about their password. 29 | /// 30 | public FeedbackItem Feedback { get; internal set; } 31 | 32 | /// 33 | /// Gets the number of guesses the password is estimated to need. 34 | /// 35 | public double Guesses { get; internal set; } 36 | 37 | /// 38 | /// Gets log10(the number of guesses) the password is estimated to need. 39 | /// 40 | public double GuessesLog10 => Math.Log10(Guesses); 41 | 42 | /// 43 | /// Gets the sequence of matches that were used to assess the password. 44 | /// 45 | public IEnumerable MatchSequence { get; internal set; } 46 | 47 | /// 48 | /// Gets the password that was used to generate these results. 49 | /// 50 | public string Password { get; internal set; } 51 | 52 | /// 53 | /// Gets a score from 0 to 4 (inclusive), with 0 being least secure and 4 being most secure, calculated from the nubmer of guesses estimated to be needed. 54 | /// 55 | public int Score { get; internal set; } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /zxcvbn-core/Scoring/BruteForceGuessesCalculator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Zxcvbn.Matcher.Matches; 3 | 4 | namespace Zxcvbn.Scoring 5 | { 6 | /// 7 | /// Estimates the number of attempts needed to brute-force the password. 8 | /// 9 | internal class BruteForceGuessesCalculator 10 | { 11 | /// 12 | /// The minimum submatch guesses for a multi character string. 13 | /// 14 | public const double MinSubmatchGuessesMultiCharacter = 50; 15 | 16 | /// 17 | /// The minimum submatch guesses for a single character. 18 | /// 19 | public const double MinSubmatchGuessesSingleCharacter = 10; 20 | 21 | private const double BruteforceCardinality = 10; 22 | 23 | /// 24 | /// Estimates the attempts required to guess the password. 25 | /// 26 | /// The match. 27 | /// The guesses estimate. 28 | public static double CalculateGuesses(BruteForceMatch match) 29 | { 30 | var guesses = Math.Pow(BruteforceCardinality, match.Token.Length); 31 | if (double.IsPositiveInfinity(guesses)) 32 | guesses = double.MaxValue; 33 | 34 | var minGuesses = match.Token.Length == 1 ? MinSubmatchGuessesSingleCharacter + 1 : MinSubmatchGuessesMultiCharacter + 1; 35 | 36 | return Math.Max(guesses, minGuesses); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /zxcvbn-core/Scoring/DateGuessesCalculator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Zxcvbn.Matcher; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn.Scoring 6 | { 7 | /// 8 | /// Estimates the number of attempts needed to guess the date. 9 | /// 10 | internal class DateGuessesCalculator 11 | { 12 | /// 13 | /// The minimum distance between the reference date and the provided date. 14 | /// 15 | public const int MinimumYearSpace = 20; 16 | 17 | /// 18 | /// Estimates the attempts required to guess the password. 19 | /// 20 | /// The match. 21 | /// The guesses estimate. 22 | public static double CalculateGuesses(DateMatch match) 23 | { 24 | var yearSpace = Math.Max(Math.Abs(match.Year - DateMatcher.ReferenceYear), MinimumYearSpace); 25 | 26 | var guesses = yearSpace * 365.0; 27 | if (!string.IsNullOrEmpty(match.Separator)) 28 | guesses *= 4; 29 | 30 | return guesses; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /zxcvbn-core/Scoring/DictionaryGuessesCalculator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Zxcvbn.Matcher.Matches; 4 | 5 | namespace Zxcvbn.Scoring 6 | { 7 | /// 8 | /// Estimates the number of attempts needed to dictionary search for the password. 9 | /// 10 | internal class DictionaryGuessesCalculator 11 | { 12 | /// 13 | /// Estimates the attempts required to guess the password. 14 | /// 15 | /// The match. 16 | /// The guesses estimate. 17 | public static double CalculateGuesses(DictionaryMatch match) 18 | { 19 | match.BaseGuesses = match.Rank; 20 | match.UppercaseVariations = UppercaseVariations(match.Token); 21 | match.L33tVariations = L33tVariations(match); 22 | var reversedVariations = match.Reversed ? 2 : 1; 23 | 24 | return match.BaseGuesses * match.UppercaseVariations * match.L33tVariations * reversedVariations; 25 | } 26 | 27 | /// 28 | /// Calculates the number of l33t variations in the word. 29 | /// 30 | /// The match. 31 | /// The number of possible variations. 32 | internal static double L33tVariations(DictionaryMatch match) 33 | { 34 | if (!match.L33t) 35 | return 1; 36 | 37 | var variations = 1.0; 38 | 39 | foreach (var subbed in match.Sub.Keys) 40 | { 41 | var unsubbed = match.Sub[subbed]; 42 | var s = match.Token.ToLower().Count(c => c == subbed); 43 | var u = match.Token.ToLower().Count(c => c == unsubbed); 44 | 45 | if (s == 0 || u == 0) 46 | { 47 | variations *= 2; 48 | } 49 | else 50 | { 51 | var p = Math.Min(u, s); 52 | var possibilities = 0.0; 53 | for (var i = 1; i <= p; i++) 54 | possibilities += PasswordScoring.Binomial(u + s, i); 55 | variations *= possibilities; 56 | } 57 | } 58 | 59 | return variations; 60 | } 61 | 62 | /// 63 | /// Calculates the number of uppercase variations in the word. 64 | /// 65 | /// The token. 66 | /// The number of possible variations. 67 | internal static double UppercaseVariations(string token) 68 | { 69 | if (token.All(c => char.IsLower(c)) || token.ToLower() == token) 70 | return 1; 71 | 72 | if ((char.IsUpper(token.First()) && token.Skip(1).All(c => char.IsLower(c))) 73 | || token.All(c => char.IsUpper(c)) 74 | || (char.IsUpper(token.Last()) && token.Take(token.Length - 1).All(c => char.IsLower(c)))) 75 | return 2; 76 | 77 | var u = token.Count(c => char.IsUpper(c)); 78 | var l = token.Count(c => char.IsLower(c)); 79 | var variations = 0.0; 80 | 81 | for (var i = 1; i <= Math.Min(u, l); i++) 82 | variations += PasswordScoring.Binomial(u + l, i); 83 | 84 | return variations; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /zxcvbn-core/Scoring/RegexGuessesCalculator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Zxcvbn.Matcher; 4 | using Zxcvbn.Matcher.Matches; 5 | 6 | namespace Zxcvbn.Scoring 7 | { 8 | /// 9 | /// Estimates the number of attempts needed to guess the value picked out by regular expression. 10 | /// 11 | internal class RegexGuessesCalculator 12 | { 13 | /// 14 | /// Estimates the attempts required to guess the password. 15 | /// 16 | /// The match. 17 | /// The guesses estimate. 18 | public static double CalculateGuesses(RegexMatch match) 19 | { 20 | switch (match.RegexName) 21 | { 22 | case "recent_year": 23 | var yearSpace = Math.Abs(int.Parse(match.Token, CultureInfo.InvariantCulture) - DateMatcher.ReferenceYear); 24 | yearSpace = Math.Max(yearSpace, DateGuessesCalculator.MinimumYearSpace); 25 | return yearSpace; 26 | 27 | default: 28 | throw new InvalidOperationException(); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /zxcvbn-core/Scoring/RepeatGuessesCalculator.cs: -------------------------------------------------------------------------------- 1 | using Zxcvbn.Matcher.Matches; 2 | 3 | namespace Zxcvbn.Scoring 4 | { 5 | /// 6 | /// Estimates the number of attempts needed to guesses the number of repeats. 7 | /// 8 | internal class RepeatGuessesCalculator 9 | { 10 | /// 11 | /// Estimates the attempts required to guess the password. 12 | /// 13 | /// The match. 14 | /// The guesses estimate. 15 | public static double CalculateGuesses(RepeatMatch match) 16 | { 17 | return match.BaseGuesses * match.RepeatCount; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /zxcvbn-core/Scoring/SequenceGuessesCalculator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Zxcvbn.Matcher.Matches; 3 | 4 | namespace Zxcvbn.Scoring 5 | { 6 | /// 7 | /// Estimates the number of attempts needed to guess the sequence. 8 | /// 9 | internal class SequenceGuessesCalculator 10 | { 11 | private static readonly List ObviousStartCharacters = new List 12 | { 13 | 'a', 'A', 'z', 'Z', '0', '1', '9', 14 | }; 15 | 16 | /// 17 | /// Estimates the attempts required to guess the password. 18 | /// 19 | /// The match. 20 | /// The guesses estimate. 21 | public static double CalculateGuesses(SequenceMatch match) 22 | { 23 | double baseGuesses; 24 | 25 | if (ObviousStartCharacters.Contains(match.Token[0])) 26 | { 27 | baseGuesses = 4; 28 | } 29 | else 30 | { 31 | if (char.IsDigit(match.Token[0])) 32 | baseGuesses = 10; 33 | else 34 | baseGuesses = 26; 35 | } 36 | 37 | if (!match.Ascending) 38 | baseGuesses *= 2; 39 | 40 | return baseGuesses * match.Token.Length; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /zxcvbn-core/Scoring/SpatialGuessesCalculator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Zxcvbn.Matcher; 4 | using Zxcvbn.Matcher.Matches; 5 | 6 | namespace Zxcvbn.Scoring 7 | { 8 | /// 9 | /// Estimates the number of attempts needed to guess the spatial pattern. 10 | /// 11 | internal static class SpatialGuessesCalculator 12 | { 13 | /// 14 | /// The average number of adjacent characters on a keyboard. 15 | /// 16 | internal static readonly double KeyboardAverageDegree = (int)Math.Round(CalculateAverageDegree(SpatialMatcher.SpatialGraphs.First(s => s.Name == "qwerty"))); 17 | 18 | /// 19 | /// The number of starting positions on a keyboard. 20 | /// 21 | internal static readonly int KeyboardStartingPositions = SpatialMatcher.SpatialGraphs.First(s => s.Name == "qwerty").AdjacencyGraph.Keys.Count; 22 | 23 | /// 24 | /// The average number of adjacent characters on a keypad. 25 | /// 26 | internal static readonly double KeypadAverageDegree = (int)Math.Round(CalculateAverageDegree(SpatialMatcher.SpatialGraphs.First(s => s.Name == "keypad"))); 27 | 28 | /// 29 | /// The number of starting positions on a keypad. 30 | /// 31 | internal static readonly int KeypadStartingPositions = SpatialMatcher.SpatialGraphs.First(s => s.Name == "keypad").AdjacencyGraph.Keys.Count; 32 | 33 | /// 34 | /// Estimates the attempts required to guess the password. 35 | /// 36 | /// The match. 37 | /// The guesses estimate. 38 | public static double CalculateGuesses(SpatialMatch match) 39 | { 40 | int s; 41 | double d; 42 | if (match.Graph == "qwerty" || match.Graph == "dvorak") 43 | { 44 | s = KeyboardStartingPositions; 45 | d = KeyboardAverageDegree; 46 | } 47 | else 48 | { 49 | s = KeypadStartingPositions; 50 | d = KeypadAverageDegree; 51 | } 52 | 53 | double guesses = 0; 54 | var l = match.Token.Length; 55 | var t = match.Turns; 56 | 57 | for (var i = 2; i <= l; i++) 58 | { 59 | var possibleTurns = Math.Min(t, i - 1); 60 | for (var j = 1; j <= possibleTurns; j++) 61 | { 62 | guesses += PasswordScoring.Binomial(i - 1, j - 1) * s * Math.Pow(d, j); 63 | } 64 | } 65 | 66 | if (match.ShiftedCount > 0) 67 | { 68 | var shifted = match.ShiftedCount; 69 | var unshifted = match.Token.Length - match.ShiftedCount; 70 | if (shifted == 0 || unshifted == 0) 71 | { 72 | guesses *= 2; 73 | } 74 | else 75 | { 76 | double variations = 0; 77 | for (var i = 1; i <= Math.Min(shifted, unshifted); i++) 78 | { 79 | variations += PasswordScoring.Binomial(shifted + unshifted, i); 80 | } 81 | 82 | guesses *= variations; 83 | } 84 | } 85 | 86 | return guesses; 87 | } 88 | 89 | private static double CalculateAverageDegree(SpatialGraph graph) 90 | { 91 | var average = 0.0; 92 | foreach (var key in graph.AdjacencyGraph.Keys) 93 | { 94 | average += graph.AdjacencyGraph[key].Count(s => s != null); 95 | } 96 | 97 | average /= graph.AdjacencyGraph.Keys.Count; 98 | return average; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /zxcvbn-core/TimeEstimates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Zxcvbn 4 | { 5 | /// 6 | /// Estimates how long a password will take to crack under various conditions. 7 | /// 8 | internal static class TimeEstimates 9 | { 10 | /// 11 | /// Calculates the estimated attack times. 12 | /// 13 | /// The number of guesses required. 14 | /// A summary of the estimated attack times. 15 | public static AttackTimes EstimateAttackTimes(double guesses) 16 | { 17 | var crackTimesSeconds = new CrackTimes 18 | { 19 | OfflineFastHashing1e10PerSecond = guesses / 1e10, 20 | OfflineSlowHashing1e4PerSecond = guesses / 1e4, 21 | OnlineNoThrottling10PerSecond = guesses / 10, 22 | OnlineThrottling100PerHour = guesses / (100.0 / 3600), 23 | }; 24 | var crackTimesDisplay = new CrackTimesDisplay 25 | { 26 | OfflineFastHashing1e10PerSecond = DisplayTime(crackTimesSeconds.OfflineFastHashing1e10PerSecond), 27 | OfflineSlowHashing1e4PerSecond = DisplayTime(crackTimesSeconds.OfflineSlowHashing1e4PerSecond), 28 | OnlineNoThrottling10PerSecond = DisplayTime(crackTimesSeconds.OnlineNoThrottling10PerSecond), 29 | OnlineThrottling100PerHour = DisplayTime(crackTimesSeconds.OnlineThrottling100PerHour), 30 | }; 31 | 32 | return new AttackTimes 33 | { 34 | CrackTimesDisplay = crackTimesDisplay, 35 | CrackTimesSeconds = crackTimesSeconds, 36 | Score = GuessesToScore(guesses), 37 | }; 38 | } 39 | 40 | private static string DisplayTime(double seconds) 41 | { 42 | const double minute = 60; 43 | const double hour = minute * 60; 44 | const double day = hour * 24; 45 | const double month = day * 31; 46 | const double year = month * 12; 47 | const double century = year * 100; 48 | 49 | int? displayNumber = null; 50 | string displayString; 51 | 52 | if (seconds < 1) 53 | return "less than a second"; 54 | if (seconds < minute) 55 | { 56 | displayNumber = (int)Math.Round(seconds); 57 | displayString = $"{displayNumber} second"; 58 | } 59 | else if (seconds < hour) 60 | { 61 | displayNumber = (int)Math.Round(seconds / minute); 62 | displayString = $"{displayNumber} minute"; 63 | } 64 | else if (seconds < day) 65 | { 66 | displayNumber = (int)Math.Round(seconds / hour); 67 | displayString = $"{displayNumber} hour"; 68 | } 69 | else if (seconds < month) 70 | { 71 | displayNumber = (int)Math.Round(seconds / day); 72 | displayString = $"{displayNumber} day"; 73 | } 74 | else if (seconds < year) 75 | { 76 | displayNumber = (int)Math.Round(seconds / month); 77 | displayString = $"{displayNumber} month"; 78 | } 79 | else if (seconds < century) 80 | { 81 | displayNumber = (int)Math.Round(seconds / year); 82 | displayString = $"{displayNumber} year"; 83 | } 84 | else 85 | { 86 | displayString = "centuries"; 87 | } 88 | 89 | if (displayNumber.HasValue && displayNumber != 1) 90 | displayString += "s"; 91 | 92 | return displayString; 93 | } 94 | 95 | private static int GuessesToScore(double guesses) 96 | { 97 | const int delta = 5; 98 | if (guesses < 1e3 + delta) 99 | return 0; 100 | if (guesses < 1e6 + delta) 101 | return 1; 102 | if (guesses < 1e8 + delta) 103 | return 2; 104 | if (guesses < 1e10 + delta) 105 | return 3; 106 | return 4; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /zxcvbn-core/Utility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace Zxcvbn 7 | { 8 | /// 9 | /// A few useful extension methods used through the Zxcvbn project. 10 | /// 11 | internal static class Utility 12 | { 13 | /// 14 | /// Returns a list of the lines of text from an embedded resource in the assembly. 15 | /// 16 | /// The name of the resource to get the contents from. 17 | /// An enumerable of lines of text in the resource or null if the resource does not exist. 18 | public static IEnumerable GetEmbeddedResourceLines(string resourceName) 19 | { 20 | var asm = typeof(Utility).GetTypeInfo().Assembly; 21 | if (!asm.GetManifestResourceNames().Contains(resourceName)) return null; // Not an embedded resource 22 | 23 | var lines = new List(); 24 | 25 | using (var stream = asm.GetManifestResourceStream(resourceName)) 26 | using (var text = new StreamReader(stream)) 27 | { 28 | while (!text.EndOfStream) 29 | { 30 | lines.Add(text.ReadLine()); 31 | } 32 | } 33 | 34 | return lines; 35 | } 36 | 37 | /// 38 | /// Reverse a string in one call. 39 | /// 40 | /// String to reverse. 41 | /// String in reverse. 42 | public static string StringReverse(this string str) 43 | { 44 | return new string(str.Reverse().ToArray()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /zxcvbn-core/zxcvbn-core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;net461;netstandard1.3 4 | zxcvbn-core 5 | mickford;Tony Richards (trichards57);Dan Wheeler;DropBox 6 | zxcvbn-core 7 | C#/.NET port of Dan Wheeler/DropBox's Zxcvbn JS password strength estimation library. Updated for .Net Core. 8 | https://github.com/trichards57/zxcvbn-cs 9 | https://github.com/trichards57/zxcvbn-cs.git 10 | password;strength;validation;zxcvbn 11 | Copyright (c) 2012 Dropbox, Inc. Copyright (c) 2020-2021 Tony Richards 12 | Zxcvbn 13 | $(TargetDir)zxcvbn-core.xml 14 | true 15 | zxcvbn-strong-name-key.snk 16 | true 17 | true 18 | snupkg 19 | true 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | True 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /zxcvbn-core/zxcvbn-strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trichards57/zxcvbn-cs/2e680287ca64bde8ee5366a17ac0d2af7a11d8ea/zxcvbn-core/zxcvbn-strong-name-key.snk -------------------------------------------------------------------------------- /zxcvbn-cs.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{71211407-F148-4CD8-A83E-2E0399B8D97D}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | .gitignore = .gitignore 10 | azure-pipelines.yml = azure-pipelines.yml 11 | GitVersion.yml = GitVersion.yml 12 | LICENSE = LICENSE 13 | nuget.exe = nuget.exe 14 | README.md = README.md 15 | tests.bat = tests.bat 16 | EndProjectSection 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zxcvbn-core", "zxcvbn-core\zxcvbn-core.csproj", "{5D3F6D54-EB79-4980-B09A-5C72136E8B4A}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zxcvbn-core-test", "zxcvbn-core-test\zxcvbn-core-test.csproj", "{65B256F9-4874-4D6F-9A46-D881FAB0215B}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zxcvbn-core-list-builder", "zxcvbn-core-list-builder\zxcvbn-core-list-builder.csproj", "{80BA1964-B98A-4D34-95B8-DFA51CD3A378}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {5D3F6D54-EB79-4980-B09A-5C72136E8B4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {5D3F6D54-EB79-4980-B09A-5C72136E8B4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {5D3F6D54-EB79-4980-B09A-5C72136E8B4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {5D3F6D54-EB79-4980-B09A-5C72136E8B4A}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {65B256F9-4874-4D6F-9A46-D881FAB0215B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {65B256F9-4874-4D6F-9A46-D881FAB0215B}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {65B256F9-4874-4D6F-9A46-D881FAB0215B}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {65B256F9-4874-4D6F-9A46-D881FAB0215B}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {80BA1964-B98A-4D34-95B8-DFA51CD3A378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {80BA1964-B98A-4D34-95B8-DFA51CD3A378}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {80BA1964-B98A-4D34-95B8-DFA51CD3A378}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {80BA1964-B98A-4D34-95B8-DFA51CD3A378}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {FF2D379C-4018-4EF8-B032-89FCE8B2A37D} 48 | EndGlobalSection 49 | EndGlobal 50 | --------------------------------------------------------------------------------