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:
parent
23b6883166
commit
7ef8bc68c8
1
go.mod
1
go.mod
|
@ -61,6 +61,7 @@ require (
|
||||||
github.com/status-im/rendezvous v1.3.4-0.20211008144244-bdf13155817d
|
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/status-go/extkeys v1.1.2
|
||||||
github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501
|
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/stretchr/testify v1.7.0
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
|
||||||
github.com/tsenart/tb v0.0.0-20181025101425-0d2499c8b6e9
|
github.com/tsenart/tb v0.0.0-20181025101425-0d2499c8b6e9
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -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/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 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/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/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-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/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/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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/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.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/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=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
signercore "github.com/ethereum/go-ethereum/signer/core"
|
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"
|
||||||
"github.com/status-im/status-go/api/multiformat"
|
"github.com/status-im/status-go/api/multiformat"
|
||||||
"github.com/status-im/status-go/eth-node/types"
|
"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.
|
// HashTypedData unmarshalls data into TypedData, validates it and hashes it.
|
||||||
//export HashTypeData
|
//export HashTypedData
|
||||||
func HashTypedData(data string) string {
|
func HashTypedData(data string) string {
|
||||||
var typed typeddata.TypedData
|
var typed typeddata.TypedData
|
||||||
err := json.Unmarshal([]byte(data), &typed)
|
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.
|
// HashTypedDataV4 unmarshalls data into TypedData, validates it and hashes it.
|
||||||
//export HashTypeDataV4
|
//export HashTypedDataV4
|
||||||
func HashTypedDataV4(data string) string {
|
func HashTypedDataV4(data string) string {
|
||||||
var typed signercore.TypedData
|
var typed signercore.TypedData
|
||||||
err := json.Unmarshal([]byte(data), &typed)
|
err := json.Unmarshal([]byte(data), &typed)
|
||||||
|
@ -742,3 +744,26 @@ func ImageServerTLSCert() string {
|
||||||
|
|
||||||
return cert
|
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), ¶ms)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
zxcvbn
|
||||||
|
debug.test
|
|
@ -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.
|
|
@ -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)
|
|
@ -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/
|
|
@ -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
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
216
vendor/github.com/status-im/zxcvbn-go/entropy/entropyCalculator.go
generated
vendored
Normal file
216
vendor/github.com/status-im/zxcvbn-go/entropy/entropyCalculator.go
generated
vendored
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -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
|
||||||
|
}
|
|
@ -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}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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})
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -494,6 +494,16 @@ github.com/status-im/rendezvous/server
|
||||||
github.com/status-im/status-go/extkeys
|
github.com/status-im/status-go/extkeys
|
||||||
# github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501
|
# github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501
|
||||||
github.com/status-im/tcp-shaker
|
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 v1.7.0
|
||||||
github.com/stretchr/testify/assert
|
github.com/stretchr/testify/assert
|
||||||
github.com/stretchr/testify/require
|
github.com/stretchr/testify/require
|
||||||
|
|
Loading…
Reference in New Issue