entropyCalculator.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. package entropy
  2. import (
  3. "github.com/nbutton23/zxcvbn-go/adjacency"
  4. "github.com/nbutton23/zxcvbn-go/match"
  5. "github.com/nbutton23/zxcvbn-go/utils/math"
  6. "math"
  7. "regexp"
  8. "unicode"
  9. )
  10. const (
  11. START_UPPER string = `^[A-Z][^A-Z]+$`
  12. END_UPPER string = `^[^A-Z]+[A-Z]$'`
  13. ALL_UPPER string = `^[A-Z]+$`
  14. NUM_YEARS = float64(119) // years match against 1900 - 2019
  15. NUM_MONTHS = float64(12)
  16. NUM_DAYS = float64(31)
  17. )
  18. var (
  19. KEYPAD_STARTING_POSITIONS = len(adjacency.AdjacencyGph["keypad"].Graph)
  20. KEYPAD_AVG_DEGREE = adjacency.AdjacencyGph["keypad"].CalculateAvgDegree()
  21. )
  22. func DictionaryEntropy(match match.Match, rank float64) float64 {
  23. baseEntropy := math.Log2(rank)
  24. upperCaseEntropy := extraUpperCaseEntropy(match)
  25. //TODO: L33t
  26. return baseEntropy + upperCaseEntropy
  27. }
  28. func extraUpperCaseEntropy(match match.Match) float64 {
  29. word := match.Token
  30. allLower := true
  31. for _, char := range word {
  32. if unicode.IsUpper(char) {
  33. allLower = false
  34. break
  35. }
  36. }
  37. if allLower {
  38. return float64(0)
  39. }
  40. //a capitalized word is the most common capitalization scheme,
  41. //so it only doubles the search space (uncapitalized + capitalized): 1 extra bit of entropy.
  42. //allcaps and end-capitalized are common enough too, underestimate as 1 extra bit to be safe.
  43. for _, regex := range []string{START_UPPER, END_UPPER, ALL_UPPER} {
  44. matcher := regexp.MustCompile(regex)
  45. if matcher.MatchString(word) {
  46. return float64(1)
  47. }
  48. }
  49. //Otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters with U uppercase letters or
  50. //less. Or, if there's more uppercase than lower (for e.g. PASSwORD), the number of ways to lowercase U+L letters
  51. //with L lowercase letters or less.
  52. countUpper, countLower := float64(0), float64(0)
  53. for _, char := range word {
  54. if unicode.IsUpper(char) {
  55. countUpper++
  56. } else if unicode.IsLower(char) {
  57. countLower++
  58. }
  59. }
  60. totalLenght := countLower + countUpper
  61. var possibililities float64
  62. for i := float64(0); i <= math.Min(countUpper, countLower); i++ {
  63. possibililities += float64(zxcvbn_math.NChoseK(totalLenght, i))
  64. }
  65. if possibililities < 1 {
  66. return float64(1)
  67. }
  68. return float64(math.Log2(possibililities))
  69. }
  70. func SpatialEntropy(match match.Match, turns int, shiftCount int) float64 {
  71. var s, d float64
  72. if match.DictionaryName == "qwerty" || match.DictionaryName == "dvorak" {
  73. //todo: verify qwerty and dvorak have the same length and degree
  74. s = float64(len(adjacency.BuildQwerty().Graph))
  75. d = adjacency.BuildQwerty().CalculateAvgDegree()
  76. } else {
  77. s = float64(KEYPAD_STARTING_POSITIONS)
  78. d = KEYPAD_AVG_DEGREE
  79. }
  80. possibilities := float64(0)
  81. length := float64(len(match.Token))
  82. //TODO: Should this be <= or just < ?
  83. //Estimate the number of possible patterns w/ length L or less with t turns or less
  84. for i := float64(2); i <= length+1; i++ {
  85. possibleTurns := math.Min(float64(turns), i-1)
  86. for j := float64(1); j <= possibleTurns+1; j++ {
  87. x := zxcvbn_math.NChoseK(i-1, j-1) * s * math.Pow(d, j)
  88. possibilities += x
  89. }
  90. }
  91. entropy := math.Log2(possibilities)
  92. //add extra entropu for shifted keys. ( % instead of 5 A instead of a)
  93. //Math is similar to extra entropy for uppercase letters in dictionary matches.
  94. if S := float64(shiftCount); S > float64(0) {
  95. possibilities = float64(0)
  96. U := length - S
  97. for i := float64(0); i < math.Min(S, U)+1; i++ {
  98. possibilities += zxcvbn_math.NChoseK(S+U, i)
  99. }
  100. entropy += math.Log2(possibilities)
  101. }
  102. return entropy
  103. }
  104. func RepeatEntropy(match match.Match) float64 {
  105. cardinality := CalcBruteForceCardinality(match.Token)
  106. entropy := math.Log2(cardinality * float64(len(match.Token)))
  107. return entropy
  108. }
  109. //TODO: Validate against python
  110. func CalcBruteForceCardinality(password string) float64 {
  111. lower, upper, digits, symbols := float64(0), float64(0), float64(0), float64(0)
  112. for _, char := range password {
  113. if unicode.IsLower(char) {
  114. lower = float64(26)
  115. } else if unicode.IsDigit(char) {
  116. digits = float64(10)
  117. } else if unicode.IsUpper(char) {
  118. upper = float64(26)
  119. } else {
  120. symbols = float64(33)
  121. }
  122. }
  123. cardinality := lower + upper + digits + symbols
  124. return cardinality
  125. }
  126. func SequenceEntropy(match match.Match, dictionaryLength int, ascending bool) float64 {
  127. firstChar := match.Token[0]
  128. baseEntropy := float64(0)
  129. if string(firstChar) == "a" || string(firstChar) == "1" {
  130. baseEntropy = float64(0)
  131. } else {
  132. baseEntropy = math.Log2(float64(dictionaryLength))
  133. //TODO: should this be just the first or any char?
  134. if unicode.IsUpper(rune(firstChar)) {
  135. baseEntropy++
  136. }
  137. }
  138. if !ascending {
  139. baseEntropy++
  140. }
  141. return baseEntropy + math.Log2(float64(len(match.Token)))
  142. }
  143. func ExtraLeetEntropy(match match.Match, password string) float64 {
  144. var subsitutions float64
  145. var unsub float64
  146. subPassword := password[match.I:match.J]
  147. for index, char := range subPassword {
  148. if string(char) != string(match.Token[index]) {
  149. subsitutions++
  150. } else {
  151. //TODO: Make this only true for 1337 chars that are not subs?
  152. unsub++
  153. }
  154. }
  155. var possibilities float64
  156. for i := float64(0); i <= math.Min(subsitutions, unsub)+1; i++ {
  157. possibilities += zxcvbn_math.NChoseK(subsitutions+unsub, i)
  158. }
  159. if possibilities <= 1 {
  160. return float64(1)
  161. }
  162. return math.Log2(possibilities)
  163. }
  164. func YearEntropy(dateMatch match.DateMatch) float64 {
  165. return math.Log2(NUM_YEARS)
  166. }
  167. func DateEntropy(dateMatch match.DateMatch) float64 {
  168. var entropy float64
  169. if dateMatch.Year < 100 {
  170. entropy = math.Log2(NUM_DAYS * NUM_MONTHS * 100)
  171. } else {
  172. entropy = math.Log2(NUM_DAYS * NUM_MONTHS * NUM_YEARS)
  173. }
  174. if dateMatch.Separator != "" {
  175. entropy += 2 //add two bits for separator selection [/,-,.,etc]
  176. }
  177. return entropy
  178. }