From 953e4e20862734ade67c8763550324fded753bbb Mon Sep 17 00:00:00 2001 From: Nathan Button Date: Tue, 28 Aug 2018 20:30:34 -0600 Subject: [PATCH] Fix some small preformance issues as well as get lint passing --- adjacency/adjcmartix.go | 98 ++++++++++++++++++++---------------- data/bindata.go | 2 +- entropy/entropyCalculator.go | 47 ++++++++--------- frequency/frequency.go | 27 +++++----- match/match.go | 4 ++ matching/dateMatchers.go | 47 +++++++++-------- matching/dictionaryMatch.go | 10 ++-- matching/leet.go | 10 ++-- matching/matching.go | 57 ++++++++++----------- matching/matching_test.go | 2 +- matching/repeatMatch.go | 5 +- matching/sequenceMatch.go | 9 ++-- matching/spatialMatch.go | 17 ++++--- scoring/scoring.go | 17 +++---- utils/math/mathutils.go | 8 +-- utils/math/mathutils_test.go | 2 +- zxcvbn.go | 3 +- zxcvbn_test.go | 37 ++++---------- 18 files changed, 203 insertions(+), 199 deletions(-) diff --git a/adjacency/adjcmartix.go b/adjacency/adjcmartix.go index 3320d59..66ad30b 100644 --- a/adjacency/adjcmartix.go +++ b/adjacency/adjcmartix.go @@ -3,65 +3,76 @@ package adjacency import ( "encoding/json" "log" - // "fmt" + "github.com/nbutton23/zxcvbn-go/data" ) -type AdjacencyGraph struct { +// Graph holds information about different graphs +type Graph struct { Graph map[string][]string averageDegree float64 Name string } -var AdjacencyGph = make(map[string]AdjacencyGraph) +// GraphMap is a map of all graphs +var GraphMap = make(map[string]Graph) func init() { - AdjacencyGph["qwerty"] = BuildQwerty() - AdjacencyGph["dvorak"] = BuildDvorak() - AdjacencyGph["keypad"] = BuildKeypad() - AdjacencyGph["macKeypad"] = BuildMacKeypad() - AdjacencyGph["l33t"] = BuildLeet() + GraphMap["qwerty"] = BuildQwerty() + GraphMap["dvorak"] = BuildDvorak() + GraphMap["keypad"] = BuildKeypad() + GraphMap["macKeypad"] = BuildMacKeypad() + GraphMap["l33t"] = BuildLeet() } -func BuildQwerty() AdjacencyGraph { - data, err := zxcvbn_data.Asset("data/Qwerty.json") +//BuildQwerty builds the Qwerty Graph +func BuildQwerty() Graph { + data, err := data.Asset("data/Qwerty.json") if err != nil { panic("Can't find asset") } - return GetAdjancencyGraphFromFile(data, "qwerty") -} -func BuildDvorak() AdjacencyGraph { - data, err := zxcvbn_data.Asset("data/Dvorak.json") - if err != nil { - panic("Can't find asset") - } - return GetAdjancencyGraphFromFile(data, "dvorak") -} -func BuildKeypad() AdjacencyGraph { - data, err := zxcvbn_data.Asset("data/Keypad.json") - if err != nil { - panic("Can't find asset") - } - return GetAdjancencyGraphFromFile(data, "keypad") -} -func BuildMacKeypad() AdjacencyGraph { - data, err := zxcvbn_data.Asset("data/MacKeypad.json") - if err != nil { - panic("Can't find asset") - } - return GetAdjancencyGraphFromFile(data, "mac_keypad") -} -func BuildLeet() AdjacencyGraph { - data, err := zxcvbn_data.Asset("data/L33t.json") - if err != nil { - panic("Can't find asset") - } - return GetAdjancencyGraphFromFile(data, "keypad") + return getAdjancencyGraphFromFile(data, "qwerty") } -func GetAdjancencyGraphFromFile(data []byte, name string) AdjacencyGraph { +//BuildDvorak builds the Dvorak Graph +func BuildDvorak() Graph { + data, err := data.Asset("data/Dvorak.json") + if err != nil { + panic("Can't find asset") + } + return getAdjancencyGraphFromFile(data, "dvorak") +} - var graph AdjacencyGraph +//BuildKeypad builds the Keypad Graph +func BuildKeypad() Graph { + data, err := data.Asset("data/Keypad.json") + if err != nil { + panic("Can't find asset") + } + return getAdjancencyGraphFromFile(data, "keypad") +} + +//BuildMacKeypad builds the Mac Keypad Graph +func BuildMacKeypad() Graph { + data, err := data.Asset("data/MacKeypad.json") + if err != nil { + panic("Can't find asset") + } + return getAdjancencyGraphFromFile(data, "mac_keypad") +} + +//BuildLeet builds the L33T Graph +func BuildLeet() Graph { + data, err := data.Asset("data/L33t.json") + if err != nil { + panic("Can't find asset") + } + return getAdjancencyGraphFromFile(data, "keypad") +} + +func getAdjancencyGraphFromFile(data []byte, name string) Graph { + + var graph Graph err := json.Unmarshal(data, &graph) if err != nil { log.Fatal(err) @@ -70,10 +81,11 @@ func GetAdjancencyGraphFromFile(data []byte, name string) AdjacencyGraph { return graph } +// CalculateAvgDegree calclates the average degree between nodes in the graph //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() float64 { +func (adjGrp Graph) CalculateAvgDegree() float64 { if adjGrp.averageDegree != float64(0) { return adjGrp.averageDegree } @@ -82,7 +94,7 @@ func (adjGrp AdjacencyGraph) CalculateAvgDegree() float64 { for _, value := range adjGrp.Graph { for _, char := range value { - if char != "" || char != " " { + if len(char) != 0 || char != " " { avg += float64(len(char)) count++ } diff --git a/data/bindata.go b/data/bindata.go index e5dfede..f3a0c01 100644 --- a/data/bindata.go +++ b/data/bindata.go @@ -12,7 +12,7 @@ // data/Surnames.json // DO NOT EDIT! -package zxcvbn_data +package data import ( "bytes" diff --git a/entropy/entropyCalculator.go b/entropy/entropyCalculator.go index 028732d..8f57ea0 100644 --- a/entropy/entropyCalculator.go +++ b/entropy/entropyCalculator.go @@ -10,19 +10,20 @@ import ( ) const ( - START_UPPER string = `^[A-Z][^A-Z]+$` - END_UPPER string = `^[^A-Z]+[A-Z]$'` - ALL_UPPER string = `^[A-Z]+$` - NUM_YEARS = float64(119) // years match against 1900 - 2019 - NUM_MONTHS = float64(12) - NUM_DAYS = float64(31) + numYears = float64(119) // years match against 1900 - 2019 + numMonths = float64(12) + numDays = float64(31) ) var ( - KEYPAD_STARTING_POSITIONS = len(adjacency.AdjacencyGph["keypad"].Graph) - KEYPAD_AVG_DEGREE = adjacency.AdjacencyGph["keypad"].CalculateAvgDegree() + startUpperRx = regexp.MustCompile(`^[A-Z][^A-Z]+$`) + endUpperRx = regexp.MustCompile(`^[^A-Z]+[A-Z]$'`) + allUpperRx = regexp.MustCompile(`^[A-Z]+$`) + keyPadStartingPositions = len(adjacency.GraphMap["keypad"].Graph) + keyPadAvgDegree = adjacency.GraphMap["keypad"].CalculateAvgDegree() ) +// DictionaryEntropy calculates the entropy of a dictionary match func DictionaryEntropy(match match.Match, rank float64) float64 { baseEntropy := math.Log2(rank) upperCaseEntropy := extraUpperCaseEntropy(match) @@ -49,9 +50,7 @@ func extraUpperCaseEntropy(match match.Match) float64 { //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) - + for _, matcher := range []*regexp.Regexp{startUpperRx, endUpperRx, allUpperRx} { if matcher.MatchString(word) { return float64(1) } @@ -72,7 +71,7 @@ func extraUpperCaseEntropy(match match.Match) float64 { var possibililities float64 for i := float64(0); i <= math.Min(countUpper, countLower); i++ { - possibililities += float64(zxcvbn_math.NChoseK(totalLenght, i)) + possibililities += float64(zxcvbnmath.NChoseK(totalLenght, i)) } if possibililities < 1 { @@ -82,6 +81,7 @@ func extraUpperCaseEntropy(match match.Match) float64 { return float64(math.Log2(possibililities)) } +// SpatialEntropy calculates the entropy for spatial matches func SpatialEntropy(match match.Match, turns int, shiftCount int) float64 { var s, d float64 if match.DictionaryName == "qwerty" || match.DictionaryName == "dvorak" { @@ -89,8 +89,8 @@ func SpatialEntropy(match match.Match, turns int, shiftCount int) float64 { s = float64(len(adjacency.BuildQwerty().Graph)) d = adjacency.BuildQwerty().CalculateAvgDegree() } else { - s = float64(KEYPAD_STARTING_POSITIONS) - d = KEYPAD_AVG_DEGREE + s = float64(keyPadStartingPositions) + d = keyPadAvgDegree } possibilities := float64(0) @@ -102,7 +102,7 @@ func SpatialEntropy(match match.Match, turns int, shiftCount int) float64 { for i := float64(2); i <= length+1; i++ { possibleTurns := math.Min(float64(turns), i-1) for j := float64(1); j <= possibleTurns+1; j++ { - x := zxcvbn_math.NChoseK(i-1, j-1) * s * math.Pow(d, j) + x := zxcvbnmath.NChoseK(i-1, j-1) * s * math.Pow(d, j) possibilities += x } } @@ -116,7 +116,7 @@ func SpatialEntropy(match match.Match, turns int, shiftCount int) float64 { U := length - S for i := float64(0); i < math.Min(S, U)+1; i++ { - possibilities += zxcvbn_math.NChoseK(S+U, i) + possibilities += zxcvbnmath.NChoseK(S+U, i) } entropy += math.Log2(possibilities) @@ -125,6 +125,7 @@ func SpatialEntropy(match match.Match, turns int, shiftCount int) float64 { return entropy } +// RepeatEntropy calculates the entropy for repeating entropy func RepeatEntropy(match match.Match) float64 { cardinality := CalcBruteForceCardinality(match.Token) entropy := math.Log2(cardinality * float64(len(match.Token))) @@ -132,6 +133,7 @@ func RepeatEntropy(match match.Match) float64 { return entropy } +// CalcBruteForceCardinality calculates the brute force cardinality //TODO: Validate against python func CalcBruteForceCardinality(password string) float64 { lower, upper, digits, symbols := float64(0), float64(0), float64(0), float64(0) @@ -152,6 +154,7 @@ func CalcBruteForceCardinality(password string) float64 { return cardinality } +// SequenceEntropy calculates the entropy for sequences such as 4567 or cdef func SequenceEntropy(match match.Match, dictionaryLength int, ascending bool) float64 { firstChar := match.Token[0] baseEntropy := float64(0) @@ -171,6 +174,7 @@ func SequenceEntropy(match match.Match, dictionaryLength int, ascending bool) fl return baseEntropy + math.Log2(float64(len(match.Token))) } +// ExtraLeetEntropy calulates the added entropy provied by l33t substitustions func ExtraLeetEntropy(match match.Match, password string) float64 { var subsitutions float64 var unsub float64 @@ -187,7 +191,7 @@ func ExtraLeetEntropy(match match.Match, password string) float64 { var possibilities float64 for i := float64(0); i <= math.Min(subsitutions, unsub)+1; i++ { - possibilities += zxcvbn_math.NChoseK(subsitutions+unsub, i) + possibilities += zxcvbnmath.NChoseK(subsitutions+unsub, i) } if possibilities <= 1 { @@ -196,16 +200,13 @@ func ExtraLeetEntropy(match match.Match, password string) float64 { return math.Log2(possibilities) } -func YearEntropy(dateMatch match.DateMatch) float64 { - return math.Log2(NUM_YEARS) -} - +// DateEntropy calculates the entropy provided by a date func DateEntropy(dateMatch match.DateMatch) float64 { var entropy float64 if dateMatch.Year < 100 { - entropy = math.Log2(NUM_DAYS * NUM_MONTHS * 100) + entropy = math.Log2(numDays * numMonths * 100) } else { - entropy = math.Log2(NUM_DAYS * NUM_MONTHS * NUM_YEARS) + entropy = math.Log2(numDays * numMonths * numYears) } if dateMatch.Separator != "" { diff --git a/frequency/frequency.go b/frequency/frequency.go index 5718830..5ef1979 100644 --- a/frequency/frequency.go +++ b/frequency/frequency.go @@ -2,16 +2,17 @@ package frequency import ( "encoding/json" - "github.com/nbutton23/zxcvbn-go/data" "log" -) -type FrequencyList struct { + "github.com/nbutton23/zxcvbn-go/data" +) +// List holds a frequency list +type List struct { Name string List []string } - -var FrequencyLists = make(map[string]FrequencyList) +// Lists holds all the frequency list in a map +var Lists = make(map[string]List) func init() { maleFilePath := getAsset("data/MaleNames.json") @@ -20,24 +21,24 @@ func init() { englishFilePath := getAsset("data/English.json") passwordsFilePath := getAsset("data/Passwords.json") - FrequencyLists["MaleNames"] = GetStringListFromAsset(maleFilePath, "MaleNames") - FrequencyLists["FemaleNames"] = GetStringListFromAsset(femaleFilePath, "FemaleNames") - FrequencyLists["Surname"] = GetStringListFromAsset(surnameFilePath, "Surname") - FrequencyLists["English"] = GetStringListFromAsset(englishFilePath, "English") - FrequencyLists["Passwords"] = GetStringListFromAsset(passwordsFilePath, "Passwords") + Lists["MaleNames"] = getStringListFromAsset(maleFilePath, "MaleNames") + Lists["FemaleNames"] = getStringListFromAsset(femaleFilePath, "FemaleNames") + Lists["Surname"] = getStringListFromAsset(surnameFilePath, "Surname") + Lists["English"] = getStringListFromAsset(englishFilePath, "English") + Lists["Passwords"] = getStringListFromAsset(passwordsFilePath, "Passwords") } func getAsset(name string) []byte { - data, err := zxcvbn_data.Asset(name) + data, err := data.Asset(name) if err != nil { panic("Error getting asset " + name) } return data } -func GetStringListFromAsset(data []byte, name string) FrequencyList { +func getStringListFromAsset(data []byte, name string) List { - var tempList FrequencyList + var tempList List err := json.Unmarshal(data, &tempList) if err != nil { log.Fatal(err) diff --git a/match/match.go b/match/match.go index 0523943..5d3ef30 100644 --- a/match/match.go +++ b/match/match.go @@ -1,5 +1,6 @@ package match +//Matches is an alies for []Match used for sorting type Matches []Match func (s Matches) Len() int { @@ -18,6 +19,7 @@ func (s Matches) Less(i, j int) bool { } } +// Match represents different matches type Match struct { Pattern string I, J int @@ -26,6 +28,7 @@ type Match struct { Entropy float64 } +//DateMatch is specifilly a match for type date type DateMatch struct { Pattern string I, J int @@ -34,6 +37,7 @@ type DateMatch struct { Day, Month, Year int64 } +//Matcher are a func and ID that can be used to match different passwords type Matcher struct { MatchingFunc func(password string) []Match ID string diff --git a/matching/dateMatchers.go b/matching/dateMatchers.go index e55b3da..012bc06 100644 --- a/matching/dateMatchers.go +++ b/matching/dateMatchers.go @@ -1,25 +1,33 @@ package matching import ( - "regexp" "strconv" "strings" + "regexp" "github.com/nbutton23/zxcvbn-go/entropy" "github.com/nbutton23/zxcvbn-go/match" ) const ( - DATESEP_MATCHER_NAME = "DATESEP" - DATEWITHOUTSEP_MATCHER_NAME = "DATEWITHOUT" + dateSepMatcherName = "DATESEP" + dateWithOutSepMatcherName = "DATEWITHOUT" ) +var ( + dateRxYearSuffix = regexp.MustCompile( `((\d{1,2})(\s|-|\/|\\|_|\.)(\d{1,2})(\s|-|\/|\\|_|\.)(19\d{2}|200\d|201\d|\d{2}))`) + dateRxYearPrefix = regexp.MustCompile(`((19\d{2}|200\d|201\d|\d{2})(\s|-|/|\\|_|\.)(\d{1,2})(\s|-|/|\\|_|\.)(\d{1,2}))`) + dateWithOutSepMatch = regexp.MustCompile( `\d{4,8}`) +) + +//FilterDateSepMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher func FilterDateSepMatcher(m match.Matcher) bool { - return m.ID == DATESEP_MATCHER_NAME + return m.ID == dateSepMatcherName } +//FilterDateWithoutSepMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher func FilterDateWithoutSepMatcher(m match.Matcher) bool { - return m.ID == DATEWITHOUTSEP_MATCHER_NAME + return m.ID == dateWithOutSepMatcherName } func checkDate(day, month, year int64) (bool, int64, int64, int64) { @@ -60,9 +68,8 @@ func dateSepMatchHelper(password string) []match.DateMatch { var matches []match.DateMatch - matcher := regexp.MustCompile(DATE_RX_YEAR_SUFFIX) - for _, v := range matcher.FindAllString(password, len(password)) { - splitV := matcher.FindAllStringSubmatch(v, len(v)) + for _, v := range dateRxYearSuffix.FindAllString(password, len(password)) { + splitV := dateRxYearSuffix.FindAllStringSubmatch(v, len(v)) i := strings.Index(password, v) j := i + len(v) day, _ := strconv.ParseInt(splitV[0][4], 10, 16) @@ -72,9 +79,8 @@ func dateSepMatchHelper(password string) []match.DateMatch { matches = append(matches, match) } - matcher = regexp.MustCompile(DATE_RX_YEAR_PREFIX) - for _, v := range matcher.FindAllString(password, len(password)) { - splitV := matcher.FindAllStringSubmatch(v, len(v)) + for _, v := range dateRxYearPrefix.FindAllString(password, len(password)) { + splitV := dateRxYearPrefix.FindAllStringSubmatch(v, len(v)) i := strings.Index(password, v) j := i + len(v) day, _ := strconv.ParseInt(splitV[0][4], 10, 16) @@ -98,13 +104,13 @@ func dateSepMatchHelper(password string) []match.DateMatch { } -type DateMatchCandidate struct { +type dateMatchCandidate struct { DayMonth string Year string I, J int } -type DateMatchCandidateTwo struct { +type dateMatchCandidateTwo struct { Day string Month string Year string @@ -132,13 +138,12 @@ func dateWithoutSepMatch(password string) []match.Match { //TODO Has issues with 6 digit dates func dateWithoutSepMatchHelper(password string) (matches []match.DateMatch) { - matcher := regexp.MustCompile(DATE_WITHOUT_SEP_MATCH) - for _, v := range matcher.FindAllString(password, len(password)) { + for _, v := range dateWithOutSepMatch.FindAllString(password, len(password)) { i := strings.Index(password, v) j := i + len(v) length := len(v) lastIndex := length - 1 - var candidatesRoundOne []DateMatchCandidate + var candidatesRoundOne []dateMatchCandidate if length <= 6 { //2-digit year prefix @@ -155,7 +160,7 @@ func dateWithoutSepMatchHelper(password string) (matches []match.DateMatch) { candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[0:lastIndex-3], v[lastIndex-3:], i, j)) } - var candidatesRoundTwo []DateMatchCandidateTwo + var candidatesRoundTwo []dateMatchCandidateTwo for _, c := range candidatesRoundOne { if len(c.DayMonth) == 2 { candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0:0], c.DayMonth[1:1], c.Year, c.I, c.J)) @@ -194,11 +199,11 @@ func dateWithoutSepMatchHelper(password string) (matches []match.DateMatch) { return matches } -func buildDateMatchCandidate(dayMonth, year string, i, j int) DateMatchCandidate { - return DateMatchCandidate{DayMonth: dayMonth, Year: year, I: i, J: j} +func buildDateMatchCandidate(dayMonth, year string, i, j int) dateMatchCandidate { + return dateMatchCandidate{DayMonth: dayMonth, Year: year, I: i, J: j} } -func buildDateMatchCandidateTwo(day, month string, year string, i, j int) DateMatchCandidateTwo { +func buildDateMatchCandidateTwo(day, month string, year string, i, j int) dateMatchCandidateTwo { - return DateMatchCandidateTwo{Day: day, Month: month, Year: year, I: i, J: j} + return dateMatchCandidateTwo{Day: day, Month: month, Year: year, I: i, J: j} } diff --git a/matching/dictionaryMatch.go b/matching/dictionaryMatch.go index b76921f..57afeda 100644 --- a/matching/dictionaryMatch.go +++ b/matching/dictionaryMatch.go @@ -18,19 +18,21 @@ func buildDictMatcher(dictName string, rankedDict map[string]int) func(password } func dictionaryMatch(password string, dictionaryName string, rankedDict map[string]int) []match.Match { - length := len(password) var results []match.Match pwLower := strings.ToLower(password) + pwLowerRunes := []rune(pwLower) + length := len(pwLowerRunes) + for i := 0; i < length; i++ { for j := i; j < length; j++ { - word := pwLower[i : j+1] - if val, ok := rankedDict[word]; ok { + word := pwLowerRunes[i : j+1] + if val, ok := rankedDict[string(word)]; ok { matchDic := match.Match{Pattern: "dictionary", DictionaryName: dictionaryName, I: i, J: j, - Token: password[i : j+1], + Token: string([]rune(password)[i : j+1]), } matchDic.Entropy = entropy.DictionaryEntropy(matchDic, float64(val)) diff --git a/matching/leet.go b/matching/leet.go index 917ba8a..610f197 100644 --- a/matching/leet.go +++ b/matching/leet.go @@ -7,10 +7,12 @@ import ( "github.com/nbutton23/zxcvbn-go/match" ) -const L33T_MATCHER_NAME = "l33t" +// L33TMatcherName id +const L33TMatcherName = "l33t" +//FilterL33tMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher func FilterL33tMatcher(m match.Matcher) bool { - return m.ID == L33T_MATCHER_NAME + return m.ID == L33TMatcherName } func l33tMatch(password string) []match.Match { @@ -19,7 +21,7 @@ func l33tMatch(password string) []match.Match { var matches []match.Match for _, permutation := range permutations { - for _, mather := range DICTIONARY_MATCHERS { + for _, mather := range dictionaryMatchers { matches = append(matches, mather.MatchingFunc(permutation)...) } } @@ -45,7 +47,7 @@ func getPermutations(password string) []string { // inside the provided password. func relevantL33tSubtable(password string) map[string][]string { relevantSubs := make(map[string][]string) - for key, values := range L33T_TABLE.Graph { + for key, values := range l33tTable.Graph { for _, value := range values { if strings.Contains(password, value) { relevantSubs[key] = append(relevantSubs[key], value) diff --git a/matching/matching.go b/matching/matching.go index 70f1631..4577db8 100644 --- a/matching/matching.go +++ b/matching/matching.go @@ -9,28 +9,23 @@ import ( ) var ( - DICTIONARY_MATCHERS []match.Matcher - MATCHERS []match.Matcher - ADJACENCY_GRAPHS []adjacency.AdjacencyGraph - L33T_TABLE adjacency.AdjacencyGraph + dictionaryMatchers []match.Matcher + matchers []match.Matcher + adjacencyGraphs []adjacency.Graph + l33tTable adjacency.Graph - SEQUENCES map[string]string -) - -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}` + sequences map[string]string ) func init() { loadFrequencyList() } +// Omnimatch runs all matchers against the password func Omnimatch(password string, userInputs []string, filters ...func(match.Matcher) bool) (matches []match.Match) { //Can I run into the issue where nil is not equal to nil? - if DICTIONARY_MATCHERS == nil || ADJACENCY_GRAPHS == nil { + if dictionaryMatchers == nil || adjacencyGraphs == nil { loadFrequencyList() } @@ -39,7 +34,7 @@ func Omnimatch(password string, userInputs []string, filters ...func(match.Match matches = userInputMatcher(password) } - for _, matcher := range MATCHERS { + for _, matcher := range matchers { shouldBeFiltered := false for i := range filters { if filters[i](matcher) { @@ -57,31 +52,31 @@ func Omnimatch(password string, userInputs []string, filters ...func(match.Match func loadFrequencyList() { - for n, list := range frequency.FrequencyLists { - DICTIONARY_MATCHERS = append(DICTIONARY_MATCHERS, match.Matcher{MatchingFunc: buildDictMatcher(n, buildRankedDict(list.List)), ID: n}) + for n, list := range frequency.Lists { + dictionaryMatchers = append(dictionaryMatchers, match.Matcher{MatchingFunc: buildDictMatcher(n, buildRankedDict(list.List)), ID: n}) } - L33T_TABLE = adjacency.AdjacencyGph["l33t"] + l33tTable = adjacency.GraphMap["l33t"] - ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["qwerty"]) - ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["dvorak"]) - ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["keypad"]) - ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["macKeypad"]) + adjacencyGraphs = append(adjacencyGraphs, adjacency.GraphMap["qwerty"]) + adjacencyGraphs = append(adjacencyGraphs, adjacency.GraphMap["dvorak"]) + adjacencyGraphs = append(adjacencyGraphs, adjacency.GraphMap["keypad"]) + adjacencyGraphs = append(adjacencyGraphs, adjacency.GraphMap["macKeypad"]) //l33tFilePath, _ := filepath.Abs("adjacency/L33t.json") //L33T_TABLE = adjacency.GetAdjancencyGraphFromFile(l33tFilePath, "l33t") - SEQUENCES = make(map[string]string) - SEQUENCES["lower"] = "abcdefghijklmnopqrstuvwxyz" - SEQUENCES["upper"] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - SEQUENCES["digits"] = "0123456789" + sequences = make(map[string]string) + sequences["lower"] = "abcdefghijklmnopqrstuvwxyz" + sequences["upper"] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + sequences["digits"] = "0123456789" - MATCHERS = append(MATCHERS, DICTIONARY_MATCHERS...) - MATCHERS = append(MATCHERS, match.Matcher{MatchingFunc: spatialMatch, ID: SPATIAL_MATCHER_NAME}) - MATCHERS = append(MATCHERS, match.Matcher{MatchingFunc: repeatMatch, ID: REPEAT_MATCHER_NAME}) - MATCHERS = append(MATCHERS, match.Matcher{MatchingFunc: sequenceMatch, ID: SEQUENCE_MATCHER_NAME}) - MATCHERS = append(MATCHERS, match.Matcher{MatchingFunc: l33tMatch, ID: L33T_MATCHER_NAME}) - MATCHERS = append(MATCHERS, match.Matcher{MatchingFunc: dateSepMatcher, ID: DATESEP_MATCHER_NAME}) - MATCHERS = append(MATCHERS, match.Matcher{MatchingFunc: dateWithoutSepMatch, ID: DATEWITHOUTSEP_MATCHER_NAME}) + matchers = append(matchers, dictionaryMatchers...) + matchers = append(matchers, match.Matcher{MatchingFunc: spatialMatch, ID: spatialMatcherName}) + matchers = append(matchers, match.Matcher{MatchingFunc: repeatMatch, ID: repeatMatcherName}) + matchers = append(matchers, match.Matcher{MatchingFunc: sequenceMatch, ID: sequenceMatcherName}) + matchers = append(matchers, match.Matcher{MatchingFunc: l33tMatch, ID: L33TMatcherName}) + matchers = append(matchers, match.Matcher{MatchingFunc: dateSepMatcher, ID: dateSepMatcherName}) + matchers = append(matchers, match.Matcher{MatchingFunc: dateWithoutSepMatch, ID: dateWithOutSepMatcherName}) } diff --git a/matching/matching_test.go b/matching/matching_test.go index 4bd4bc6..9eb07a6 100644 --- a/matching/matching_test.go +++ b/matching/matching_test.go @@ -106,7 +106,7 @@ func TestSpatialMatchDvorak(t *testing.T) { func TestDictionaryMatch(t *testing.T) { var matches []match.Match - for _, dicMatcher := range DICTIONARY_MATCHERS { + for _, dicMatcher := range dictionaryMatchers { matchesTemp := dicMatcher.MatchingFunc("first") matches = append(matches, matchesTemp...) } diff --git a/matching/repeatMatch.go b/matching/repeatMatch.go index 97bd33b..a93e459 100644 --- a/matching/repeatMatch.go +++ b/matching/repeatMatch.go @@ -7,10 +7,11 @@ import ( "github.com/nbutton23/zxcvbn-go/match" ) -const REPEAT_MATCHER_NAME = "REPEAT" +const repeatMatcherName = "REPEAT" +//FilterRepeatMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher func FilterRepeatMatcher(m match.Matcher) bool { - return m.ID == REPEAT_MATCHER_NAME + return m.ID == repeatMatcherName } func repeatMatch(password string) []match.Match { diff --git a/matching/sequenceMatch.go b/matching/sequenceMatch.go index 89f1526..e0ed052 100644 --- a/matching/sequenceMatch.go +++ b/matching/sequenceMatch.go @@ -7,10 +7,11 @@ import ( "github.com/nbutton23/zxcvbn-go/match" ) -const SEQUENCE_MATCHER_NAME = "SEQ" +const sequenceMatcherName = "SEQ" +//FilterSequenceMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher func FilterSequenceMatcher(m match.Matcher) bool { - return m.ID == SEQUENCE_MATCHER_NAME + return m.ID == sequenceMatcherName } func sequenceMatch(password string) []match.Match { @@ -20,7 +21,7 @@ func sequenceMatch(password string) []match.Match { var seq string var seqName string seqDirection := 0 - for seqCandidateName, seqCandidate := range SEQUENCES { + for seqCandidateName, seqCandidate := range sequences { iN := strings.Index(seqCandidate, string(password[i])) var jN int if j < len(password) { @@ -64,7 +65,7 @@ func sequenceMatch(password string) []match.Match { } break } else { - j += 1 + j++ } } diff --git a/matching/spatialMatch.go b/matching/spatialMatch.go index 145cfb8..961d9b1 100644 --- a/matching/spatialMatch.go +++ b/matching/spatialMatch.go @@ -8,14 +8,15 @@ import ( "github.com/nbutton23/zxcvbn-go/match" ) -const SPATIAL_MATCHER_NAME = "SPATIAL" +const spatialMatcherName = "SPATIAL" +//FilterSpatialMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher func FilterSpatialMatcher(m match.Matcher) bool { - return m.ID == SPATIAL_MATCHER_NAME + return m.ID == spatialMatcherName } func spatialMatch(password string) (matches []match.Match) { - for _, graph := range ADJACENCY_GRAPHS { + for _, graph := range adjacencyGraphs { if graph.Graph != nil { matches = append(matches, spatialMatchHelper(password, graph)...) } @@ -23,7 +24,7 @@ func spatialMatch(password string) (matches []match.Match) { return matches } -func spatialMatchHelper(password string, graph adjacency.AdjacencyGraph) (matches []match.Match) { +func spatialMatchHelper(password string, graph adjacency.Graph) (matches []match.Match) { for i := 0; i < len(password)-1; { j := i + 1 @@ -42,7 +43,7 @@ func spatialMatchHelper(password string, graph adjacency.AdjacencyGraph) (matche if j < len(password) { curChar := password[j] for _, adj := range adjacents { - curDirection += 1 + curDirection++ if strings.Index(adj, string(curChar)) != -1 { found = true @@ -51,13 +52,13 @@ func spatialMatchHelper(password string, graph adjacency.AdjacencyGraph) (matche 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 + 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 + turns++ lastDirection = foundDirection } break @@ -67,7 +68,7 @@ func spatialMatchHelper(password string, graph adjacency.AdjacencyGraph) (matche //if the current pattern continued, extend j and try to grow again if found { - j += 1 + j++ } else { //otherwise push the pattern discovered so far, if any... //don't consider length 1 or 2 chains. diff --git a/scoring/scoring.go b/scoring/scoring.go index 0456fd7..4f68a6d 100644 --- a/scoring/scoring.go +++ b/scoring/scoring.go @@ -10,19 +10,16 @@ import ( ) const ( - START_UPPER string = `^[A-Z][^A-Z]+$` - END_UPPER string = `^[^A-Z]+[A-Z]$'` - ALL_UPPER string = `^[A-Z]+$` - //for a hash function like bcrypt/scrypt/PBKDF2, 10ms per guess is a safe lower bound. //(usually a guess would take longer -- this assumes fast hardware and a small work factor.) //adjust for your site accordingly if you use another hash function, possibly by //several orders of magnitude! - SINGLE_GUESS float64 = 0.010 - NUM_ATTACKERS float64 = 100 //Cores used to make guesses - SECONDS_PER_GUESS float64 = SINGLE_GUESS / NUM_ATTACKERS + singleGuess float64 = 0.010 + numAttackers float64 = 100 //Cores used to make guesses + secondsPerGuess float64 = singleGuess / numAttackers ) +// MinEntropyMatch is the lowest entropy match found type MinEntropyMatch struct { Password string Entropy float64 @@ -34,7 +31,7 @@ type MinEntropyMatch struct { } /* -Returns minimum entropy +MinimumEntropyMatchSequence returns the minimum entropy 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. @@ -130,13 +127,13 @@ func get(a []float64, i int) float64 { } func entropyToCrackTime(entropy float64) float64 { - crackTime := (0.5 * math.Pow(float64(2), entropy)) * SECONDS_PER_GUESS + crackTime := (0.5 * math.Pow(float64(2), entropy)) * secondsPerGuess return crackTime } func roundToXDigits(number float64, digits int) float64 { - return zxcvbn_math.Round(number, .5, digits) + return zxcvbnmath.Round(number, .5, digits) } func displayTime(seconds float64) string { diff --git a/utils/math/mathutils.go b/utils/math/mathutils.go index d885479..1b989d1 100644 --- a/utils/math/mathutils.go +++ b/utils/math/mathutils.go @@ -1,12 +1,11 @@ -package zxcvbn_math +package zxcvbnmath import "math" -/** +/* +NChoseK http://blog.plover.com/math/choose.html I am surprised that I have to define these. . . Maybe i just didn't look hard enough for a lib. */ - -//http://blog.plover.com/math/choose.html func NChoseK(n, k float64) float64 { if k > n { return 0 @@ -25,6 +24,7 @@ func NChoseK(n, k float64) float64 { return r } +// Round a number func Round(val float64, roundOn float64, places int) (newVal float64) { var round float64 pow := math.Pow(10, float64(places)) diff --git a/utils/math/mathutils_test.go b/utils/math/mathutils_test.go index a085bd4..16fc113 100644 --- a/utils/math/mathutils_test.go +++ b/utils/math/mathutils_test.go @@ -1,4 +1,4 @@ -package zxcvbn_math +package zxcvbnmath import ( "github.com/stretchr/testify/assert" diff --git a/zxcvbn.go b/zxcvbn.go index 086270c..9c34b1c 100644 --- a/zxcvbn.go +++ b/zxcvbn.go @@ -9,6 +9,7 @@ import ( "github.com/nbutton23/zxcvbn-go/utils/math" ) +// PasswordStrength takes a password, userInputs and optional filters and returns a MinEntropyMatch func PasswordStrength(password string, userInputs []string, filters ...func(match.Matcher) bool) scoring.MinEntropyMatch { start := time.Now() matches := matching.Omnimatch(password, userInputs, filters...) @@ -16,6 +17,6 @@ func PasswordStrength(password string, userInputs []string, filters ...func(matc end := time.Now() calcTime := end.Nanosecond() - start.Nanosecond() - result.CalcTime = zxcvbn_math.Round(float64(calcTime)*time.Nanosecond.Seconds(), .5, 3) + result.CalcTime = zxcvbnmath.Round(float64(calcTime)*time.Nanosecond.Seconds(), .5, 3) return result } diff --git a/zxcvbn_test.go b/zxcvbn_test.go index a25bd92..7a3f616 100644 --- a/zxcvbn_test.go +++ b/zxcvbn_test.go @@ -2,10 +2,7 @@ package zxcvbn import ( "testing" - - "fmt" "math" - "strconv" ) /** @@ -16,16 +13,6 @@ const ( allowableError = float64(0.05) ) -type failedTest struct { - Password string - Expect float64 - Actual float64 - PError float64 -} - -var failedTests []failedTest -var numTestRan int - func TestPasswordStrength(t *testing.T) { // Expected calculated by running zxcvbn-python @@ -65,27 +52,21 @@ func TestPasswordStrength(t *testing.T) { runTest(t, "eheuczkqyq", float64(42.813)) runTest(t, "rWibMFACxAUGZmxhVncy", float64(104.551)) runTest(t, "Ba9ZyWABu99[BK#6MBgbH88Tofv)vs$", float64(161.278)) - - formatString := "%s : error should be less than %.2f \t Acctual error was: %.4f \t Expected entropy %.4f \t Actual entropy %.4f \n" - for _, test := range failedTests { - fmt.Printf(formatString, test.Password, allowableError, test.PError, test.Expect, test.Actual) - } - - pTestPassed := (float64(numTestRan-len(failedTests)) / float64(numTestRan)) * float64(100) - - fmt.Println("\n % of the test passed " + strconv.FormatFloat(pTestPassed, 'f', -1, 64)) - } + +var formatString = "%s : error should be less than %.2f Acctual error: %.4f Expected entropy %.4f Actual entropy %.4f \n" + func runTest(t *testing.T, password string, pythonEntropy float64) { - //Calculated by running it through python-zxcvbn + goEntropy := GoPasswordStrength(password, nil) perror := math.Abs(goEntropy-pythonEntropy) / pythonEntropy - numTestRan++ - if perror > allowableError { - failedTests = append(failedTests, failedTest{Password: password, Expect: pythonEntropy, Actual: goEntropy, PError: perror}) - } + if perror > allowableError { + t.Logf(formatString, password, allowableError, perror, pythonEntropy, goEntropy ) + + t.Fail() + } } func GoPasswordStrength(password string, userInputs []string) float64 {