/// <summary> /// Perform the password matching on the given password and user inputs. /// </summary> /// <param name="password">The password to assess.</param> /// <param name="userInputs">Optionally, an enumarable of user data.</param> /// <returns>Result for the password.</returns> public static Result EvaluatePassword(string password, IEnumerable <string> userInputs = null) { password = password ?? string.Empty; userInputs = userInputs ?? Enumerable.Empty <string>(); var timer = System.Diagnostics.Stopwatch.StartNew(); var matches = GetAllMatches(password, userInputs); var result = PasswordScoring.MostGuessableMatchSequence(password, matches); timer.Stop(); var attackTimes = TimeEstimates.EstimateAttackTimes(result.Guesses); var feedback = Feedback.GetFeedback(result.Score, result.Sequence); var finalResult = new Result { CalcTime = timer.ElapsedMilliseconds, CrackTime = attackTimes.CrackTimesSeconds, CrackTimeDisplay = attackTimes.CrackTimesDisplay, Score = attackTimes.Score, MatchSequence = result.Sequence, Guesses = result.Guesses, Password = result.Password, Feedback = feedback, }; return(finalResult); }
/// <summary> /// Returns a new result structure initialised with data for the lowest entropy result of all of the matches passed in, adding brute-force /// matches where there are no lesser entropy found pattern matches. /// </summary> /// <param name="matches">Password being evaluated</param> /// <param name="password">List of matches found against the password</param> /// <returns>A result object for the lowest entropy match sequence</returns> private PasswordMetricResult FindMinimumEntropyMatch(string password, IEnumerable <Match> matches) { var bruteforce_cardinality = PasswordScoring.PasswordCardinality(password); // Minimum entropy up to position k in the password var minimumEntropyToIndex = new double[password.Length]; var bestMatchForIndex = new Match[password.Length]; for (var k = 0; k < password.Length; k++) { // Start with bruteforce scenario added to previous sequence to beat minimumEntropyToIndex[k] = (k == 0 ? 0 : minimumEntropyToIndex[k - 1]) + Math.Log(bruteforce_cardinality, 2); // All matches that end at the current character, test to see if the entropy is less foreach (var match in matches.Where(m => m.End == k)) { var candidate_entropy = (match.Begin <= 0 ? 0 : minimumEntropyToIndex[match.Begin - 1]) + match.Entropy; if (candidate_entropy < minimumEntropyToIndex[k]) { minimumEntropyToIndex[k] = candidate_entropy; bestMatchForIndex[k] = match; } } } // Walk backwards through lowest entropy matches, to build the best password sequence var matchSequence = new List <Match>(); for (var k = password.Length - 1; k >= 0; k--) { if (bestMatchForIndex[k] != null) { matchSequence.Add(bestMatchForIndex[k]); k = bestMatchForIndex[k].Begin; // Jump back to start of match } } matchSequence.Reverse(); // The match sequence might have gaps, fill in with bruteforce matching // After this the matches in matchSequence must cover the whole string (i.e. match[k].j == match[k + 1].i - 1) if (matchSequence.Count == 0) { // To make things easy, we'll separate out the case where there are no matches so everything is bruteforced matchSequence.Add(new Match() { Begin = 0, End = password.Length, Token = password, Cardinality = bruteforce_cardinality, Pattern = BruteforcePattern, Entropy = Math.Log(Math.Pow(bruteforce_cardinality, password.Length), 2) }); } else { // There are matches, so find the gaps and fill them in var matchSequenceCopy = new List <Match>(); for (var k = 0; k < matchSequence.Count; k++) { var m1 = matchSequence[k]; var m2 = (k < matchSequence.Count - 1 ? matchSequence[k + 1] : new Match() { Begin = password.Length }); // Next match, or a match past the end of the password matchSequenceCopy.Add(m1); if (m1.End < m2.Begin - 1) { // Fill in gap var ns = m1.End + 1; var ne = m2.Begin - 1; matchSequenceCopy.Add(new Match() { Begin = ns, End = ne, Token = password.Substring(ns, ne - ns + 1), Cardinality = bruteforce_cardinality, Pattern = BruteforcePattern, Entropy = Math.Log(Math.Pow(bruteforce_cardinality, ne - ns + 1), 2) }); } } matchSequence = matchSequenceCopy; } var minEntropy = (password.Length == 0 ? 0 : minimumEntropyToIndex[password.Length - 1]); var crackTime = PasswordScoring.EntropyToCrackTime(minEntropy); var result = new PasswordMetricResult(); result.Password = password; result.Entropy = Math.Round(minEntropy, 3); result.MatchSequence = matchSequence; result.CrackTime = Math.Round(crackTime, 3); result.CrackTimeDisplay = Utility.DisplayTime(crackTime, this.translation); result.Score = PasswordScoring.CrackTimeToScore(crackTime); //starting feedback if ((matchSequence == null) || (matchSequence.Count() == 0)) { result.warning = Warning.Default; result.suggestions.Clear(); result.suggestions.Add(Suggestion.Default); } else { //no Feedback if score is good or great if (result.Score > 2) { result.warning = Warning.Empty; result.suggestions.Clear(); result.suggestions.Add(Suggestion.Empty); } else { //tie feedback to the longest match for longer sequences Match longestMatch = GetLongestMatch(matchSequence); GetMatchFeedback(longestMatch, matchSequence.Count() == 1, result); result.suggestions.Insert(0, Suggestion.AddAnotherWordOrTwo); } } return(result); }
private void GetDictionaryMatchFeedback(DictionaryMatch match, bool isSoleMatch, PasswordMetricResult result) { if (match.DictionaryName.Equals("passwords")) { //todo: add support for reversed words if (isSoleMatch == true && !(match is L33tDictionaryMatch)) { if (match.Rank <= 10) { result.warning = Warning.Top10Passwords; } else if (match.Rank <= 100) { result.warning = Warning.Top100Passwords; } else { result.warning = Warning.CommonPasswords; } } else if (PasswordScoring.CrackTimeToScore(PasswordScoring.EntropyToCrackTime(match.Entropy)) <= 1) { result.warning = Warning.SimilarCommonPasswords; } } else if (match.DictionaryName.Equals("english")) { if (isSoleMatch == true) { result.warning = Warning.WordEasy; } } else if (match.DictionaryName.Equals("surnames") || match.DictionaryName.Equals("male_names") || match.DictionaryName.Equals("female_names")) { if (isSoleMatch == true) { result.warning = Warning.NameSurnamesEasy; } else { result.warning = Warning.CommonNameSurnamesEasy; } } else { result.warning = Warning.Empty; } string word = match.Token; if (Regex.IsMatch(word, PasswordScoring.StartUpper)) { result.suggestions.Add(Suggestion.CapsDontHelp); } else if (Regex.IsMatch(word, PasswordScoring.AllUpper) && !word.Equals(word.ToLowerInvariant())) { result.suggestions.Add(Suggestion.AllCapsEasy); } //todo: add support for reversed words //if match.reversed and match.token.length >= 4 // suggestions.push "Reversed words aren't much harder to guess" if (match is L33tDictionaryMatch) { result.suggestions.Add(Suggestion.PredictableSubstitutionsEasy); } }
/// <summary> /// Estimate the extra entropy in a token that comes from mixing upper and lowercase letters. /// This has been moved to a static function so that it can be used in multiple entropy calculations. /// </summary> /// <param name="word">The word to calculate uppercase entropy for</param> /// <returns>An estimation of the entropy gained from casing in <paramref name="word"/></returns> public static double CalculateUppercaseEntropy(string word) { const string StartUpper = "^[A-Z][^A-Z]+$"; const string EndUpper = "^[^A-Z]+[A-Z]$"; const string AllUpper = "^[^a-z]+$"; const string AllLower = "^[^A-Z]+$"; if (Regex.IsMatch(word, AllLower)) { return(0); } // If the word is all uppercase add's only one bit of entropy, add only one bit for initial/end single cap only if (new[] { StartUpper, EndUpper, AllUpper }.Any(re => Regex.IsMatch(word, re))) { return(1); } var lowers = word.Where(c => 'a' <= c && c <= 'z').Count(); var uppers = word.Where(c => 'A' <= c && c <= 'Z').Count(); // Calculate numer of ways to capitalise (or inverse if there are fewer lowercase chars) and return lg for entropy return(Math.Log(Enumerable.Range(0, Math.Min(uppers, lowers) + 1).Sum(i => PasswordScoring.Binomial(uppers + lowers, i)), 2)); }
/// <summary> /// Returns a new result structure initialised with data for the lowest entropy result of all of the matches passed in, adding brute-force /// matches where there are no lesser entropy found pattern matches. /// </summary> /// <param name="matches">Password being evaluated</param> /// <param name="password">List of matches found against the password</param> /// <returns>A result object for the lowest entropy match sequence</returns> private Result FindMinimumEntropyMatch(string password, IEnumerable <Match> matches) { var bruteforce_cardinality = PasswordScoring.PasswordCardinality(password); // Minimum entropy up to position k in the password var minimumEntropyToIndex = new double[password.Length]; var bestMatchForIndex = new Match[password.Length]; for (var k = 0; k < password.Length; k++) { // Start with bruteforce scenario added to previous sequence to beat minimumEntropyToIndex[k] = (k == 0 ? 0 : minimumEntropyToIndex[k - 1]) + Math.Log(bruteforce_cardinality, 2); // All matches that end at the current character, test to see if the entropy is less foreach (var match in matches.Where(m => m.j == k)) { var candidate_entropy = (match.i <= 0 ? 0 : minimumEntropyToIndex[match.i - 1]) + match.Entropy; if (candidate_entropy < minimumEntropyToIndex[k]) { minimumEntropyToIndex[k] = candidate_entropy; bestMatchForIndex[k] = match; } } } // Walk backwards through lowest entropy matches, to build the best password sequence var matchSequence = new List <Match>(); for (var k = password.Length - 1; k >= 0; k--) { if (bestMatchForIndex[k] != null) { matchSequence.Add(bestMatchForIndex[k]); k = bestMatchForIndex[k].i; // Jump back to start of match } } matchSequence.Reverse(); // The match sequence might have gaps, fill in with bruteforce matching // After this the matches in matchSequence must cover the whole string (i.e. match[k].j == match[k + 1].i - 1) if (matchSequence.Count == 0) { // To make things easy, we'll separate out the case where there are no matches so everything is bruteforced matchSequence.Add(new Match() { i = 0, j = password.Length, Token = password, Cardinality = bruteforce_cardinality, Pattern = BruteforcePattern, Entropy = Math.Log(Math.Pow(bruteforce_cardinality, password.Length), 2) }); } else { // There are matches, so find the gaps and fill them in var matchSequenceCopy = new List <Match>(); for (var k = 0; k < matchSequence.Count; k++) { var m1 = matchSequence[k]; var m2 = (k < matchSequence.Count - 1 ? matchSequence[k + 1] : new Match() { i = password.Length }); // Next match, or a match past the end of the password matchSequenceCopy.Add(m1); if (m1.j < m2.i - 1) { // Fill in gap var ns = m1.j + 1; var ne = m2.i - 1; matchSequenceCopy.Add(new Match() { i = ns, j = ne, Token = password.Substring(ns, ne - ns + 1), Cardinality = bruteforce_cardinality, Pattern = BruteforcePattern, Entropy = Math.Log(Math.Pow(bruteforce_cardinality, ne - ns + 1), 2) }); } } matchSequence = matchSequenceCopy; } var minEntropy = (password.Length == 0 ? 0 : minimumEntropyToIndex[password.Length - 1]); var crackTime = PasswordScoring.EntropyToCrackTime(minEntropy); var result = new Result() { Password = password, Entropy = Math.Round(minEntropy, 3), MatchSequence = matchSequence, CrackTime = Math.Round(crackTime, 3), CrackTimeDisplay = Utility.DisplayTime(crackTime, translation), Score = PasswordScoring.CrackTimeToScore(crackTime) }; return(result); }
public static Result MostGuessableMatchSequence(string password, IEnumerable <Match> matches, bool _exclude_additive = false) { int n = password.Length; List <List <Match> > matchesByJ = new List <List <Match> >(); foreach (int i in EnumerableUtility.Range(0, n)) { matchesByJ.Add(new List <Match>()); } foreach (Match m in matches) { matchesByJ[m.j].Add(m); } foreach (List <Match> lst in matchesByJ) { lst.Sort(delegate(Match m1, Match m2) { return(m1.i - m2.i); }); } Optimal optimal = new Optimal(n); /* # helper: considers whether a length-l sequence ending at match m is better (fewer guesses) # than previously encountered sequences, updating state if so. */ void update(Match m, int l) { // helper: considers whether a length-l sequence ending at match m is better (fewer guesses) // than previously encountered sequences, updating state if so. int k = m.j; double pi = EstimateGuesses(m, password); if (l > 1) { /* # we're considering a length-l sequence ending with match m: # obtain the product term in the minimization function by multiplying m's guesses # by the product of the length-(l-1) sequence ending just before m, at m.i - 1. */ pi *= optimal.pi[m.i - 1][l - 1]; } // calculate the minimization func double g = factorial(l) * pi; if (!_exclude_additive) { g += Math.Pow(MIN_GUESSES_BEFORE_GROWING_SEQUENCE, l - 1); } // update state if new best // first see if any competing sequences covering this prefix, with l or fewer matches, // fare better than this sequence. if so, skip it and return foreach (KeyValuePair <int, double> val in optimal.g[k]) { int competingL = val.Key; double competingG = val.Value; if (competingL > l) { continue; } if (competingG <= g) { return; } } optimal.g[k][l] = g; optimal.m[k][l] = m; optimal.pi[k][l] = pi; } // helper: make bruteforce match objects spanning i to j, inclusive. Match makeBruteForceMatch(int i, int j) { Match m = new Match(); m.Token = password.Substring(i, j - i + 1); m.Pattern = "bruteforce"; m.i = i; m.j = j; return(m); } // helper: evaluate bruteforce matches ending at k. void bruteForceUpdate(int k) { // see eif a single bruteforce match spanning the k-prefix is optimal Match m = makeBruteForceMatch(0, k); update(m, 1); foreach (int i in EnumerableUtility.Range(1, k + 1)) { // generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k) // see if adding these new matches to any of the sequences in optimal[i-1] // leads to new bests m = makeBruteForceMatch(i, k); foreach (KeyValuePair <int, Match> val in optimal.m[i - 1]) { int l = val.Key; // corner: an optimal sequence will never have two adjacent bruteforce matches // it is strictly better to have a single bruteforce match spanning the same region: // same contribution to the guess product with a lower length // --> safe to skip those cases if (val.Value.Pattern != null && val.Value.Pattern.Equals("bruteforce")) { continue; } // try adding m to this length-l sequence update(m, l + 1); } } } // helper: step backwards through optimal.m starting at the end, // constructing the final optimal match sequence List <Match> unwind(int nN) { List <Match> optimalMatchSequenceList = new List <Match>(); int k = nN - 1; // find the final best seqence length and score int l = 1; double g = Double.MaxValue; foreach (KeyValuePair <int, double> val in optimal.g[k]) { int candidateL = val.Key; double candidateG = val.Value; if (candidateG < g) { l = candidateL; g = candidateG; } } while (k >= 0) { Match m = optimal.m[k][l]; optimalMatchSequenceList.Insert(0, m); k = m.i - 1; l--; } return(optimalMatchSequenceList); } foreach (int k in EnumerableUtility.Range(0, n)) { foreach (Match m in matchesByJ[k]) { if (m.i > 0) { foreach (int l in optimal.m[m.i - 1].Keys) { update(m, l + 1); } } else { update(m, 1); } } bruteForceUpdate(k); } List <Match> optimalMatchSequence = unwind(n); int optimalL = optimalMatchSequence.Count; double guesses = (password.Length == 0) ? 1 : optimal.g[n - 1][optimalL]; double seconds = guesses / Math.Pow(10, 4); double crackTime = PasswordScoring.GuessesToCrackTime(guesses); // UnityEngine.Debug.Log($"Seconds: {seconds}"); // Debug.Log($"Score: {PasswordScoring.GuessesToScore(guesses)}"); int score = PasswordScoring.GuessesToScore(guesses); return(new Result { Password = password, Guesses = guesses, GuessesLog10 = Math.Log10(guesses), MatchSequence = optimalMatchSequence, CrackTime = Math.Round(crackTime, 3), CrackTimeDisplay = Utility.DisplayTime(crackTime), Score = score }); }