From e479b1bf0a5f627cd05f2b19ae5f087aa945bea3 Mon Sep 17 00:00:00 2001 From: Nathan Button Date: Tue, 6 Oct 2015 12:43:37 -0600 Subject: [PATCH] Scoring partly done. Its now in a state that it can be used. =D --- .gitignore | 1 + adjacency/adjacmartix_test.go | 8 +- adjacency/adjcmartix.go | 41 ++--- match/match.go | 38 ++++ matching/matching.go | 289 ++++++++++++++++--------------- scoring/scoring.go | 315 ++++++++++++++++++++++++++++++++++ utils/math/mathutils.go | 33 ++-- utils/math/mathutils_test.go | 2 +- zxcvbn.go | 18 +- 9 files changed, 560 insertions(+), 185 deletions(-) create mode 100644 .gitignore create mode 100644 match/match.go create mode 100644 scoring/scoring.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eae064a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +zxcvbn diff --git a/adjacency/adjacmartix_test.go b/adjacency/adjacmartix_test.go index aaff9da..bc61e76 100644 --- a/adjacency/adjacmartix_test.go +++ b/adjacency/adjacmartix_test.go @@ -9,25 +9,25 @@ import ( nbutton: Really the value is not as important to me than they don't change, which happened during development. */ func TestCalculateDegreeQwert(t *testing.T) { - avgDegreeQwert := AdjacencyGph.Qwerty.CalculateAvgDegree() + avgDegreeQwert := buildQwerty().CalculateAvgDegree() assert.Equal(t, float32(1.531915), avgDegreeQwert, "Avg degree for qwerty should be 1.531915") } func TestCalculateDegreeDvorak(t *testing.T) { - avgDegreeQwert := AdjacencyGph.Dvorak.CalculateAvgDegree() + avgDegreeQwert := buildDvorak().CalculateAvgDegree() assert.Equal(t, float32(1.531915), avgDegreeQwert, "Avg degree for dvorak should be 1.531915") } func TestCalculateDegreeKeypad(t *testing.T) { - avgDegreeQwert := AdjacencyGph.Keypad.CalculateAvgDegree() + avgDegreeQwert := buildKeypad().CalculateAvgDegree() assert.Equal(t, float32(0.62222224), avgDegreeQwert, "Avg degree for keypad should be 0.62222224") } func TestCalculateDegreeMacKepad(t *testing.T) { - avgDegreeQwert := AdjacencyGph.MacKeypad.CalculateAvgDegree() + avgDegreeQwert := buildMacKeypad().CalculateAvgDegree() assert.Equal(t, float32(0.6458333), avgDegreeQwert, "Avg degree for mackeyPad should be 0.6458333") } diff --git a/adjacency/adjcmartix.go b/adjacency/adjcmartix.go index d5b66b6..6ed22e7 100644 --- a/adjacency/adjcmartix.go +++ b/adjacency/adjcmartix.go @@ -10,42 +10,31 @@ import ( type AdjacencyGraph struct { Graph map[string][6]string - averageDegree float32 + averageDegree float64 Name string } -var AdjacencyGph []AdjacencyGraph; -func init(){ - //todo get currentloc so that i don't have to know the whole path - log.SetFlags(log.Lshortfile) - AdjacencyGph = append(AdjacencyGph, buildQwerty()) - AdjacencyGph = append(AdjacencyGph, buildDvorak()) - AdjacencyGph = append(AdjacencyGph, buildKeypad()) - AdjacencyGph = append(AdjacencyGph, buildMacKeypad()) - -} - func buildQwerty() AdjacencyGraph { - filePath, _ := filepath.Abs("adjacency/Qwerty.json") - return getAdjancencyGraphFromFile(filePath, "qwerty") + filePath, _ := filepath.Abs("Qwerty.json") + return GetAdjancencyGraphFromFile(filePath, "qwerty") } func buildDvorak() AdjacencyGraph { - filePath, _ := filepath.Abs("adjacency/Dvorak.json") - return getAdjancencyGraphFromFile(filePath, "dvorak") + filePath, _ := filepath.Abs("Dvorak.json") + return GetAdjancencyGraphFromFile(filePath, "dvorak") } func buildKeypad() AdjacencyGraph { - filePath, _ := filepath.Abs("adjacency/Keypad.json") - return getAdjancencyGraphFromFile(filePath, "keypad") + filePath, _ := filepath.Abs("Keypad.json") + return GetAdjancencyGraphFromFile(filePath, "keypad") } func buildMacKeypad() AdjacencyGraph { - filePath, _ := filepath.Abs("adjacency/MacKeypad.json") - return getAdjancencyGraphFromFile(filePath, "mac_keypad") + filePath, _ := filepath.Abs("MacKeypad.json") + return GetAdjancencyGraphFromFile(filePath, "mac_keypad") } -func getAdjancencyGraphFromFile(filePath string, name string) AdjacencyGraph { +func GetAdjancencyGraphFromFile(filePath string, name string) AdjacencyGraph { data, err := ioutil.ReadFile(filePath) if err != nil { @@ -65,17 +54,17 @@ func getAdjancencyGraphFromFile(filePath string, name string) AdjacencyGraph { //on qwerty, 'g' has degree 6, being adjacent to 'ftyhbv'. '\' has degree 1. //this calculates the average over all keys. //TODO double check that i ported this correctly scoring.coffee ln 5 -func (adjGrp AdjacencyGraph) CalculateAvgDegree() (float32) { - if adjGrp.averageDegree != float32(0) { +func (adjGrp AdjacencyGraph) CalculateAvgDegree() (float64) { + if adjGrp.averageDegree != float64(0) { return adjGrp.averageDegree } - var avg float32 - var count float32 + var avg float64 + var count float64 for _, value := range adjGrp.Graph { for _, char := range value { if char != "" || char != " " { - avg += float32(len(char)) + avg += float64(len(char)) count++ } } diff --git a/match/match.go b/match/match.go new file mode 100644 index 0000000..2a82197 --- /dev/null +++ b/match/match.go @@ -0,0 +1,38 @@ +package match + +type Matches []Match +func (s Matches)Len() int { + return len(s) +} +func (s Matches)Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s Matches) Less(i, j int) bool { + if s[i].I < s[j].I { + return true + } else if s[i].I == s[j].I { + return s[i].J < s[j].J + } else { + return false + } +} +type Match struct { + Pattern string + I, J int + Token string + MatchedWord string + Rank float64 + DictionaryName string + Turns int + ShiftedCount int + Entropy float64 +} + +type DateMatch struct { + Pattern string + I, J int + Token string + Separator string + Day, Month, Year int64 + +} \ No newline at end of file diff --git a/matching/matching.go b/matching/matching.go index c3fd058..432b5b8 100644 --- a/matching/matching.go +++ b/matching/matching.go @@ -1,85 +1,95 @@ package matching import ( "strings" - "github.com/bradfitz/slice" "regexp" "strconv" "zxcvbn-go/frequency" "path/filepath" "zxcvbn-go/adjacency" + "zxcvbn-go/match" + "sort" ) var ( - DICTIONARY_MATCHERS []func(password string) []Match + DICTIONARY_MATCHERS []func(password string) []match.Match + MATCHERS []func(password string) []match.Match + ADJACENCY_GRAPHS []adjacency.AdjacencyGraph; + KEYBOARD_STARTING_POSITIONS int + KEYBOARD_AVG_DEGREE float64 + KEYPAD_STARTING_POSITIONS int + KEYPAD_AVG_DEGREE float64 ) - const ( - //TODO: Invalid regex for Golang since it has a \2 - DATE_RX_YEAR_SUFFIX string = `((\d{1,2})(\s|-|\/|\\|_|\.)(\d{1,2})(\s|-|\/|\\|_|\.)(19\d{2}|200\d|201\d|\d{2}))` - DATE_RX_YEAR_PREFIX string = `((19\d{2}|200\d|201\d|\d{2})(\s|-|/|\\|_|\.)(\d{1,2})(\s|-|/|\\|_|\.)(\d{1,2}))` - DATE_WITHOUT_SEP_MATCH string = `\d{4,8}` - ) -type Match struct { - Pattern string - I, J int - Token string - MatchedWord string - Rank int - DictionaryName string - Turns int - ShiftedCount int -} +const ( + DATE_RX_YEAR_SUFFIX string = `((\d{1,2})(\s|-|\/|\\|_|\.)(\d{1,2})(\s|-|\/|\\|_|\.)(19\d{2}|200\d|201\d|\d{2}))` + DATE_RX_YEAR_PREFIX string = `((19\d{2}|200\d|201\d|\d{2})(\s|-|/|\\|_|\.)(\d{1,2})(\s|-|/|\\|_|\.)(\d{1,2}))` + DATE_WITHOUT_SEP_MATCH string = `\d{4,8}` +) -type DateMatch struct { - Pattern string - I, J int - Token string - Separator string - Day, Month, Year int64 - -} func init() { - loadFrequencyList() + } -func Omnimatch(password string, userInputs []string) []Match { +func Omnimatch(password string, userInputs []string) (matches []match.Match) { - userInputMatcher := buildDictMatcher("user_inputs", buildRankedDict(userInputs)) - matches := userInputMatcher(password) - - for _, matcher := range DICTIONARY_MATCHERS { - mtemp := matcher(password) - for _,v:= range mtemp { - matches = append(matches, v) - } + if DICTIONARY_MATCHERS == nil || ADJACENCY_GRAPHS == nil { + loadFrequencyList() } - slice.Sort(matches,func(i, j int)bool{ - //TODO fix this - return false; - }) + + if userInputs != nil { + userInputMatcher := buildDictMatcher("user_inputs", buildRankedDict(userInputs)) + matches = userInputMatcher(password) + } + + for _, matcher := range MATCHERS { + mtemp := matcher(password) + matches = append(matches, mtemp...) + } + sort.Sort(match.Matches(matches)) return matches } -func loadFrequencyList(){ +func loadFrequencyList() { maleFilePath, _ := filepath.Abs("frequency/MaleNames.json") femaleFilePath, _ := filepath.Abs("frequency/FemaleNames.json") surnameFilePath, _ := filepath.Abs("frequency/Surnames.json") englishFilePath, _ := filepath.Abs("frequency/English.json") passwordsFilePath, _ := filepath.Abs("frequency/Passwords.json") - DICTIONARY_MATCHERS = append(DICTIONARY_MATCHERS, buildDictMatcher("MaleNames", buildRankedDict(frequency.GetStringListFromFile(maleFilePath)))) DICTIONARY_MATCHERS = append(DICTIONARY_MATCHERS, buildDictMatcher("FemaleNames", buildRankedDict(frequency.GetStringListFromFile(femaleFilePath)))) DICTIONARY_MATCHERS = append(DICTIONARY_MATCHERS, buildDictMatcher("Surnames", buildRankedDict(frequency.GetStringListFromFile(surnameFilePath)))) DICTIONARY_MATCHERS = append(DICTIONARY_MATCHERS, buildDictMatcher("English", buildRankedDict(frequency.GetStringListFromFile(englishFilePath)))) DICTIONARY_MATCHERS = append(DICTIONARY_MATCHERS, buildDictMatcher("Passwords", buildRankedDict(frequency.GetStringListFromFile(passwordsFilePath)))) + + qwertyfilePath, _ := filepath.Abs("adjacency/Qwerty.json") + dvorakfilePath, _ := filepath.Abs("adjacency/Dvorak.json") + keypadfilePath, _ := filepath.Abs("adjacency/Keypad.json") + macKeypadfilePath, _ := filepath.Abs("adjacency/MacKeypad.json") + + qwertGraph := adjacency.GetAdjancencyGraphFromFile(qwertyfilePath, "qwert") + keypadGraph := adjacency.GetAdjancencyGraphFromFile(keypadfilePath, "keypad") + + + KEYBOARD_AVG_DEGREE = qwertGraph.CalculateAvgDegree() + KEYBOARD_STARTING_POSITIONS = len(qwertGraph.Graph) + KEYPAD_AVG_DEGREE = keypadGraph.CalculateAvgDegree() + KEYPAD_STARTING_POSITIONS = len(keypadGraph.Graph) + + ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, qwertGraph) + ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.GetAdjancencyGraphFromFile(dvorakfilePath, "dvorak")) + ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, keypadGraph) + ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.GetAdjancencyGraphFromFile(macKeypadfilePath, "macKepad")) + + MATCHERS = append(MATCHERS, DICTIONARY_MATCHERS...) + MATCHERS = append(MATCHERS, SpatialMatch) } -func buildDictMatcher(dictName string, rankedDict map[string]int) func(password string) []Match { - return func (password string) []Match{ - matches := dictionaryMatch(password, rankedDict) +func buildDictMatcher(dictName string, rankedDict map[string]int) func(password string) []match.Match { + return func(password string) []match.Match { + matches := dictionaryMatch(password, dictName, rankedDict) for _, v := range matches { v.DictionaryName = dictName } @@ -88,21 +98,22 @@ func buildDictMatcher(dictName string, rankedDict map[string]int) func(password } -func dictionaryMatch(password string, rankedDict map[string]int) []Match{ +func dictionaryMatch(password string, dictionaryName string, rankedDict map[string]int) []match.Match { length := len(password) - var results []Match + var results []match.Match pwLower := strings.ToLower(password) - for i :=0; i=6 { + if length >= 6 { //4-digit year prefix candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[4:], v[0:4], i, j)) //4-digit year sufix - candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[0:lastIndex-4], v[lastIndex-4:], i, j)) + candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[0:lastIndex - 4], v[lastIndex - 4:], i, j)) } - var candidatesRoundTwo []DateMatch + var candidatesRoundTwo []match.DateMatch for _, c := range candidatesRoundOne { if len(c.DayMonth) == 2 { candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0], c.DayMonth[1], c.Year, c.I, c.J)) @@ -227,84 +235,81 @@ func buildDateMatchCandidate(dayMonth, year string, i, j int) DateMatchCandidate return DateMatchCandidate{DayMonth: dayMonth, Year:year, I:i, J:j} } -func buildDateMatchCandidateTwo(day, month byte, year string, i, j int) DateMatch { +func buildDateMatchCandidateTwo(day, month byte, year string, i, j int) match.DateMatch { sDay := string(day) sMonth := string(month) intDay, _ := strconv.ParseInt(sDay, 10, 16) intMonth, _ := strconv.ParseInt(sMonth, 10, 16) intYear, _ := strconv.ParseInt(year, 10, 16) - return DateMatch{Day:intDay, Month:intMonth, Year:intYear, I:i, J:j} + return match.DateMatch{Day:intDay, Month:intMonth, Year:intYear, I:i, J:j} } -func SpatialMatch(password string) []Match{ - var matches []Match - for _,graph := range adjacency.AdjacencyGph { +func SpatialMatch(password string) (matches []match.Match) { + for _, graph := range ADJACENCY_GRAPHS { matches = append(matches, spatialMatchHelper(password, graph)...) } - return matches } -func spatialMatchHelper(password string, graph adjacency.AdjacencyGraph) []Match{ - var matches []Match - for i := 0; i < len(password) -1; { - j := i+1 - lastDirection := -99 //and int that it should never be! - turns := 0 - shiftedCount := 0 +func spatialMatchHelper(password string, graph adjacency.AdjacencyGraph) (matches []match.Match) { + for i := 0; i < len(password) - 1; { + j := i + 1 + lastDirection := -99 //and int that it should never be! + turns := 0 + shiftedCount := 0 - for ;; { - prevChar := password[j-1] - found := false - foundDirection := -1 - curDirection := -1 - adjacents := graph.Graph[string(prevChar)] -// Consider growing pattern by one character if j hasn't gone over the edge - if j < len(password) { - curChar := password[j] - for _,adj := range adjacents { - curDirection += 1 + for ;; { + prevChar := password[j - 1] + found := false + foundDirection := -1 + curDirection := -1 + adjacents := graph.Graph[string(prevChar)] + // Consider growing pattern by one character if j hasn't gone over the edge + if j < len(password) { + curChar := password[j] + for _, adj := range adjacents { + curDirection += 1 - if strings.Index(adj, string(curChar)) != -1 { - found = true - foundDirection = curDirection + if strings.Index(adj, string(curChar)) != -1 { + found = true + foundDirection = curDirection - if strings.Index(adj, string(curChar)) == 1 { -// index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc. -// for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted. + if strings.Index(adj, string(curChar)) == 1 { + // index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc. + // for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted. - shiftedCount += 1 - } - - if lastDirection != foundDirection { -// adding a turn is correct even in the initial case when last_direction is null: -// every spatial pattern starts with a turn. - turns += 1 - lastDirection = foundDirection - } - break + shiftedCount += 1 } - } - } -// if the current pattern continued, extend j and try to grow again - if found { - j +=1 - } else { -// otherwise push the pattern discovered so far, if any... - -// don't consider length 1 or 2 chains. - if j - i > 2 { - matches = append(matches, Match{Pattern:"spatial", I:i, J:j, Token:password[i:j], DictionaryName:graph.Name, Turns:turns, ShiftedCount:shiftedCount }) + if lastDirection != foundDirection { + // adding a turn is correct even in the initial case when last_direction is null: + // every spatial pattern starts with a turn. + turns += 1 + lastDirection = foundDirection + } + break } -// . . . and then start a new search from the rest of the password - i = j - break } } + // if the current pattern continued, extend j and try to grow again + if found { + j += 1 + } else { + // otherwise push the pattern discovered so far, if any... + + // don't consider length 1 or 2 chains. + if j - i > 2 { + matches = append(matches, match.Match{Pattern:"spatial", I:i, J:j - 1, Token:password[i:j], DictionaryName:graph.Name, Turns:turns, ShiftedCount:shiftedCount }) + } + // . . . and then start a new search from the rest of the password + i = j + break + } } + + } return matches } diff --git a/scoring/scoring.go b/scoring/scoring.go new file mode 100644 index 0000000..9d74d54 --- /dev/null +++ b/scoring/scoring.go @@ -0,0 +1,315 @@ +package scoring +import ( + "zxcvbn-go/match" + "unicode" + "fmt" + "math" + "sort" + "regexp" + "zxcvbn-go/utils/math" + "zxcvbn-go/matching" +) + + +const ( + START_UPPER string = `^[A-Z][^A-Z]+$` + END_UPPER string = `^[^A-Z]+[A-Z]$'` + ALL_UPPER string = `^[A-Z]+$` + SINGLE_GUESS float64 = 0.010 + NUM_ATTACKERS float64 = 100 + SECONDS_PER_GUESS float64 = SINGLE_GUESS / NUM_ATTACKERS +) +type MinEntropyMatch struct { + Password string + Entropy float64 + MatchSequence []match.Match //TODO ? + CrackTime float64 + CrackTimeDisplay string + Score int + CalcTime float64 +} + +/* +Returns minimum entropy TODO + + Takes a list of overlapping matches, returns the non-overlapping sublist with + minimum entropy. O(nm) dp alg for length-n password with m candidate matches. + */ +func MinimumEntropyMatchSequence(password string, matches []match.Match) MinEntropyMatch { + bruteforceCardinality := float64(calcBruteforceCardinality(password)) + upToK := make([]float64, len(password)) + backPointers := make([]match.Match, len(password)) + + for k := 0; k < len(password); k++ { + upToK[k] = get(upToK, k - 1) + math.Log2(bruteforceCardinality) + + for _, match := range matches { + if match.J != k { + continue + } + + i, j := match.I, match.J + // see if best entropy up to i-1 + entropy of match is less that current min at j + upTo := get(upToK, i - 1) + calculatedEntropy := calcEntropy(match) + match.Entropy = calculatedEntropy + candidateEntropy := upTo + calculatedEntropy + + if candidateEntropy < upToK[j] { + upToK[j] = candidateEntropy + match.Entropy = candidateEntropy + backPointers[j] = match + } + } + } + + // walk backwards and decode the best sequence + var matchSequence []match.Match + passwordLen := len(password) + passwordLen-- + for k := passwordLen; k >= 0; { + match := backPointers[k] + if match.Pattern != "" { + matchSequence = append(matchSequence, match) + k = match.I - 1 + + } else { + k-- + } + + } + sort.Sort(match.Matches(matchSequence)) + + makeBruteForecMatch := func(i, j int) match.Match { + return match.Match{Pattern:"bruteforce", + I:i, + J:j, + Token:password[i:j + 1], + Entropy:math.Log2(math.Pow(bruteforceCardinality, float64(j - i)))} + + } + + k := 0 + var matchSequenceCopy []match.Match + for _, match := range matchSequence { + i, j := match.I, match.J + if i - k > 0 { + matchSequenceCopy = append(matchSequenceCopy, makeBruteForecMatch(k, i - 1)) + } + k = j + 1 + matchSequenceCopy = append(matchSequenceCopy, match) + } + + if k < len(password) { + matchSequenceCopy = append(matchSequenceCopy, makeBruteForecMatch(k, len(password) - 1)) + } + var minEntropy float64 + if len(password) == 0 { + minEntropy = float64(0) + } else { + minEntropy = upToK[len(password) - 1 ] + } + + crackTime := roundToXDigits(entropyToCrackTime(minEntropy), 3) + return MinEntropyMatch{Password:password, + Entropy:roundToXDigits(minEntropy, 3), + MatchSequence:matchSequenceCopy, + CrackTime:crackTime, + CrackTimeDisplay:displayTime(crackTime), + Score:crackTimeToScore(crackTime)} + +} +func get(a []float64, i int) float64 { + if i < 0 || i >= len(a) { + return float64(0) + } + + return a[i] +} +func calcBruteforceCardinality(password string) int { + lower, upper, digits, symbols := 0, 0, 0, 0 + + for _, char := range password { + if unicode.IsLower(char) { + lower = 26 + } else if unicode.IsDigit(char) { + digits = 10 + } else if unicode.IsUpper(char) { + upper = 26 + } else { + symbols = 33 + } + } + + cardinality := lower + upper + digits + symbols + return cardinality +} + +func calcEntropy(match match.Match) float64 { + if match.Entropy > float64(0) { + return match.Entropy + } + + var entropy float64 + if match.Pattern == "dictionary" { + entropy = dictionaryEntropy(match) + } else if match.Pattern == "spatial" { + entropy = spatialEntropy(match) + } + + match.Entropy = entropy + //TODO finish implement this. . . this looks to be the meat and potatoes of the calculation + return match.Entropy +} + +func dictionaryEntropy(match match.Match) float64 { + baseEntropy := math.Log2(match.Rank) + upperCaseEntropy := extraUpperCaseEntropy(match) + //TODO: L33t + return baseEntropy + upperCaseEntropy +} + +func spatialEntropy(match match.Match) float64 { + var s, d float64 + if match.DictionaryName == "qwerty" || match.DictionaryName == "dvorak" { + s = float64(matching.KEYBOARD_STARTING_POSITIONS) + d = matching.KEYBOARD_AVG_DEGREE + } else { + s = float64(matching.KEYPAD_STARTING_POSITIONS) + d = matching.KEYPAD_AVG_DEGREE + } + + possibilities := float64(0) + + lenght := float64(len(match.Token)) + t := match.Turns + + //TODO: Should this be <= or just < ? + //Estimate the number of possible patterns w/ lenght L or less with t turns or less + for i := float64(2); i <= lenght + 1; i++ { + possibleTurns := math.Min(float64(t), i - 1) + for j := float64(1); j <= possibleTurns + 1; j++ { + x := zxcvbn_math.NChoseK(i - 1, j - 1) * s * math.Pow(d, j) + possibilities += x + } + } + + entropy := math.Log2(possibilities) + + //add extra entropu for shifted keys. ( % instead of 5 A instead of a) + //Math is similar to extra entropy for uppercase letters in dictionary matches. + + if S := float64(match.ShiftedCount); S > float64(0) { + possibilities = float64(0) + U := lenght - S + + for i := float64(0); i < math.Min(S, U) + 1; i++ { + possibilities += zxcvbn_math.NChoseK(S + U, i) + } + + entropy += math.Log2(possibilities) + } + + return entropy +} +func extraUpperCaseEntropy(match match.Match) float64 { + word := match.Token + + allLower := true + + for _, char := range word { + if unicode.IsUpper(char) { + allLower = false + break + } + } + if allLower { + return float64(0) + } + + //a capitalized word is the most common capitalization scheme, + //so it only doubles the search space (uncapitalized + capitalized): 1 extra bit of entropy. + //allcaps and end-capitalized are common enough too, underestimate as 1 extra bit to be safe. + + for _, regex := range []string{START_UPPER, END_UPPER, ALL_UPPER} { + matcher := regexp.MustCompile(regex) + + if matcher.MatchString(word) { + return float64(1) + } + } + //Otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters with U uppercase letters or + //less. Or, if there's more uppercase than lower (for e.g. PASSwORD), the number of ways to lowercase U+L letters + //with L lowercase letters or less. + + countUpper, countLower := float64(0), float64(0) + for _, char := range word { + if unicode.IsUpper(char) { + countUpper++ + } else if unicode.IsLower(char) { + countLower++ + } + } + totalLenght := countLower + countUpper + var possibililities float64 + + for i := float64(0); i <= math.Min(countUpper, countLower); i++ { + possibililities += float64(zxcvbn_math.NChoseK(totalLenght, i)) + } + + if possibililities < 1 { + return float64(1) + } + + return float64(math.Log2(possibililities)) +} + +func entropyToCrackTime(entropy float64) float64 { + crackTime := (0.5 * math.Pow(float64(2), entropy)) * SECONDS_PER_GUESS + + return crackTime +} + +func roundToXDigits(number float64, digits int) float64 { + return zxcvbn_math.Round(number, .5, digits) +} + +func displayTime(seconds float64) string { + formater := "%d %s" + minute := float64(60) + hour := minute * float64(60) + day := hour * float64(24) + month := day * float64(31) + year := month * float64(12) + century := year * float64(100) + + if seconds < minute { + return "instant" + } else if seconds < hour { + return fmt.Sprintf(formater, (1 + math.Ceil(seconds / minute)), "minutes") + } else if seconds < day { + return fmt.Sprintf(formater, (1 + math.Ceil(seconds / hour)), "hours") + } else if seconds < month { + return fmt.Sprintf(formater, (1 + math.Ceil(seconds / day)), "days") + } else if seconds < year { + return fmt.Sprintf(formater, (1 + math.Ceil(seconds / month)), "months") + }else if seconds < century { + return fmt.Sprintf(formater, (1 + math.Ceil(seconds / century)), "years") + } else { + return "centuries" + } +} + +func crackTimeToScore(seconds float64) int { + if seconds < math.Pow(10, 2) { + return 0 + } else if seconds < math.Pow(10, 4) { + return 1 + } else if seconds < math.Pow(10, 6) { + return 2 + } else if seconds < math.Pow(10, 8) { + return 3 + } + + return 4 +} \ No newline at end of file diff --git a/utils/math/mathutils.go b/utils/math/mathutils.go index 5c7c065..6fb605e 100644 --- a/utils/math/mathutils.go +++ b/utils/math/mathutils.go @@ -1,4 +1,5 @@ -package math +package zxcvbn_math +import "math" @@ -7,22 +8,34 @@ I am surprised that I have to define these. . . Maybe i just didn't look hard en */ //http://blog.plover.com/math/choose.html -func NChoseK(n, k uint) uint64 { - uN := uint64(n) - uK := uint64(k) - if uK > uN { +func NChoseK(n, k float64) float64 { + if k > n { return 0 - } else if uK == 0 { + } else if k == 0 { return 1 } - var r uint64 = 1 + var r float64 = 1 - for d := uint64(1) ; d <= uK; d++ { - r *= uN + for d := float64(1); d <= k; d++ { + r *= n r /= d - uN-- + n-- } return r +} + +func Round(val float64, roundOn float64, places int) (newVal float64) { + var round float64 + pow := math.Pow(10, float64(places)) + digit := pow * val + _, div := math.Modf(digit) + if div >= roundOn { + round = math.Ceil(digit) + } else { + round = math.Floor(digit) + } + newVal = round / pow + return } \ No newline at end of file diff --git a/utils/math/mathutils_test.go b/utils/math/mathutils_test.go index 175f427..890002a 100644 --- a/utils/math/mathutils_test.go +++ b/utils/math/mathutils_test.go @@ -1,4 +1,4 @@ -package math +package zxcvbn_math import ( "testing" "github.com/stretchr/testify/assert" diff --git a/zxcvbn.go b/zxcvbn.go index 1969ae2..6d56c26 100644 --- a/zxcvbn.go +++ b/zxcvbn.go @@ -3,9 +3,23 @@ package main import ( "fmt" "zxcvbn-go/matching" + "zxcvbn-go/scoring" + "time" + "zxcvbn-go/utils/math" ) func main() { - fmt.Println("Start") - fmt.Println(matching.SpatialMatch("qw@!andghjandfTandftg")) + password :="qw@!abcdPLSB$6D" + fmt.Println(PasswordStrength(password, nil)) +} + +func PasswordStrength(password string, userInputs []string) scoring.MinEntropyMatch { + start := time.Now() + matches := matching.Omnimatch(password, userInputs) + result := scoring.MinimumEntropyMatchSequence(password, matches) + end := time.Now() + + calcTime := end.Nanosecond() - start.Nanosecond() + result.CalcTime = zxcvbn_math.Round(float64(calcTime)*time.Nanosecond.Seconds(), .5, 3) + return result } \ No newline at end of file