2016-01-21 22:26:33 +00:00
|
|
|
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]+$`
|
2016-03-08 16:57:50 +00:00
|
|
|
NUM_YEARS = float64(119) // years match against 1900 - 2019
|
|
|
|
NUM_MONTHS = float64(12)
|
|
|
|
NUM_DAYS = float64(31)
|
2016-01-21 22:26:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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" {
|
2016-01-21 22:40:34 +00:00
|
|
|
//todo: verify qwerty and dvorak have the same length and degree
|
2016-01-21 22:26:33 +00:00
|
|
|
s = float64(len(adjacency.BuildQwerty().Graph))
|
2016-01-21 22:40:34 +00:00
|
|
|
d = adjacency.BuildQwerty().CalculateAvgDegree()
|
2016-01-21 22:26:33 +00:00
|
|
|
} else {
|
2016-01-21 22:40:34 +00:00
|
|
|
s = float64(KEYPAD_STARTING_POSITIONS)
|
|
|
|
d = KEYPAD_AVG_DEGREE
|
2016-01-21 22:26:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-01-21 22:40:34 +00:00
|
|
|
//TODO: Validate against python
|
2016-01-21 22:26:33 +00:00
|
|
|
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))
|
2016-01-29 21:53:09 +00:00
|
|
|
//TODO: should this be just the first or any char?
|
2016-01-21 22:26:33 +00:00
|
|
|
if unicode.IsUpper(rune(firstChar)) {
|
|
|
|
baseEntropy++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !ascending {
|
|
|
|
baseEntropy++
|
|
|
|
}
|
|
|
|
return baseEntropy + math.Log2(float64(len(match.Token)))
|
|
|
|
}
|
2016-01-29 21:53:09 +00:00
|
|
|
|
|
|
|
func ExtraLeetEntropy(match match.Match, password string) float64 {
|
|
|
|
var subsitutions float64
|
|
|
|
var unsub float64
|
|
|
|
subPassword := password[match.I:match.J]
|
|
|
|
for index, char := range subPassword {
|
|
|
|
if string(char) != string(match.Token[index]) {
|
|
|
|
subsitutions++
|
|
|
|
} else {
|
|
|
|
//TODO: Make this only true for 1337 chars that are not subs?
|
|
|
|
unsub++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var possibilities float64
|
|
|
|
|
|
|
|
for i := float64(0); i <= math.Min(subsitutions, unsub)+1; i++ {
|
|
|
|
possibilities += zxcvbn_math.NChoseK(subsitutions+unsub, i)
|
|
|
|
}
|
|
|
|
|
|
|
|
if possibilities <= 1 {
|
|
|
|
return float64(1)
|
|
|
|
}
|
|
|
|
return math.Log2(possibilities)
|
|
|
|
}
|
2016-03-08 16:57:50 +00:00
|
|
|
|
|
|
|
func YearEntropy(dateMatch match.DateMatch) float64 {
|
|
|
|
return math.Log2(NUM_YEARS)
|
|
|
|
}
|
|
|
|
|
|
|
|
func DateEntropy(dateMatch match.DateMatch) float64 {
|
|
|
|
var entropy float64
|
|
|
|
if dateMatch.Year < 100 {
|
|
|
|
entropy = math.Log2(NUM_DAYS * NUM_MONTHS * 100)
|
|
|
|
} else {
|
|
|
|
entropy = math.Log2(NUM_DAYS * NUM_MONTHS * NUM_YEARS)
|
|
|
|
}
|
|
|
|
|
|
|
|
if dateMatch.Separator != "" {
|
|
|
|
entropy += 2 //add two bits for separator selection [/,-,.,etc]
|
|
|
|
}
|
|
|
|
return entropy
|
|
|
|
}
|