feat: Added new endpoint to get password strength info (#2589)

By integrating `zxcvbn` module, it has been added a new endpoint to get password strength quality information like Entropy, CrackTime, CrackTimeDisplay, Score, MatchSequence and CalcTime.

Added related dependences.

Closes #4980
This commit is contained in:
Noelia 2022-03-18 13:20:13 +01:00 committed by GitHub
parent 23b6883166
commit 7ef8bc68c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2084 additions and 2 deletions

1
go.mod
View File

@ -61,6 +61,7 @@ require (
github.com/status-im/rendezvous v1.3.4-0.20211008144244-bdf13155817d
github.com/status-im/status-go/extkeys v1.1.2
github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501
github.com/status-im/zxcvbn-go v0.0.0-20220311183720-5e8676676857
github.com/stretchr/testify v1.7.0
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
github.com/tsenart/tb v0.0.0-20181025101425-0d2499c8b6e9

3
go.sum
View File

@ -1235,12 +1235,15 @@ github.com/status-im/status-go/extkeys v1.1.2 h1:FSjARgDathJ3rIapJt851LsIXP9Oyuu
github.com/status-im/status-go/extkeys v1.1.2/go.mod h1:hCmFzb2jiiVF2voZKYbzuhOQiHHCmyLJsZJXrFFg7BY=
github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501 h1:oa0KU5jJRNtXaM/P465MhvSFo/HM2O8qi2DDuPcd7ro=
github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501/go.mod h1:RYo/itke1oU5k/6sj9DNM3QAwtE5rZSgg5JnkOv83hk=
github.com/status-im/zxcvbn-go v0.0.0-20220311183720-5e8676676857 h1:sPkzT7Z7uLmejOsBRlZ0kwDWpqjpHJsp834o5nbhqho=
github.com/status-im/zxcvbn-go v0.0.0-20220311183720-5e8676676857/go.mod h1:lq9I5ROto5tcua65GmCE6SIW7VE0ucdEBs1fn4z7uWU=
github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

View File

@ -13,6 +13,8 @@ import (
"github.com/ethereum/go-ethereum/log"
signercore "github.com/ethereum/go-ethereum/signer/core"
"github.com/status-im/zxcvbn-go"
"github.com/status-im/status-go/api"
"github.com/status-im/status-go/api/multiformat"
"github.com/status-im/status-go/eth-node/types"
@ -404,7 +406,7 @@ func SignTypedData(data, address, password string) string {
}
// HashTypedData unmarshalls data into TypedData, validates it and hashes it.
//export HashTypeData
//export HashTypedData
func HashTypedData(data string) string {
var typed typeddata.TypedData
err := json.Unmarshal([]byte(data), &typed)
@ -432,7 +434,7 @@ func SignTypedDataV4(data, address, password string) string {
}
// HashTypedDataV4 unmarshalls data into TypedData, validates it and hashes it.
//export HashTypeDataV4
//export HashTypedDataV4
func HashTypedDataV4(data string) string {
var typed signercore.TypedData
err := json.Unmarshal([]byte(data), &typed)
@ -742,3 +744,26 @@ func ImageServerTLSCert() string {
return cert
}
// GetPasswordStrength uses zxcvbn module and generates a JSON containing information about the quality of the given password
// (Entropy, CrackTime, CrackTimeDisplay, Score, MatchSequence and CalcTime).
// userInputs argument can be whatever list of strings like user's personal info or site-specific vocabulary that zxcvbn will
// make use to determine the result.
// For more details on usage see https://github.com/status-im/zxcvbn-go
func GetPasswordStrength(paramsJSON string) string {
var params struct {
Password string `json:"password"`
UserInputs []string `json:"userInputs"`
}
err := json.Unmarshal([]byte(paramsJSON), &params)
if err != nil {
return makeJSONResponse(err)
}
data, err := json.Marshal(zxcvbn.PasswordStrength(params.Password, params.UserInputs))
if err != nil {
return makeJSONResponse(fmt.Errorf("Error marshalling to json: %v", err))
}
return string(data)
}

2
vendor/github.com/status-im/zxcvbn-go/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
zxcvbn
debug.test

20
vendor/github.com/status-im/zxcvbn-go/LICENSE.txt generated vendored Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) Nathan Button
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

15
vendor/github.com/status-im/zxcvbn-go/Makefile generated vendored Normal file
View File

@ -0,0 +1,15 @@
PKG_LIST = $$( go list ./... | grep -v /vendor/ | grep -v "zxcvbn-go/data" )
.DEFAULT_GOAL := help
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: test
test: ## Run `go test {Package list}` on the packages
go test $(PKG_LIST)
.PHONY: lint
lint: ## Run `golint {Package list}`
golint $(PKG_LIST)

78
vendor/github.com/status-im/zxcvbn-go/README.md generated vendored Normal file
View File

@ -0,0 +1,78 @@
This is a goLang port of python-zxcvbn and [zxcvbn](https://github.com/dropbox/zxcvbn), which are python and JavaScript password strength
generators. zxcvbn attempts to give sound password advice through pattern
matching and conservative entropy calculations. It finds 10k common passwords,
common American names and surnames, common English words, and common patterns
like dates, repeats (aaa), sequences (abcd), and QWERTY patterns.
Please refer to https://dropbox.tech/security/zxcvbn-realistic-password-strength-estimation for the full details and
motivation behind zxcbvn. The source code for the original JavaScript (well,
actually CoffeeScript) implementation can be found at:
https://github.com/lowe/zxcvbn
Python at:
https://github.com/dropbox/python-zxcvbn
For full motivation, see:
https://dropbox.tech/security/zxcvbn-realistic-password-strength-estimation
------------------------------------------------------------------------
Use
------------------------------------------------------------------------
The zxcvbn module has the public method PasswordStrength() function. Import zxcvbn, and
call PasswordStrength(password string, userInputs []string). The function will return a
result dictionary with the following keys:
Entropy # bits
CrackTime # estimation of actual crack time, in seconds.
CrackTimeDisplay # same crack time, as a friendlier string:
# "instant", "6 minutes", "centuries", etc.
Score # [0,1,2,3,4] if crack time is less than
# [10^2, 10^4, 10^6, 10^8, Infinity].
# (useful for implementing a strength bar.)
MatchSequence # the list of patterns that zxcvbn based the
# entropy calculation on.
CalcTime # how long it took to calculate an answer,
# in milliseconds. usually only a few ms.
The userInputs argument is an splice of strings that zxcvbn
will add to its internal dictionary. This can be whatever list of
strings you like, but is meant for user inputs from other fields of the
form, like name and email. That way a password that includes the user's
personal info can be heavily penalized. This list is also good for
site-specific vocabulary.
Bug reports and pull requests welcome!
------------------------------------------------------------------------
Project Status
------------------------------------------------------------------------
Use zxcvbn_test.go to check how close to feature parity the project is.
------------------------------------------------------------------------
Acknowledgment
------------------------------------------------------------------------
Thanks to Dan Wheeler (https://github.com/lowe) for the CoffeeScript implementation
(see above.) To repeat his outside acknowledgements (which remain useful, as always):
Many thanks to Mark Burnett for releasing his 10k top passwords list:
https://xato.net/passwords/more-top-worst-passwords
and for his 2006 book,
"Perfect Passwords: Selection, Protection, Authentication"
Huge thanks to Wiktionary contributors for building a frequency list
of English as used in television and movies:
https://en.wiktionary.org/wiki/Wiktionary:Frequency_lists
Last but not least, big thanks to xkcd :)
https://xkcd.com/936/

View File

@ -0,0 +1,108 @@
package adjacency
import (
"encoding/json"
"log"
"github.com/status-im/zxcvbn-go/data"
)
// Graph holds information about different graphs
type Graph struct {
Graph map[string][]string
averageDegree float64
Name string
}
// GraphMap is a map of all graphs
var GraphMap = make(map[string]Graph)
func init() {
GraphMap["qwerty"] = BuildQwerty()
GraphMap["dvorak"] = BuildDvorak()
GraphMap["keypad"] = BuildKeypad()
GraphMap["macKeypad"] = BuildMacKeypad()
GraphMap["l33t"] = BuildLeet()
}
//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")
}
//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")
}
//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)
}
graph.Name = name
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 Graph) CalculateAvgDegree() float64 {
if adjGrp.averageDegree != float64(0) {
return adjGrp.averageDegree
}
var avg float64
var count float64
for _, value := range adjGrp.Graph {
for _, char := range value {
if len(char) != 0 || char != " " {
avg += float64(len(char))
count++
}
}
}
adjGrp.averageDegree = avg / count
return adjGrp.averageDegree
}

444
vendor/github.com/status-im/zxcvbn-go/data/bindata.go generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,216 @@
package entropy
import (
"github.com/status-im/zxcvbn-go/adjacency"
"github.com/status-im/zxcvbn-go/match"
"github.com/status-im/zxcvbn-go/utils/math"
"math"
"regexp"
"unicode"
)
const (
numYears = float64(119) // years match against 1900 - 2019
numMonths = float64(12)
numDays = float64(31)
)
var (
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)
//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 _, matcher := range []*regexp.Regexp{startUpperRx, endUpperRx, allUpperRx} {
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(zxcvbnmath.NChoseK(totalLenght, i))
}
if possibililities < 1 {
return float64(1)
}
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" {
//todo: verify qwerty and dvorak have the same length and degree
s = float64(len(adjacency.BuildQwerty().Graph))
d = adjacency.BuildQwerty().CalculateAvgDegree()
} else {
s = float64(keyPadStartingPositions)
d = keyPadAvgDegree
}
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 := zxcvbnmath.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 += zxcvbnmath.NChoseK(S+U, i)
}
entropy += math.Log2(possibilities)
}
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)))
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)
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
}
// 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)
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)))
}
// ExtraLeetEntropy calulates the added entropy provied by l33t substitustions
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 += zxcvbnmath.NChoseK(subsitutions+unsub, i)
}
if possibilities <= 1 {
return float64(1)
}
return math.Log2(possibilities)
}
// DateEntropy calculates the entropy provided by a date
func DateEntropy(dateMatch match.DateMatch) float64 {
var entropy float64
if dateMatch.Year < 100 {
entropy = math.Log2(numDays * numMonths * 100)
} else {
entropy = math.Log2(numDays * numMonths * numYears)
}
if dateMatch.Separator != "" {
entropy += 2 //add two bits for separator selection [/,-,.,etc]
}
return entropy
}

View File

@ -0,0 +1,50 @@
package frequency
import (
"encoding/json"
"log"
"github.com/status-im/zxcvbn-go/data"
)
// List holds a frequency list
type List struct {
Name string
List []string
}
// Lists holds all the frequency list in a map
var Lists = make(map[string]List)
func init() {
maleFilePath := getAsset("data/MaleNames.json")
femaleFilePath := getAsset("data/FemaleNames.json")
surnameFilePath := getAsset("data/Surnames.json")
englishFilePath := getAsset("data/English.json")
passwordsFilePath := getAsset("data/Passwords.json")
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 := data.Asset(name)
if err != nil {
panic("Error getting asset " + name)
}
return data
}
func getStringListFromAsset(data []byte, name string) List {
var tempList List
err := json.Unmarshal(data, &tempList)
if err != nil {
log.Fatal(err)
}
tempList.Name = name
return tempList
}

9
vendor/github.com/status-im/zxcvbn-go/go.mod generated vendored Normal file
View File

@ -0,0 +1,9 @@
module github.com/status-im/zxcvbn-go
go 1.14
require (
github.com/davecgh/go-spew v1.1.0
github.com/pmezard/go-difflib v1.0.0
github.com/stretchr/testify v1.1.4
)

5
vendor/github.com/status-im/zxcvbn-go/go.sum generated vendored Normal file
View File

@ -0,0 +1,5 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ=
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

44
vendor/github.com/status-im/zxcvbn-go/match/match.go generated vendored Normal file
View File

@ -0,0 +1,44 @@
package match
//Matches is an alies for []Match used for sorting
type Matches []Match
func (s Matches) Len() int {
return len(s)
}
func (s Matches) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Matches) Less(i, j int) bool {
if s[i].I < s[j].I {
return true
} else if s[i].I == s[j].I {
return s[i].J < s[j].J
} else {
return false
}
}
// Match represents different matches
type Match struct {
Pattern string
I, J int
Token string
DictionaryName string
Entropy float64
}
//DateMatch is specifilly a match for type date
type DateMatch struct {
Pattern string
I, J int
Token string
Separator string
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
}

View File

@ -0,0 +1,209 @@
package matching
import (
"regexp"
"strconv"
"strings"
"github.com/status-im/zxcvbn-go/entropy"
"github.com/status-im/zxcvbn-go/match"
)
const (
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 == dateSepMatcherName
}
//FilterDateWithoutSepMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher
func FilterDateWithoutSepMatcher(m match.Matcher) bool {
return m.ID == dateWithOutSepMatcherName
}
func checkDate(day, month, year int64) (bool, int64, int64, int64) {
if (12 <= month && month <= 31) && day <= 12 {
day, month = month, day
}
if day > 31 || month > 12 {
return false, 0, 0, 0
}
if !((1900 <= year && year <= 2019) || (0 <= year && year <= 99)) {
return false, 0, 0, 0
}
return true, day, month, year
}
func dateSepMatcher(password string) []match.Match {
dateMatches := dateSepMatchHelper(password)
var matches []match.Match
for _, dateMatch := range dateMatches {
match := match.Match{
I: dateMatch.I,
J: dateMatch.J,
Entropy: entropy.DateEntropy(dateMatch),
DictionaryName: "date_match",
Token: dateMatch.Token,
}
matches = append(matches, match)
}
return matches
}
func dateSepMatchHelper(password string) []match.DateMatch {
var matches []match.DateMatch
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)
month, _ := strconv.ParseInt(splitV[0][2], 10, 16)
year, _ := strconv.ParseInt(splitV[0][6], 10, 16)
match := match.DateMatch{Day: day, Month: month, Year: year, Separator: splitV[0][5], I: i, J: j, Token: password[i:j]}
matches = append(matches, match)
}
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)
month, _ := strconv.ParseInt(splitV[0][6], 10, 16)
year, _ := strconv.ParseInt(splitV[0][2], 10, 16)
match := match.DateMatch{Day: day, Month: month, Year: year, Separator: splitV[0][5], I: i, J: j, Token: password[i:j]}
matches = append(matches, match)
}
var out []match.DateMatch
for _, match := range matches {
if valid, day, month, year := checkDate(match.Day, match.Month, match.Year); valid {
match.Pattern = "date"
match.Day = day
match.Month = month
match.Year = year
out = append(out, match)
}
}
return out
}
type dateMatchCandidate struct {
DayMonth string
Year string
I, J int
}
type dateMatchCandidateTwo struct {
Day string
Month string
Year string
I, J int
}
func dateWithoutSepMatch(password string) []match.Match {
dateMatches := dateWithoutSepMatchHelper(password)
var matches []match.Match
for _, dateMatch := range dateMatches {
match := match.Match{
I: dateMatch.I,
J: dateMatch.J,
Entropy: entropy.DateEntropy(dateMatch),
DictionaryName: "date_match",
Token: dateMatch.Token,
}
matches = append(matches, match)
}
return matches
}
//TODO Has issues with 6 digit dates
func dateWithoutSepMatchHelper(password string) (matches []match.DateMatch) {
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
if length <= 6 {
//2-digit year prefix
candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[2:], v[0:2], i, j))
//2-digityear suffix
candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[0:lastIndex-2], v[lastIndex-2:], i, j))
}
if length >= 6 {
//4-digit year prefix
candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[4:], v[0:4], i, j))
//4-digit year sufix
candidatesRoundOne = append(candidatesRoundOne, buildDateMatchCandidate(v[0:lastIndex-3], v[lastIndex-3:], i, j))
}
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))
} else if len(c.DayMonth) == 3 {
candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0:2], c.DayMonth[2:2], c.Year, c.I, c.J))
candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0:0], c.DayMonth[1:3], c.Year, c.I, c.J))
} else if len(c.DayMonth) == 4 {
candidatesRoundTwo = append(candidatesRoundTwo, buildDateMatchCandidateTwo(c.DayMonth[0:2], c.DayMonth[2:4], c.Year, c.I, c.J))
}
}
for _, candidate := range candidatesRoundTwo {
intDay, err := strconv.ParseInt(candidate.Day, 10, 16)
if err != nil {
continue
}
intMonth, err := strconv.ParseInt(candidate.Month, 10, 16)
if err != nil {
continue
}
intYear, err := strconv.ParseInt(candidate.Year, 10, 16)
if err != nil {
continue
}
if ok, _, _, _ := checkDate(intDay, intMonth, intYear); ok {
matches = append(matches, match.DateMatch{Token: password, Pattern: "date", Day: intDay, Month: intMonth, Year: intYear, I: i, J: j})
}
}
}
return matches
}
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 {
return dateMatchCandidateTwo{Day: day, Month: month, Year: year, I: i, J: j}
}

View File

@ -0,0 +1,57 @@
package matching
import (
"strings"
"github.com/status-im/zxcvbn-go/entropy"
"github.com/status-im/zxcvbn-go/match"
)
func buildDictMatcher(dictName string, rankedDict map[string]int) func(password string) []match.Match {
return func(password string) []match.Match {
matches := dictionaryMatch(password, dictName, rankedDict)
for _, v := range matches {
v.DictionaryName = dictName
}
return matches
}
}
func dictionaryMatch(password string, dictionaryName string, rankedDict map[string]int) []match.Match {
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 := pwLowerRunes[i : j+1]
if val, ok := rankedDict[string(word)]; ok {
matchDic := match.Match{Pattern: "dictionary",
DictionaryName: dictionaryName,
I: i,
J: j,
Token: string([]rune(password)[i : j+1]),
}
matchDic.Entropy = entropy.DictionaryEntropy(matchDic, float64(val))
results = append(results, matchDic)
}
}
}
return results
}
func buildRankedDict(unrankedList []string) map[string]int {
result := make(map[string]int)
for i, v := range unrankedList {
result[strings.ToLower(v)] = i + 1
}
return result
}

234
vendor/github.com/status-im/zxcvbn-go/matching/leet.go generated vendored Normal file
View File

@ -0,0 +1,234 @@
package matching
import (
"strings"
"github.com/status-im/zxcvbn-go/entropy"
"github.com/status-im/zxcvbn-go/match"
)
// 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 == L33TMatcherName
}
func l33tMatch(password string) []match.Match {
permutations := getPermutations(password)
var matches []match.Match
for _, permutation := range permutations {
for _, mather := range dictionaryMatchers {
matches = append(matches, mather.MatchingFunc(permutation)...)
}
}
for _, match := range matches {
match.Entropy += entropy.ExtraLeetEntropy(match, password)
match.DictionaryName = match.DictionaryName + "_3117"
}
return matches
}
// This function creates a list of permutations based on a fixed table stored on data. The table
// will be reduced in order to proceed in the function using only relevant values (see
// relevantL33tSubtable).
func getPermutations(password string) []string {
substitutions := relevantL33tSubtable(password)
permutations := getAllPermutationsOfLeetSubstitutions(password, substitutions)
return permutations
}
// This function loads the table from data but only keep in memory the values that are present
// inside the provided password.
func relevantL33tSubtable(password string) map[string][]string {
relevantSubs := make(map[string][]string)
for key, values := range l33tTable.Graph {
for _, value := range values {
if strings.Contains(password, value) {
relevantSubs[key] = append(relevantSubs[key], value)
}
}
}
return relevantSubs
}
// This function creates the list of permutations of a given password using the provided table as
// reference for its operation.
func getAllPermutationsOfLeetSubstitutions(password string, table map[string][]string) []string {
result := []string{}
// create a list of tables without conflicting keys/values (this happens for "|", "7" and "1")
noConflictsTables := createListOfMapsWithoutConflicts(table)
for _, noConflictsTable := range noConflictsTables {
substitutionsMaps := createSubstitutionsMapsFromTable(noConflictsTable)
for _, substitutionsMap := range substitutionsMaps {
newValue := createWordForSubstitutionMap(password, substitutionsMap)
if !stringSliceContainsValue(result, newValue) {
result = append(result, newValue)
}
}
}
return result
}
// Create the possible list of maps removing the conflicts from it. As an example, the value "|"
// may represent "i" and "l". For each representation of the conflicting value, a new map is
// created. This may grow exponencialy according to the number of conflicts. The number of maps
// returned by this function may be reduced if the relevantL33tSubtable function was called to
// identify only relevant items.
func createListOfMapsWithoutConflicts(table map[string][]string) []map[string][]string {
// the resulting list starts with the provided table
result := []map[string][]string{}
result = append(result, table)
// iterate over the list of conflicts in order to expand the maps for each one
conflicts := retrieveConflictsListFromTable(table)
for _, value := range conflicts {
newMapList := []map[string][]string{}
// for each conflict a new list of maps will be created for every already known map
for _, currentMap := range result {
newMaps := createDifferentMapsForLeetChar(currentMap, value)
newMapList = append(newMapList, newMaps...)
}
result = newMapList
}
return result
}
// This function retrieves the list of values that appear for one or more keys. This is usefull to
// know which l33t chars can represent more than one letter.
func retrieveConflictsListFromTable(table map[string][]string) []string {
result := []string{}
foundValues := []string{}
for _, values := range table {
for _, value := range values {
if stringSliceContainsValue(foundValues, value) {
// only add on results if it was not identified as conflict before
if !stringSliceContainsValue(result, value) {
result = append(result, value)
}
} else {
foundValues = append(foundValues, value)
}
}
}
return result
}
// This function aims to create different maps for a given char if this char represents a conflict.
// If the specified char is not a conflit one, the same map will be returned. In scenarios which
// the provided char can not be found on map, an empty list will be returned. This function was
// designed to be used on conflicts situations.
func createDifferentMapsForLeetChar(table map[string][]string, leetChar string) []map[string][]string {
result := []map[string][]string{}
keysWithSameValue := retrieveListOfKeysWithSpecificValueFromTable(table, leetChar)
for _, key := range keysWithSameValue {
newMap := copyMapRemovingSameValueFromOtherKeys(table, key, leetChar)
result = append(result, newMap)
}
return result
}
// This function retrieves the list of keys that can be represented using the given value.
func retrieveListOfKeysWithSpecificValueFromTable(table map[string][]string, valueToFind string) []string {
result := []string{}
for key, values := range table {
for _, value := range values {
if value == valueToFind && !stringSliceContainsValue(result, key) {
result = append(result, key)
}
}
}
return result
}
// This function returns a lsit of substitution map from a given table. Each map in the result will
// provide only one representation for each value. As an example, if the provided map contains the
// values "@" and "4" in the possibilities to represent "a", two maps will be created where one
// will contain "a" mapping to "@" and the other one will provide "a" mapping to "4".
func createSubstitutionsMapsFromTable(table map[string][]string) []map[string]string {
result := []map[string]string{{"": ""}}
for key, values := range table {
newResult := []map[string]string{}
for _, mapInCurrentResult := range result {
for _, value := range values {
newMapForValue := copyMap(mapInCurrentResult)
newMapForValue[key] = value
newResult = append(newResult, newMapForValue)
}
}
result = newResult
}
// verification to make sure that the slice was filled
if len(result) == 1 && len(result[0]) == 1 && result[0][""] == "" {
return []map[string]string{}
}
return result
}
// This function replaces the values provided on substitution map over the provided word.
func createWordForSubstitutionMap(word string, substitutionMap map[string]string) string {
result := word
for key, value := range substitutionMap {
result = strings.Replace(result, value, key, -1)
}
return result
}
func stringSliceContainsValue(slice []string, value string) bool {
for _, valueInSlice := range slice {
if valueInSlice == value {
return true
}
}
return false
}
func copyMap(table map[string]string) map[string]string {
result := make(map[string]string)
for key, value := range table {
result[key] = value
}
return result
}
// This function creates a new map based on the one provided but excluding possible representations
// of the same value on other keys.
func copyMapRemovingSameValueFromOtherKeys(table map[string][]string, keyToFix string, valueToFix string) map[string][]string {
result := make(map[string][]string)
for key, values := range table {
for _, value := range values {
if !(value == valueToFix && key != keyToFix) {
result[key] = append(result[key], value)
}
}
}
return result
}

View File

@ -0,0 +1,82 @@
package matching
import (
"sort"
"github.com/status-im/zxcvbn-go/adjacency"
"github.com/status-im/zxcvbn-go/frequency"
"github.com/status-im/zxcvbn-go/match"
)
var (
dictionaryMatchers []match.Matcher
matchers []match.Matcher
adjacencyGraphs []adjacency.Graph
l33tTable adjacency.Graph
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 dictionaryMatchers == nil || adjacencyGraphs == nil {
loadFrequencyList()
}
if userInputs != nil {
userInputMatcher := buildDictMatcher("user_inputs", buildRankedDict(userInputs))
matches = userInputMatcher(password)
}
for _, matcher := range matchers {
shouldBeFiltered := false
for i := range filters {
if filters[i](matcher) {
shouldBeFiltered = true
break
}
}
if !shouldBeFiltered {
matches = append(matches, matcher.MatchingFunc(password)...)
}
}
sort.Sort(match.Matches(matches))
return matches
}
func loadFrequencyList() {
for n, list := range frequency.Lists {
dictionaryMatchers = append(dictionaryMatchers, match.Matcher{MatchingFunc: buildDictMatcher(n, buildRankedDict(list.List)), ID: n})
}
l33tTable = adjacency.GraphMap["l33t"]
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"
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})
}

View File

@ -0,0 +1,67 @@
package matching
import (
"strings"
"github.com/status-im/zxcvbn-go/entropy"
"github.com/status-im/zxcvbn-go/match"
)
const repeatMatcherName = "REPEAT"
//FilterRepeatMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher
func FilterRepeatMatcher(m match.Matcher) bool {
return m.ID == repeatMatcherName
}
func repeatMatch(password string) []match.Match {
var matches []match.Match
//Loop through password. if current == prev currentStreak++ else if currentStreak > 2 {buildMatch; currentStreak = 1} prev = current
var current, prev string
currentStreak := 1
var i int
var char rune
for i, char = range password {
current = string(char)
if i == 0 {
prev = current
continue
}
if strings.ToLower(current) == strings.ToLower(prev) {
currentStreak++
} else if currentStreak > 2 {
iPos := i - currentStreak
jPos := i - 1
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
}
prev = current
}
if currentStreak > 2 {
iPos := i - currentStreak + 1
jPos := i
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
}

View File

@ -0,0 +1,76 @@
package matching
import (
"strings"
"github.com/status-im/zxcvbn-go/entropy"
"github.com/status-im/zxcvbn-go/match"
)
const sequenceMatcherName = "SEQ"
//FilterSequenceMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher
func FilterSequenceMatcher(m match.Matcher) bool {
return m.ID == sequenceMatcherName
}
func sequenceMatch(password string) []match.Match {
var matches []match.Match
for i := 0; i < len(password); {
j := i + 1
var seq string
var seqName string
seqDirection := 0
for seqCandidateName, seqCandidate := range sequences {
iN := strings.Index(seqCandidate, string(password[i]))
var jN int
if j < len(password) {
jN = strings.Index(seqCandidate, string(password[j]))
} else {
jN = -1
}
if iN > -1 && jN > -1 {
direction := jN - iN
if direction == 1 || direction == -1 {
seq = seqCandidate
seqName = seqCandidateName
seqDirection = direction
break
}
}
}
if seq != "" {
for {
var prevN, curN int
if j < len(password) {
prevChar, curChar := password[j-1], password[j]
prevN, curN = strings.Index(seq, string(prevChar)), strings.Index(seq, string(curChar))
}
if j == len(password) || curN-prevN != seqDirection {
if j-i > 2 {
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 {
j++
}
}
}
i = j
}
return matches
}

View File

@ -0,0 +1,88 @@
package matching
import (
"strings"
"github.com/status-im/zxcvbn-go/adjacency"
"github.com/status-im/zxcvbn-go/entropy"
"github.com/status-im/zxcvbn-go/match"
)
const spatialMatcherName = "SPATIAL"
//FilterSpatialMatcher can be pass to zxcvbn-go.PasswordStrength to skip that matcher
func FilterSpatialMatcher(m match.Matcher) bool {
return m.ID == spatialMatcherName
}
func spatialMatch(password string) (matches []match.Match) {
for _, graph := range adjacencyGraphs {
if graph.Graph != nil {
matches = append(matches, spatialMatchHelper(password, graph)...)
}
}
return matches
}
func spatialMatchHelper(password string, graph adjacency.Graph) (matches []match.Match) {
for i := 0; i < len(password)-1; {
j := i + 1
lastDirection := -99 //an int that it should never be!
turns := 0
shiftedCount := 0
for {
prevChar := password[j-1]
found := false
foundDirection := -1
curDirection := -1
//My graphs seem to be wrong. . . and where the hell is qwerty
adjacents := graph.Graph[string(prevChar)]
//Consider growing pattern by one character if j hasn't gone over the edge
if j < len(password) {
curChar := password[j]
for _, adj := range adjacents {
curDirection++
if strings.Index(adj, string(curChar)) != -1 {
found = true
foundDirection = curDirection
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++
}
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++
lastDirection = foundDirection
}
break
}
}
}
//if the current pattern continued, extend j and try to grow again
if found {
j++
} else {
//otherwise push the pattern discovered so far, if any...
//don't consider length 1 or 2 chains.
if j-i > 2 {
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
break
}
}
}
return matches
}

View File

@ -0,0 +1,177 @@
package scoring
import (
"fmt"
"github.com/status-im/zxcvbn-go/entropy"
"github.com/status-im/zxcvbn-go/match"
"github.com/status-im/zxcvbn-go/utils/math"
"math"
"sort"
)
const (
//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!
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
MatchSequence []match.Match
CrackTime float64
CrackTimeDisplay string
Score int
CalcTime float64
}
/*
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.
*/
func MinimumEntropyMatchSequence(password string, matches []match.Match) MinEntropyMatch {
bruteforceCardinality := float64(entropy.CalcBruteForceCardinality(password))
upToK := make([]float64, len(password))
backPointers := make([]match.Match, len(password))
for k := 0; k < len(password); k++ {
upToK[k] = get(upToK, k-1) + math.Log2(bruteforceCardinality)
for _, match := range matches {
if match.J != k {
continue
}
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)
candidateEntropy := upTo + match.Entropy
if candidateEntropy < upToK[j] {
upToK[j] = candidateEntropy
match.Entropy = candidateEntropy
backPointers[j] = match
}
}
}
//walk backwards and decode the best sequence
var matchSequence []match.Match
passwordLen := len(password)
passwordLen--
for k := passwordLen; k >= 0; {
match := backPointers[k]
if match.Pattern != "" {
matchSequence = append(matchSequence, match)
k = match.I - 1
} else {
k--
}
}
sort.Sort(match.Matches(matchSequence))
makeBruteForceMatch := func(i, j int) match.Match {
return match.Match{Pattern: "bruteforce",
I: i,
J: j,
Token: password[i : j+1],
Entropy: math.Log2(math.Pow(bruteforceCardinality, float64(j-i)))}
}
k := 0
var matchSequenceCopy []match.Match
for _, match := range matchSequence {
i, j := match.I, match.J
if i-k > 0 {
matchSequenceCopy = append(matchSequenceCopy, makeBruteForceMatch(k, i-1))
}
k = j + 1
matchSequenceCopy = append(matchSequenceCopy, match)
}
if k < len(password) {
matchSequenceCopy = append(matchSequenceCopy, makeBruteForceMatch(k, len(password)-1))
}
var minEntropy float64
if len(password) == 0 {
minEntropy = float64(0)
} else {
minEntropy = upToK[len(password)-1]
}
crackTime := roundToXDigits(entropyToCrackTime(minEntropy), 3)
return MinEntropyMatch{Password: password,
Entropy: roundToXDigits(minEntropy, 3),
MatchSequence: matchSequenceCopy,
CrackTime: crackTime,
CrackTimeDisplay: displayTime(crackTime),
Score: crackTimeToScore(crackTime)}
}
func get(a []float64, i int) float64 {
if i < 0 || i >= len(a) {
return float64(0)
}
return a[i]
}
func entropyToCrackTime(entropy float64) float64 {
crackTime := (0.5 * math.Pow(float64(2), entropy)) * secondsPerGuess
return crackTime
}
func roundToXDigits(number float64, digits int) float64 {
return zxcvbnmath.Round(number, .5, digits)
}
func displayTime(seconds float64) string {
formater := "%.1f %s"
minute := float64(60)
hour := minute * float64(60)
day := hour * float64(24)
month := day * float64(31)
year := month * float64(12)
century := year * float64(100)
if seconds < minute {
return "instant"
} else if seconds < hour {
return fmt.Sprintf(formater, (1 + math.Ceil(seconds/minute)), "minutes")
} else if seconds < day {
return fmt.Sprintf(formater, (1 + math.Ceil(seconds/hour)), "hours")
} else if seconds < month {
return fmt.Sprintf(formater, (1 + math.Ceil(seconds/day)), "days")
} else if seconds < year {
return fmt.Sprintf(formater, (1 + math.Ceil(seconds/month)), "months")
} else if seconds < century {
return fmt.Sprintf(formater, (1 + math.Ceil(seconds/century)), "years")
} else {
return "centuries"
}
}
func crackTimeToScore(seconds float64) int {
if seconds < math.Pow(10, 2) {
return 0
} else if seconds < math.Pow(10, 4) {
return 1
} else if seconds < math.Pow(10, 6) {
return 2
} else if seconds < math.Pow(10, 8) {
return 3
}
return 4
}

View File

@ -0,0 +1,40 @@
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.
*/
func NChoseK(n, k float64) float64 {
if k > n {
return 0
} else if k == 0 {
return 1
}
var r float64 = 1
for d := float64(1); d <= k; d++ {
r *= n
r /= d
n--
}
return r
}
// Round a number
func Round(val float64, roundOn float64, places int) (newVal float64) {
var round float64
pow := math.Pow(10, float64(places))
digit := pow * val
_, div := math.Modf(digit)
if div >= roundOn {
round = math.Ceil(digit)
} else {
round = math.Floor(digit)
}
newVal = round / pow
return
}

22
vendor/github.com/status-im/zxcvbn-go/zxcvbn.go generated vendored Normal file
View File

@ -0,0 +1,22 @@
package zxcvbn
import (
"time"
"github.com/status-im/zxcvbn-go/match"
"github.com/status-im/zxcvbn-go/matching"
"github.com/status-im/zxcvbn-go/scoring"
"github.com/status-im/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...)
result := scoring.MinimumEntropyMatchSequence(password, matches)
end := time.Now()
calcTime := end.Nanosecond() - start.Nanosecond()
result.CalcTime = zxcvbnmath.Round(float64(calcTime)*time.Nanosecond.Seconds(), .5, 3)
return result
}

10
vendor/modules.txt vendored
View File

@ -494,6 +494,16 @@ github.com/status-im/rendezvous/server
github.com/status-im/status-go/extkeys
# github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501
github.com/status-im/tcp-shaker
# github.com/status-im/zxcvbn-go v0.0.0-20220311183720-5e8676676857
github.com/status-im/zxcvbn-go
github.com/status-im/zxcvbn-go/adjacency
github.com/status-im/zxcvbn-go/data
github.com/status-im/zxcvbn-go/entropy
github.com/status-im/zxcvbn-go/frequency
github.com/status-im/zxcvbn-go/match
github.com/status-im/zxcvbn-go/matching
github.com/status-im/zxcvbn-go/scoring
github.com/status-im/zxcvbn-go/utils/math
# github.com/stretchr/testify v1.7.0
github.com/stretchr/testify/assert
github.com/stretchr/testify/require