Merge pull request #11 from nbutton23/button-wip

Button wip
This commit is contained in:
Nathan Button 2016-01-22 09:18:37 -07:00
commit 7d8c28acb6
8 changed files with 412 additions and 269 deletions

View File

@ -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

View File

@ -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)))
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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")
}
}

View File

@ -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

View File

@ -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"`
}