commit
7d8c28acb6
|
@ -56,7 +56,7 @@ Bug reports and pull requests welcome!
|
|||
Project Status
|
||||
------------------------------------------------------------------------
|
||||
|
||||
This is a work in progress. I would welcome some review of the code. I am new to goLang and expect that I have made some mistakes; if not in logic in performance.
|
||||
Use zxcvbn_test.go to check how close to feature parity the project is.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
Acknowledgment
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
package entropy
|
||||
|
||||
import (
|
||||
"github.com/nbutton23/zxcvbn-go/adjacency"
|
||||
"github.com/nbutton23/zxcvbn-go/match"
|
||||
"github.com/nbutton23/zxcvbn-go/utils/math"
|
||||
"math"
|
||||
"regexp"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
START_UPPER string = `^[A-Z][^A-Z]+$`
|
||||
END_UPPER string = `^[^A-Z]+[A-Z]$'`
|
||||
ALL_UPPER string = `^[A-Z]+$`
|
||||
)
|
||||
|
||||
var (
|
||||
KEYPAD_STARTING_POSITIONS = len(adjacency.AdjacencyGph["keypad"].Graph)
|
||||
KEYPAD_AVG_DEGREE = adjacency.AdjacencyGph["keypad"].CalculateAvgDegree()
|
||||
)
|
||||
|
||||
func DictionaryEntropy(match match.Match, rank float64) float64 {
|
||||
baseEntropy := math.Log2(rank)
|
||||
upperCaseEntropy := extraUpperCaseEntropy(match)
|
||||
//TODO: L33t
|
||||
return baseEntropy + upperCaseEntropy
|
||||
}
|
||||
|
||||
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 SpatialEntropy(match match.Match, turns int, shiftCount int) float64 {
|
||||
var s, d float64
|
||||
if match.DictionaryName == "qwerty" || match.DictionaryName == "dvorak" {
|
||||
//todo: verify qwerty and dvorak have the same length and degree
|
||||
s = float64(len(adjacency.BuildQwerty().Graph))
|
||||
d = adjacency.BuildQwerty().CalculateAvgDegree()
|
||||
} else {
|
||||
s = float64(KEYPAD_STARTING_POSITIONS)
|
||||
d = KEYPAD_AVG_DEGREE
|
||||
}
|
||||
|
||||
possibilities := float64(0)
|
||||
|
||||
length := float64(len(match.Token))
|
||||
|
||||
//TODO: Should this be <= or just < ?
|
||||
//Estimate the number of possible patterns w/ length L or less with t turns or less
|
||||
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)
|
||||
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(shiftCount); S > float64(0) {
|
||||
possibilities = float64(0)
|
||||
U := length - 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 RepeatEntropy(match match.Match) float64 {
|
||||
cardinality := CalcBruteForceCardinality(match.Token)
|
||||
entropy := math.Log2(cardinality * float64(len(match.Token)))
|
||||
|
||||
return entropy
|
||||
}
|
||||
|
||||
//TODO: Validate against python
|
||||
func CalcBruteForceCardinality(password string) float64 {
|
||||
lower, upper, digits, symbols := float64(0), float64(0), float64(0), float64(0)
|
||||
|
||||
for _, char := range password {
|
||||
if unicode.IsLower(char) {
|
||||
lower = float64(26)
|
||||
} else if unicode.IsDigit(char) {
|
||||
digits = float64(10)
|
||||
} else if unicode.IsUpper(char) {
|
||||
upper = float64(26)
|
||||
} else {
|
||||
symbols = float64(33)
|
||||
}
|
||||
}
|
||||
|
||||
cardinality := lower + upper + digits + symbols
|
||||
return cardinality
|
||||
}
|
||||
|
||||
func SequenceEntropy(match match.Match, dictionaryLength int, ascending bool) float64 {
|
||||
firstChar := match.Token[0]
|
||||
baseEntropy := float64(0)
|
||||
if string(firstChar) == "a" || string(firstChar) == "1" {
|
||||
baseEntropy = float64(0)
|
||||
} else {
|
||||
baseEntropy = math.Log2(float64(dictionaryLength))
|
||||
//TODO: should this be just the first or any char
|
||||
if unicode.IsUpper(rune(firstChar)) {
|
||||
baseEntropy++
|
||||
}
|
||||
}
|
||||
|
||||
if !ascending {
|
||||
baseEntropy++
|
||||
}
|
||||
return baseEntropy + math.Log2(float64(len(match.Token)))
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package entropy
|
||||
|
||||
import (
|
||||
"github.com/nbutton23/zxcvbn-go/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"github.com/nbutton23/zxcvbn-go/match"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
func TestDictionaryEntropyCalculation(t *testing.T) {
|
||||
match := match.Match{
|
||||
Pattern: "dictionary",
|
||||
I: 0,
|
||||
J: 4,
|
||||
Token: "first",
|
||||
}
|
||||
|
||||
entropy := DictionaryEntropy(match, float64(20))
|
||||
|
||||
assert.Equal(t, 4.321928094887363, entropy)
|
||||
}
|
||||
|
||||
func TestSpatialEntropyCalculation(t *testing.T) {
|
||||
matchPlain := match.Match{
|
||||
Pattern: "spatial",
|
||||
I: 0,
|
||||
J: 5,
|
||||
Token: "asdfgh",
|
||||
DictionaryName:"qwerty",
|
||||
}
|
||||
entropy := SpatialEntropy(matchPlain, 0, 0)
|
||||
assert.Equal(t, 9.754887502163468, entropy)
|
||||
|
||||
matchShift := match.Match{
|
||||
Pattern: "spatial",
|
||||
I: 0,
|
||||
J: 5,
|
||||
Token: "asdFgh",
|
||||
DictionaryName:"qwerty",
|
||||
}
|
||||
entropyShift := SpatialEntropy(matchShift, 0, 1)
|
||||
assert.Equal(t, 12.562242424221072, entropyShift)
|
||||
|
||||
matchTurn := match.Match{
|
||||
Pattern: "spatial",
|
||||
I: 0,
|
||||
J: 5,
|
||||
Token: "asdcxz",
|
||||
DictionaryName:"qwerty",
|
||||
}
|
||||
entropyTurn := SpatialEntropy(matchTurn, 2, 0)
|
||||
assert.Equal(t, 14.080500893768884, entropyTurn)
|
||||
}
|
||||
|
||||
func TestRepeatMatchEntropyCalculation(t *testing.T) {
|
||||
matchRepeat := match.Match{
|
||||
Pattern: "repeat",
|
||||
I: 0,
|
||||
J: 4,
|
||||
Token: "aaaaa",
|
||||
}
|
||||
entropy := RepeatEntropy(matchRepeat)
|
||||
assert.Equal(t, 7.022367813028454, entropy)
|
||||
}
|
||||
|
||||
func TestSequenceCalculation(t *testing.T) {
|
||||
matchLower := match.Match{
|
||||
Pattern: "sequence",
|
||||
I: 0,
|
||||
J: 4,
|
||||
Token: "jklmn",
|
||||
}
|
||||
entropy := SequenceEntropy(matchLower, len("abcdefghijklmnopqrstuvwxyz"), true)
|
||||
assert.Equal(t, 7.022367813028454, entropy)
|
||||
|
||||
matchUpper := match.Match{
|
||||
Pattern: "sequence",
|
||||
I: 0,
|
||||
J: 4,
|
||||
Token: "JKLMN",
|
||||
}
|
||||
entropy = SequenceEntropy(matchUpper, len("abcdefghijklmnopqrstuvwxyz"), true)
|
||||
assert.Equal(t, 8.022367813028454, entropy)
|
||||
|
||||
matchUpperDec := match.Match{
|
||||
Pattern: "sequence",
|
||||
I: 0,
|
||||
J: 4,
|
||||
Token: "JKLMN",
|
||||
}
|
||||
entropy = SequenceEntropy(matchUpperDec, len("abcdefghijklmnopqrstuvwxyz"), false)
|
||||
assert.Equal(t, 9.022367813028454, entropy)
|
||||
|
||||
matchDigit := match.Match{
|
||||
Pattern: "sequence",
|
||||
I: 0,
|
||||
J: 4,
|
||||
Token: "34567",
|
||||
}
|
||||
entropy = SequenceEntropy(matchDigit, 10, true)
|
||||
assert.Equal(t, 5.643856189774724, entropy)
|
||||
}
|
|
@ -19,18 +19,11 @@ func (s Matches) Less(i, j int) bool {
|
|||
}
|
||||
|
||||
type Match struct {
|
||||
Pattern string
|
||||
I, J int
|
||||
Token string
|
||||
MatchedWord string
|
||||
Rank float64
|
||||
DictionaryName string
|
||||
DictionaryLength int
|
||||
Ascending bool
|
||||
Turns int
|
||||
ShiftedCount int
|
||||
Entropy float64
|
||||
RepeatedChar string
|
||||
Pattern string
|
||||
I, J int
|
||||
Token string
|
||||
DictionaryName string
|
||||
Entropy float64
|
||||
}
|
||||
|
||||
type DateMatch struct {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
// "github.com/deckarep/golang-set"
|
||||
"github.com/nbutton23/zxcvbn-go/entropy"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -105,13 +106,15 @@ func dictionaryMatch(password string, dictionaryName string, rankedDict map[stri
|
|||
for j := i; j < length; j++ {
|
||||
word := pwLower[i : j+1]
|
||||
if val, ok := rankedDict[word]; ok {
|
||||
results = append(results, match.Match{Pattern: "dictionary",
|
||||
matchDic := match.Match{Pattern: "dictionary",
|
||||
DictionaryName: dictionaryName,
|
||||
I: i,
|
||||
J: j,
|
||||
Token: password[i : j+1],
|
||||
MatchedWord: word,
|
||||
Rank: float64(val)})
|
||||
}
|
||||
matchDic.Entropy = entropy.DictionaryEntropy(matchDic, float64(val))
|
||||
|
||||
results = append(results, matchDic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -244,8 +247,6 @@ func buildDateMatchCandidateTwo(day, month byte, year string, i, j int) match.Da
|
|||
return match.DateMatch{Day: intDay, Month: intMonth, Year: intYear, I: i, J: j}
|
||||
}
|
||||
|
||||
//TODO: This is not working.
|
||||
//It appears that the Adjacency graph data is incorrect. Need to get a new copy from python-zxcvbn.
|
||||
func SpatialMatch(password string) (matches []match.Match) {
|
||||
for _, graph := range ADJACENCY_GRAPHS {
|
||||
if graph.Graph != nil {
|
||||
|
@ -304,7 +305,9 @@ func spatialMatchHelper(password string, graph adjacency.AdjacencyGraph) (matche
|
|||
//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})
|
||||
matchSpc := match.Match{Pattern: "spatial", I: i, J: j - 1, Token: password[i:j], DictionaryName: graph.Name}
|
||||
matchSpc.Entropy = entropy.SpatialEntropy(matchSpc, turns, shiftedCount)
|
||||
matches = append(matches, matchSpc)
|
||||
}
|
||||
//. . . and then start a new search from the rest of the password
|
||||
i = j
|
||||
|
@ -366,12 +369,14 @@ func RepeatMatch(password string) []match.Match {
|
|||
} else if currentStreak > 2 {
|
||||
iPos := i - currentStreak
|
||||
jPos := i - 1
|
||||
matches = append(matches, match.Match{
|
||||
Pattern: "repeat",
|
||||
I: iPos,
|
||||
J: jPos,
|
||||
Token: password[iPos : jPos+1],
|
||||
RepeatedChar: prev})
|
||||
matchRepeat := match.Match{
|
||||
Pattern: "repeat",
|
||||
I: iPos,
|
||||
J: jPos,
|
||||
Token: password[iPos : jPos+1],
|
||||
DictionaryName: prev}
|
||||
matchRepeat.Entropy = entropy.RepeatEntropy(matchRepeat)
|
||||
matches = append(matches, matchRepeat)
|
||||
currentStreak = 1
|
||||
} else {
|
||||
currentStreak = 1
|
||||
|
@ -383,12 +388,14 @@ func RepeatMatch(password string) []match.Match {
|
|||
if currentStreak > 2 {
|
||||
iPos := i - currentStreak + 1
|
||||
jPos := i
|
||||
matches = append(matches, match.Match{
|
||||
Pattern: "repeat",
|
||||
I: iPos,
|
||||
J: jPos,
|
||||
Token: password[iPos : jPos+1],
|
||||
RepeatedChar: prev})
|
||||
matchRepeat := match.Match{
|
||||
Pattern: "repeat",
|
||||
I: iPos,
|
||||
J: jPos,
|
||||
Token: password[iPos : jPos+1],
|
||||
DictionaryName: prev}
|
||||
matchRepeat.Entropy = entropy.RepeatEntropy(matchRepeat)
|
||||
matches = append(matches, matchRepeat)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
@ -431,13 +438,16 @@ func SequenceMatch(password string) []match.Match {
|
|||
|
||||
if j == len(password) || curN-prevN != seqDirection {
|
||||
if j-i > 2 {
|
||||
matches = append(matches, match.Match{Pattern: "sequence",
|
||||
I: i,
|
||||
J: j - 1,
|
||||
Token: password[i:j],
|
||||
DictionaryName: seqName,
|
||||
DictionaryLength: len(seq),
|
||||
Ascending: (seqDirection == 1)})
|
||||
matchSequence := match.Match{
|
||||
Pattern: "sequence",
|
||||
I: i,
|
||||
J: j - 1,
|
||||
Token: password[i:j],
|
||||
DictionaryName: seqName,
|
||||
}
|
||||
|
||||
matchSequence.Entropy = entropy.SequenceEntropy(matchSequence, len(seq), (seqDirection == 1))
|
||||
matches = append(matches, matchSequence)
|
||||
}
|
||||
break
|
||||
} else {
|
||||
|
|
|
@ -40,14 +40,17 @@ func TestRepeatMatch(t *testing.T) {
|
|||
assert.Len(t, matches, 2, "Lenght should be 2")
|
||||
|
||||
for _, match := range matches {
|
||||
if strings.ToLower(match.RepeatedChar) == "b" {
|
||||
if strings.ToLower(match.DictionaryName) == "b" {
|
||||
assert.Equal(t, 3, match.I)
|
||||
assert.Equal(t, 6, match.J)
|
||||
assert.Equal(t, "bBbB", match.Token)
|
||||
assert.NotZero(t, match.Entropy, "Entropy should be set")
|
||||
} else {
|
||||
assert.Equal(t, 0, match.I)
|
||||
assert.Equal(t, 2, match.J)
|
||||
assert.Equal(t, "aaa", match.Token)
|
||||
assert.NotZero(t, match.Entropy, "Entropy should be set")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,14 +66,17 @@ func TestSequenceMatch(t *testing.T) {
|
|||
assert.Equal(t, 0, match.I)
|
||||
assert.Equal(t, 3, match.J)
|
||||
assert.Equal(t, "abcd", match.Token)
|
||||
assert.NotZero(t, match.Entropy, "Entropy should be set")
|
||||
} else if match.DictionaryName == "upper" {
|
||||
assert.Equal(t, 10, match.I)
|
||||
assert.Equal(t, 14, match.J)
|
||||
assert.Equal(t, "LMNOP", match.Token)
|
||||
assert.NotZero(t, match.Entropy, "Entropy should be set")
|
||||
} else if match.DictionaryName == "digits" {
|
||||
assert.Equal(t, 21, match.I)
|
||||
assert.Equal(t, 24, match.J)
|
||||
assert.Equal(t, "1234", match.Token)
|
||||
assert.NotZero(t, match.Entropy, "Entropy should be set")
|
||||
} else {
|
||||
assert.True(t, false, "Unknow dictionary")
|
||||
}
|
||||
|
@ -80,15 +86,18 @@ func TestSequenceMatch(t *testing.T) {
|
|||
func TestSpatialMatchQwerty(t *testing.T) {
|
||||
matches := SpatialMatch("qwerty")
|
||||
assert.Len(t, matches, 1, "Lenght should be 1")
|
||||
assert.NotZero(t, matches[0].Entropy, "Entropy should be set")
|
||||
|
||||
matches = SpatialMatch("asdf")
|
||||
assert.Len(t, matches, 1, "Lenght should be 1")
|
||||
assert.NotZero(t, matches[0].Entropy, "Entropy should be set")
|
||||
|
||||
}
|
||||
|
||||
func TestSpatialMatchDvorak(t *testing.T) {
|
||||
matches := SpatialMatch("aoeuidhtns")
|
||||
assert.Len(t, matches, 1, "Lenght should be 1")
|
||||
assert.NotZero(t, matches[0].Entropy, "Entropy should be set")
|
||||
|
||||
}
|
||||
|
||||
|
@ -100,6 +109,10 @@ func TestDictionaryMatch(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Len(t, matches, 4, "Lenght should be 4")
|
||||
for _, match := range matches {
|
||||
assert.NotZero(t, match.Entropy, "Entropy should be set")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,11 @@ package scoring
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/nbutton23/zxcvbn-go/adjacency"
|
||||
"github.com/nbutton23/zxcvbn-go/entropy"
|
||||
"github.com/nbutton23/zxcvbn-go/match"
|
||||
"github.com/nbutton23/zxcvbn-go/matching"
|
||||
"github.com/nbutton23/zxcvbn-go/utils/math"
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -43,7 +40,7 @@ Returns minimum entropy
|
|||
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))
|
||||
bruteforceCardinality := float64(entropy.CalcBruteForceCardinality(password))
|
||||
upToK := make([]float64, len(password))
|
||||
backPointers := make([]match.Match, len(password))
|
||||
|
||||
|
@ -58,7 +55,7 @@ func MinimumEntropyMatchSequence(password string, matches []match.Match) MinEntr
|
|||
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)
|
||||
calculatedEntropy := match.Entropy
|
||||
match.Entropy = calculatedEntropy
|
||||
candidateEntropy := upTo + calculatedEntropy
|
||||
|
||||
|
@ -87,7 +84,7 @@ func MinimumEntropyMatchSequence(password string, matches []match.Match) MinEntr
|
|||
}
|
||||
sort.Sort(match.Matches(matchSequence))
|
||||
|
||||
makeBruteForecMatch := func(i, j int) match.Match {
|
||||
makeBruteForceMatch := func(i, j int) match.Match {
|
||||
return match.Match{Pattern: "bruteforce",
|
||||
I: i,
|
||||
J: j,
|
||||
|
@ -101,14 +98,14 @@ func MinimumEntropyMatchSequence(password string, matches []match.Match) MinEntr
|
|||
for _, match := range matchSequence {
|
||||
i, j := match.I, match.J
|
||||
if i-k > 0 {
|
||||
matchSequenceCopy = append(matchSequenceCopy, makeBruteForecMatch(k, i-1))
|
||||
matchSequenceCopy = append(matchSequenceCopy, makeBruteForceMatch(k, i-1))
|
||||
}
|
||||
k = j + 1
|
||||
matchSequenceCopy = append(matchSequenceCopy, match)
|
||||
}
|
||||
|
||||
if k < len(password) {
|
||||
matchSequenceCopy = append(matchSequenceCopy, makeBruteForecMatch(k, len(password)-1))
|
||||
matchSequenceCopy = append(matchSequenceCopy, makeBruteForceMatch(k, len(password)-1))
|
||||
}
|
||||
var minEntropy float64
|
||||
if len(password) == 0 {
|
||||
|
@ -133,170 +130,6 @@ func get(a []float64, i int) float64 {
|
|||
|
||||
return a[i]
|
||||
}
|
||||
func calcBruteforceCardinality(password string) float64 {
|
||||
lower, upper, digits, symbols := float64(0), float64(0), float64(0), float64(0)
|
||||
|
||||
for _, char := range password {
|
||||
if unicode.IsLower(char) {
|
||||
lower = float64(26)
|
||||
} else if unicode.IsDigit(char) {
|
||||
digits = float64(10)
|
||||
} else if unicode.IsUpper(char) {
|
||||
upper = float64(26)
|
||||
} else {
|
||||
symbols = float64(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)
|
||||
} else if match.Pattern == "repeat" {
|
||||
entropy = repeatEntropy(match)
|
||||
} else if match.Pattern == "sequence" {
|
||||
entropy = sequenceEntropy(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(len(adjacency.BuildQwerty().Graph))
|
||||
d = adjacency.BuildKeypad().CalculateAvgDegree()
|
||||
} else {
|
||||
s = float64(matching.KEYPAD_STARTING_POSITIONS)
|
||||
d = matching.KEYPAD_AVG_DEGREE
|
||||
}
|
||||
|
||||
possibilities := float64(0)
|
||||
|
||||
length := float64(len(match.Token))
|
||||
t := match.Turns
|
||||
|
||||
//TODO: Should this be <= or just < ?
|
||||
//Estimate the number of possible patterns w/ length L or less with t turns or less
|
||||
for i := float64(2); i <= length+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 := length - 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 sequenceEntropy(match match.Match) float64 {
|
||||
firstChar := match.Token[0]
|
||||
baseEntropy := float64(0)
|
||||
if string(firstChar) == "a" || string(firstChar) == "1" {
|
||||
baseEntropy = float64(0)
|
||||
} else {
|
||||
baseEntropy = math.Log2(float64(match.DictionaryLength))
|
||||
if unicode.IsUpper(rune(firstChar)) {
|
||||
baseEntropy++
|
||||
}
|
||||
}
|
||||
|
||||
if !match.Ascending {
|
||||
baseEntropy++
|
||||
}
|
||||
return baseEntropy + math.Log2(float64(len(match.Token)))
|
||||
}
|
||||
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 repeatEntropy(match match.Match) float64 {
|
||||
cardinality := calcBruteforceCardinality(match.Token)
|
||||
entropy := math.Log2(cardinality * float64(len(match.Token)))
|
||||
|
||||
return entropy
|
||||
}
|
||||
|
||||
func entropyToCrackTime(entropy float64) float64 {
|
||||
crackTime := (0.5 * math.Pow(float64(2), entropy)) * SECONDS_PER_GUESS
|
||||
|
|
137
zxcvbn_test.go
137
zxcvbn_test.go
|
@ -1,70 +1,93 @@
|
|||
package zxcvbn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
"time"
|
||||
|
||||
"math"
|
||||
"strconv"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/**
|
||||
Use these test to see how close to feature parity the library is.
|
||||
*/
|
||||
|
||||
|
||||
const (
|
||||
allowableError = float64(0.01)
|
||||
)
|
||||
|
||||
type failedTest struct {
|
||||
Password string
|
||||
Expect float64
|
||||
Actual float64
|
||||
PError float64
|
||||
|
||||
}
|
||||
|
||||
var failedTests []failedTest
|
||||
var numTestRan int
|
||||
|
||||
func TestPasswordStrength(t *testing.T) {
|
||||
cfg := &quick.Config{Rand: rand.New(rand.NewSource(time.Now().Unix()))}
|
||||
err := quick.CheckEqual(GoPasswordStrength, PythonPasswordStrength, cfg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
//Expected calculated by running zxcvbn-python
|
||||
runTest(t, "zxcvbn", float64(6.845490050944376))
|
||||
runTest(t, "Tr0ub4dour&3",float64(17.296) )
|
||||
runTest(t,"qwER43@!", float64(26.44) )
|
||||
runTest(t,"correcthorsebatterystaple", float64(45.212) )
|
||||
runTest(t,"coRrecth0rseba++ery9.23.2007staple$", float64(66.018) )
|
||||
runTest(t,"D0g..................", float64(20.678) )
|
||||
runTest(t, "abcdefghijk987654321", float64(11.951))
|
||||
runTest(t, "neverforget13/3/1997", float64(32.628))
|
||||
runTest(t, "1qaz2wsx3edc", float64(19.314))
|
||||
runTest(t, "temppass22", float64(22.179))
|
||||
runTest(t, "briansmith", float64(4.322))
|
||||
runTest(t, "briansmith4mayor", float64(18.64))
|
||||
runTest(t, "password1", float64(2.0))
|
||||
runTest(t, "viking", float64(7.531))
|
||||
runTest(t, "thx1138", float64(7.426))
|
||||
runTest(t, "ScoRpi0ns", float64(20.621))
|
||||
runTest(t, "do you know", float64(4.585))
|
||||
runTest(t, "ryanhunter2000", float64(14.506))
|
||||
runTest(t, "rianhunter2000", float64(21.734))
|
||||
runTest(t, "asdfghju7654rewq", float64(29.782))
|
||||
runTest(t, "AOEUIDHG&*()LS_", float64(33.254))
|
||||
runTest(t, "12345678", float64(1.585))
|
||||
runTest(t, "defghi6789", float64(12.607))
|
||||
runTest(t, "rosebud", float64(7.937))
|
||||
runTest(t, "Rosebud", float64(8.937))
|
||||
runTest(t, "ROSEBUD", float64(8.937))
|
||||
runTest(t, "rosebuD", float64(8.937))
|
||||
runTest(t, "ros3bud99", float64(19.276))
|
||||
runTest(t, "r0s3bud99", float64(19.276))
|
||||
runTest(t, "R0$38uD99", float64(34.822))
|
||||
runTest(t, "verlineVANDERMARK", float64(26.293))
|
||||
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) )
|
||||
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
func GoPasswordStrength(password string, userInputs []string) float64 {
|
||||
return PasswordStrength(password, userInputs).Entropy
|
||||
}
|
||||
|
||||
func PythonPasswordStrength(password string, userInputs []string) float64 {
|
||||
cmd := exec.Command("python", append([]string{"-", password}, userInputs...)...)
|
||||
cmd.Stdin = bytes.NewBufferString(py)
|
||||
|
||||
o, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Println("outErr:", err)
|
||||
}
|
||||
|
||||
var pmatch pyMatch
|
||||
if err := json.Unmarshal(o, &pmatch); err != nil {
|
||||
fmt.Println("json:", err)
|
||||
}
|
||||
|
||||
return pmatch.Entropy
|
||||
}
|
||||
|
||||
const py = `import zxcvbn
|
||||
import json
|
||||
import sys
|
||||
|
||||
print json.dumps(zxcvbn.password_strength(sys.argv[1], sys.argv[2:len(sys.argv)]))
|
||||
`
|
||||
|
||||
type pyMatch struct {
|
||||
CalcTime float64 `json:"calc_time"`
|
||||
CrackTime float64 `json:"crack_time"`
|
||||
CrackTimeDisplay string `json:"crack_time_display"`
|
||||
Entropy float64 `json:"entropy"`
|
||||
MatchSequence []struct {
|
||||
BaseEntropy float64 `json:"base_entropy"`
|
||||
DictionaryName string `json:"dictionary_name"`
|
||||
Entropy float64 `json:"entropy"`
|
||||
I int64 `json:"i"`
|
||||
J int64 `json:"j"`
|
||||
L33tEntropy float64 `json:"l33t_entropy"`
|
||||
MatchedWord string `json:"matched_word"`
|
||||
Pattern string `json:"pattern"`
|
||||
Rank int64 `json:"rank"`
|
||||
Token string `json:"token"`
|
||||
UppercaseEntropy float64 `json:"uppercase_entropy"`
|
||||
} `json:"match_sequence"`
|
||||
Password string `json:"password"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue