From cfd1b29f12782d6add7eab7a741dea39b4e4c6f9 Mon Sep 17 00:00:00 2001 From: Tony Richards Date: Wed, 28 Oct 2020 19:29:27 +0000 Subject: [PATCH] Fixed some discrepancies with the typescript library. Also made compatible with .Net Standard 1.3 --- LICENSE | 1 + README.md | 53 ++++--------------- appveyor.yml | 2 +- zxcvbn-core-test-console/Program.cs | 14 +++++ .../zxcvbn-core-test-console.csproj | 14 +++++ zxcvbn-core/Feedback.cs | 3 +- zxcvbn-core/GlobalSuppressions.cs | 2 + zxcvbn-core/Matcher/DictionaryMatcher.cs | 5 +- zxcvbn-core/Matcher/L33tMatcher.cs | 2 +- zxcvbn-core/Result.cs | 10 +++- .../Scoring/BruteForceGuessesCalculator.cs | 6 +-- .../Scoring/DictionaryGuessesCalculator.cs | 7 ++- zxcvbn-core/TimeEstimates.cs | 6 +-- zxcvbn-core/zxcvbn-core.csproj | 5 +- zxcvbn-cs.sln | 2 +- 15 files changed, 66 insertions(+), 66 deletions(-) create mode 100644 zxcvbn-core-test-console/Program.cs create mode 100644 zxcvbn-core-test-console/zxcvbn-core-test-console.csproj diff --git a/LICENSE b/LICENSE index 2bb439f..de8d2ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ Copyright (c) 2012 Dropbox, Inc. +Copyright (c) 2020 Tony Richards Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index f675ef5..b5c302f 100644 --- a/README.md +++ b/README.md @@ -25,69 +25,36 @@ From the `Zxcvbn` readme: > > http://tech.dropbox.com/?p=165 -This port aims to produce comparable results with the JS version of `Zxcvbn`. 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 that it return the same results -as the JS version. - -There are some implementation differences, however, so exact results are not guaranteed. +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. +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 +that it return the same score as the JS version (some other details vary a little). +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 +and I can investigate. ### Using `Zxcvbn-cs` The included Visual Studio project will create a single assembly, Zxcvbn.dll, which is all that is required to be included in your project. -To evaluate a single password: - -``` C# -using Zxcvbn; - -//... - -var result = Zxcvbn.MatchPassword("p@ssw0rd"); -``` - -To evaluate many passwords, create an instance of `Zxcvbn` and then use that to evaluate your passwords. -This avoids reloading dictionaries etc. for every password: +To evaluate a password: ``` C# using Zxcvbn; //... -var zx = new Zxcvbn(); - -foreach (var password in passwords) -{ - var result = zx.EvaluatePassword(password); - - //... -} +var result = Zxcvbn.Core.EvaluatePassword("p@ssw0rd"); ``` -Both `MatchPassword` and `EvaluatePassword` take an optional second parameter that contains an enumerable of +`EvaluatePassword` takes an optional second parameter that contains an enumerable of user data strings to also match the password against. ### Interpreting Results -The `Result` structure returned from password evaluation is interpreted the same way as with JS `Zxcvbn`: - -- `result.Entropy`: bits of entropy for the password -- `result.CrackTime`: an estimation of actual crack time, in seconds. -- `result.CrackTimeDisplay`: the crack time, as a friendlier string: "instant", "6 minutes", "centuries", etc. -- `result.Score`: [0,1,2,3,4] if crack time is less than [10\*\*2, 10\*\*4, 10\*\*6, 10\*\*8, Infinity]. (useful for implementing a strength bar.) -- `result.MatchSequence`: the list of pattern matches that was used to calculate Entropy. -- `result.CalculationTime`: how long `Zxcvbn` took to calculate the results. - -### More Information - -For more information on why password entropy is calculated as it is, refer to `Zxcvbn`s originators: - -https://github.com/lowe/zxcvbn - -http://tech.dropbox.com/?p=165 +The `Result` structure returned from password evaluation is interpreted the same way as with JS `Zxcvbn`. +- `result.Score`: 0-4 indicating the estimated strength of the password. ### Licence diff --git a/appveyor.yml b/appveyor.yml index 5ba91de..e241549 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 2.0.{build} +version: 5.0.{build} image: Visual Studio 2019 init: - git config --global core.autocrlf true diff --git a/zxcvbn-core-test-console/Program.cs b/zxcvbn-core-test-console/Program.cs new file mode 100644 index 0000000..f7c9b69 --- /dev/null +++ b/zxcvbn-core-test-console/Program.cs @@ -0,0 +1,14 @@ +using System; + +namespace Zxcvbn.TestConsole +{ + internal class Program + { + private static void Main(string[] args) + { + var result = Zxcvbn.Core.EvaluatePassword("Applesoranges!"); + + Console.WriteLine(result.Score); + } + } +} diff --git a/zxcvbn-core-test-console/zxcvbn-core-test-console.csproj b/zxcvbn-core-test-console/zxcvbn-core-test-console.csproj new file mode 100644 index 0000000..b484432 --- /dev/null +++ b/zxcvbn-core-test-console/zxcvbn-core-test-console.csproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp3.1 + Zxcvbn.TestConsole + zxcvbn-core-test-console + false + + + + + + diff --git a/zxcvbn-core/Feedback.cs b/zxcvbn-core/Feedback.cs index ae0719f..9711427 100644 --- a/zxcvbn-core/Feedback.cs +++ b/zxcvbn-core/Feedback.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Globalization; using System.Linq; using Zxcvbn.Matcher.Matches; @@ -93,7 +92,7 @@ private static FeedbackItem GetDictionaryMatchFeedback(DictionaryMatch match, bo var word = match.Token; if (char.IsUpper(word[0])) suggestions.Add("Capitalization doesn't help very much"); - else if (word.All(c => char.IsUpper(c)) && word.ToLower(CultureInfo.InvariantCulture) != word) + else if (word.All(c => char.IsUpper(c)) && word.ToLower() != word) suggestions.Add("All-uppercase is almost as easy to guess as all-lowercase"); if (match.Reversed && match.Token.Length >= 4) diff --git a/zxcvbn-core/GlobalSuppressions.cs b/zxcvbn-core/GlobalSuppressions.cs index 0e74275..c9c52ea 100644 --- a/zxcvbn-core/GlobalSuppressions.cs +++ b/zxcvbn-core/GlobalSuppressions.cs @@ -10,3 +10,5 @@ [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "To match this project's coding style.")] [assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "This normalization isn't security critical")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "No header required.")] +[assembly: SuppressMessage("Globalization", "CA1304:Specify CultureInfo", Justification = "Not supported in all the desired target libraries.")] +[assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "Not supported in all the desired target libraries.")] diff --git a/zxcvbn-core/Matcher/DictionaryMatcher.cs b/zxcvbn-core/Matcher/DictionaryMatcher.cs index c10e8d8..94802c9 100644 --- a/zxcvbn-core/Matcher/DictionaryMatcher.cs +++ b/zxcvbn-core/Matcher/DictionaryMatcher.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using Zxcvbn.Matcher.Matches; @@ -42,7 +41,7 @@ public DictionaryMatcher(string name, IEnumerable wordList) dictionaryName = name; // Must ensure that the dictionary is using lowercase words only - rankedDictionary = BuildRankedDictionary(wordList.Select(w => w.ToLower(CultureInfo.InvariantCulture))); + rankedDictionary = BuildRankedDictionary(wordList.Select(w => w.ToLower())); } /// @@ -52,7 +51,7 @@ public DictionaryMatcher(string name, IEnumerable wordList) /// An enumerable of dictionary matches. public virtual IEnumerable MatchPassword(string password) { - var passwordLower = password.ToLower(CultureInfo.InvariantCulture); + var passwordLower = password.ToLower(); var length = passwordLower.Length; var matches = new List(); diff --git a/zxcvbn-core/Matcher/L33tMatcher.cs b/zxcvbn-core/Matcher/L33tMatcher.cs index 08e8533..512b33a 100644 --- a/zxcvbn-core/Matcher/L33tMatcher.cs +++ b/zxcvbn-core/Matcher/L33tMatcher.cs @@ -73,7 +73,7 @@ public IEnumerable MatchPassword(string password) foreach (DictionaryMatch match in matcher.MatchPassword(subbedPassword)) { var token = password.Substring(match.i, match.j - match.i + 1); - if (token.Equals(match.MatchedWord, StringComparison.InvariantCultureIgnoreCase)) + if (token.ToLower().Equals(match.MatchedWord.ToLower())) continue; var matchSub = new Dictionary(); diff --git a/zxcvbn-core/Result.cs b/zxcvbn-core/Result.cs index 8da4fbf..72bcadb 100644 --- a/zxcvbn-core/Result.cs +++ b/zxcvbn-core/Result.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Zxcvbn.Matcher.Matches; namespace Zxcvbn @@ -31,7 +32,12 @@ public class Result /// /// Gets the number of guesses the password is estimated to need. /// - public double Guesses { get; internal set; } + public long Guesses { get; internal set; } + + /// + /// Gets log10(the number of guesses) the password is estimated to need. + /// + public double GuessesLog10 => Math.Log10(Guesses); /// /// Gets the sequence of matches that were used to assess the password. diff --git a/zxcvbn-core/Scoring/BruteForceGuessesCalculator.cs b/zxcvbn-core/Scoring/BruteForceGuessesCalculator.cs index ad1f365..2dc5b01 100644 --- a/zxcvbn-core/Scoring/BruteForceGuessesCalculator.cs +++ b/zxcvbn-core/Scoring/BruteForceGuessesCalculator.cs @@ -27,13 +27,13 @@ internal class BruteForceGuessesCalculator /// The guesses estimate. public static long CalculateGuesses(BruteForceMatch match) { - var guesses = (long)Math.Pow(BruteforceCardinality, match.Token.Length); + var guesses = Math.Pow(BruteforceCardinality, match.Token.Length); if (double.IsPositiveInfinity(guesses)) - guesses = long.MaxValue; + guesses = double.MaxValue; var minGuesses = match.Token.Length == 1 ? MinSubmatchGuessesSingleCharacter + 1 : MinSubmatchGuessesMultiCharacter + 1; - return Math.Max(guesses, minGuesses); + return (long)Math.Max(guesses, minGuesses); } } } diff --git a/zxcvbn-core/Scoring/DictionaryGuessesCalculator.cs b/zxcvbn-core/Scoring/DictionaryGuessesCalculator.cs index fe272cd..da4fa5c 100644 --- a/zxcvbn-core/Scoring/DictionaryGuessesCalculator.cs +++ b/zxcvbn-core/Scoring/DictionaryGuessesCalculator.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Linq; using Zxcvbn.Matcher.Matches; @@ -40,8 +39,8 @@ internal static long L33tVariations(DictionaryMatch match) foreach (var subbed in match.Sub.Keys) { var unsubbed = match.Sub[subbed]; - var s = match.Token.ToLower(CultureInfo.InvariantCulture).Count(c => c == subbed); - var u = match.Token.ToLower(CultureInfo.InvariantCulture).Count(c => c == unsubbed); + var s = match.Token.ToLower().Count(c => c == subbed); + var u = match.Token.ToLower().Count(c => c == unsubbed); if (s == 0 || u == 0) { @@ -67,7 +66,7 @@ internal static long L33tVariations(DictionaryMatch match) /// The number of possible variations. internal static long UppercaseVariations(string token) { - if (token.All(c => char.IsLower(c)) || token.ToLower(CultureInfo.InvariantCulture) == token) + if (token.All(c => char.IsLower(c)) || token.ToLower() == token) return 1; if ((char.IsUpper(token.First()) && token.Skip(1).All(c => char.IsLower(c))) diff --git a/zxcvbn-core/TimeEstimates.cs b/zxcvbn-core/TimeEstimates.cs index 8a2d089..c03cbba 100644 --- a/zxcvbn-core/TimeEstimates.cs +++ b/zxcvbn-core/TimeEstimates.cs @@ -16,14 +16,14 @@ public static AttackTimes EstimateAttackTimes(double guesses) { var crackTimesSeconds = new CrackTimes { - OfflineFastHashing1e10PerSecond = guesses / (100 / 3600), + OfflineFastHashing1e10PerSecond = guesses / (100.0 / 3600), OfflineSlowHashing1e4PerSecond = guesses / 10, OnlineNoThrottling10PerSecond = guesses / 1e4, OnlineThrottling100PerHour = guesses / 1e10, }; var crackTimesDisplay = new CrackTimesDisplay { - OfflineFastHashing1e10PerSecond = DisplayTime(guesses / (100 / 3600)), + OfflineFastHashing1e10PerSecond = DisplayTime(guesses / (100.0 / 3600)), OfflineSlowHashing1e4PerSecond = DisplayTime(guesses / 10), OnlineNoThrottling10PerSecond = DisplayTime(guesses / 1e4), OnlineThrottling100PerHour = DisplayTime(guesses / 1e10), @@ -44,7 +44,7 @@ private static string DisplayTime(double seconds) const double day = hour * 24; const double month = day * 31; const double year = month * 12; - const double century = year * 1000; + const double century = year * 100; int? displayNumber = null; string displayString; diff --git a/zxcvbn-core/zxcvbn-core.csproj b/zxcvbn-core/zxcvbn-core.csproj index 4007cea..960bedd 100644 --- a/zxcvbn-core/zxcvbn-core.csproj +++ b/zxcvbn-core/zxcvbn-core.csproj @@ -3,7 +3,7 @@ C#/.NET port of Dan Wheeler/DropBox's Zxcvbn JS password strength estimation library. Updated for .Net Core. mickford;Tony Richards (trichards57);Dan Wheeler;DropBox - netstandard2.0 + netstandard2.0;net451;netstandard1.3 zxcvbn-core zxcvbn-core password;strength;validation;zxcvbn @@ -37,5 +37,4 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - \ No newline at end of file + diff --git a/zxcvbn-cs.sln b/zxcvbn-cs.sln index 1bcbe22..e3b53f4 100644 --- a/zxcvbn-cs.sln +++ b/zxcvbn-cs.sln @@ -20,7 +20,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zxcvbn-core", "zxcvbn-core\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zxcvbn-core-test", "zxcvbn-core-test\zxcvbn-core-test.csproj", "{65B256F9-4874-4D6F-9A46-D881FAB0215B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zxcvbn-core-list-builder", "zxcvbn-core-list-builder\zxcvbn-core-list-builder.csproj", "{80BA1964-B98A-4D34-95B8-DFA51CD3A378}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zxcvbn-core-list-builder", "zxcvbn-core-list-builder\zxcvbn-core-list-builder.csproj", "{80BA1964-B98A-4D34-95B8-DFA51CD3A378}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution