Merge pull request #114 from GoASTScanner/feature

Consider entropy when warning on hardcoded credentials
This commit is contained in:
Grant Murphy 2017-01-14 14:46:19 -08:00 committed by GitHub
commit f6aeaa8dec
32 changed files with 8604 additions and 15 deletions

View file

@ -1,6 +1,6 @@
language: go
before_script:
- go vet ./...
- go vet $(go list ./... | grep -v /vendor/)
go:
- 1.5
- tip

View file

@ -19,11 +19,34 @@ import (
"go/ast"
"go/token"
"regexp"
"github.com/nbutton23/zxcvbn-go"
"strconv"
)
type Credentials struct {
gas.MetaData
pattern *regexp.Regexp
entropyThreshold float64
perCharThreshold float64
truncate int
ignoreEntropy bool
}
func truncate(s string, n int) string {
if n > len(s) {
return s
}
return s[:n]
}
func (r *Credentials) isHighEntropyString(str string) bool {
s := truncate(str, r.truncate)
info := zxcvbn.PasswordStrength(s, []string{})
entropyPerChar := info.Entropy / float64(len(s))
return (info.Entropy >= r.entropyThreshold ||
(info.Entropy >= (r.entropyThreshold/2) &&
entropyPerChar >= r.perCharThreshold))
}
func (r *Credentials) Match(n ast.Node, ctx *gas.Context) (*gas.Issue, error) {
@ -41,13 +64,15 @@ func (r *Credentials) matchAssign(assign *ast.AssignStmt, ctx *gas.Context) (*ga
if ident, ok := i.(*ast.Ident); ok {
if r.pattern.MatchString(ident.Name) {
for _, e := range assign.Rhs {
if rhs, ok := e.(*ast.BasicLit); ok && rhs.Kind == token.STRING {
if val, err := gas.GetString(e); err == nil {
if r.ignoreEntropy || (!r.ignoreEntropy && r.isHighEntropyString(val)) {
return gas.NewIssue(ctx, assign, r.What, r.Severity, r.Confidence), nil
}
}
}
}
}
}
return nil, nil
}
@ -63,23 +88,57 @@ func (r *Credentials) matchGenDecl(decl *ast.GenDecl, ctx *gas.Context) (*gas.Is
if len(valueSpec.Values) <= index {
index = len(valueSpec.Values) - 1
}
if rhs, ok := valueSpec.Values[index].(*ast.BasicLit); ok && rhs.Kind == token.STRING {
if val, err := gas.GetString(valueSpec.Values[index]); err == nil {
if r.ignoreEntropy || (!r.ignoreEntropy && r.isHighEntropyString(val)) {
return gas.NewIssue(ctx, valueSpec, r.What, r.Severity, r.Confidence), nil
}
}
}
}
}
}
return nil, nil
}
func NewHardcodedCredentials(conf map[string]interface{}) (gas.Rule, []ast.Node) {
pattern := `(?i)passwd|pass|password|pwd|secret|token`
entropyThreshold := 80.0
perCharThreshold := 3.0
ignoreEntropy := false
var truncateString int = 16
if val, ok := conf["G101"]; ok {
pattern = val.(string)
conf := val.(map[string]string)
if configPattern, ok := conf["pattern"]; ok {
pattern = configPattern
}
if configIgnoreEntropy, ok := conf["ignore_entropy"]; ok {
if parsedBool, err := strconv.ParseBool(configIgnoreEntropy); err == nil {
ignoreEntropy = parsedBool
}
}
if configEntropyThreshold, ok := conf["entropy_threshold"]; ok {
if parsedNum, err := strconv.ParseFloat(configEntropyThreshold, 64); err == nil {
entropyThreshold = parsedNum
}
}
if configCharThreshold, ok := conf["per_char_threshold"]; ok {
if parsedNum, err := strconv.ParseFloat(configCharThreshold, 64); err == nil {
perCharThreshold = parsedNum
}
}
if configTruncate, ok := conf["truncate"]; ok {
if parsedInt, err := strconv.Atoi(configTruncate); err == nil {
truncateString = parsedInt
}
}
}
return &Credentials{
pattern: regexp.MustCompile(pattern),
entropyThreshold: entropyThreshold,
perCharThreshold: perCharThreshold,
ignoreEntropy: ignoreEntropy,
truncate: truncateString,
MetaData: gas.MetaData{
What: "Potential hardcoded credentials",
Confidence: gas.Low,

View file

@ -25,6 +25,51 @@ func TestHardcoded(t *testing.T) {
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
import "fmt"
func main() {
username := "admin"
password := "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
func TestHardcodedWithEntropy(t *testing.T) {
config := map[string]interface{}{"ignoreNosec": false}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
import "fmt"
func main() {
username := "admin"
password := "secret"
fmt.Println("Doing something with: ", username, password)
}`, analyzer)
checkTestResults(t, issues, 0, "Potential hardcoded credentials")
}
func TestHardcodedIgnoreEntropy(t *testing.T) {
config := map[string]interface{}{
"ignoreNosec": false,
"G101": map[string]string{
"ignore_entropy": "true",
},
}
analyzer := gas.NewAnalyzer(config, nil)
analyzer.AddRule(NewHardcodedCredentials(config))
issues := gasTestRunner(
`
package samples
@ -50,7 +95,7 @@ func TestHardcodedGlobalVar(t *testing.T) {
import "fmt"
var password = "admin"
var password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() {
username := "admin"
@ -70,7 +115,7 @@ func TestHardcodedConstant(t *testing.T) {
import "fmt"
const password = "secret"
const password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
func main() {
username := "admin"
@ -92,7 +137,7 @@ func TestHardcodedConstantMulti(t *testing.T) {
const (
username = "user"
password = "secret"
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
)
func main() {
@ -110,7 +155,7 @@ func TestHardecodedVarsNotAssigned(t *testing.T) {
package main
var password string
func init() {
password = "this is a secret string"
password = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
}`, analyzer)
checkTestResults(t, issues, 1, "Potential hardcoded credentials")
}
@ -140,7 +185,7 @@ func TestHardcodedConstString(t *testing.T) {
package main
const (
ATNStateTokenStart = "foo bar"
ATNStateTokenStart = "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
)
func main() {
println(ATNStateTokenStart)

View file

@ -1,2 +1,7 @@
# Import path | revision | Repository(optional)
github.com/ryanuber/go-glob 572520ed46dbddaed19ea3d9541bdd0494163693
# package
github.com/GoAstScanner/gas
# import
github.com/GoASTScanner/gas cc52ef5
github.com/nbutton23/zxcvbn-go a22cb81
github.com/ryanuber/go-glob v0.1

2
vendor/github.com/nbutton23/zxcvbn-go/.gitignore generated vendored Normal file
View file

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

20
vendor/github.com/nbutton23/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.

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

@ -0,0 +1,78 @@
This is a goLang port of python-zxcvbn and 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 http://tech.dropbox.com/?p=165 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:
http://tech.dropbox.com/?p=165
------------------------------------------------------------------------
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:
http://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:
http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists
Last but not least, big thanks to xkcd :)
https://xkcd.com/936/

View file

@ -0,0 +1,96 @@
package adjacency
import (
"encoding/json"
"log"
// "fmt"
"github.com/nbutton23/zxcvbn-go/data"
)
type AdjacencyGraph struct {
Graph map[string][]string
averageDegree float64
Name string
}
var AdjacencyGph = make(map[string]AdjacencyGraph)
func init() {
AdjacencyGph["qwerty"] = BuildQwerty()
AdjacencyGph["dvorak"] = BuildDvorak()
AdjacencyGph["keypad"] = BuildKeypad()
AdjacencyGph["macKeypad"] = BuildMacKeypad()
AdjacencyGph["l33t"] = BuildLeet()
}
func BuildQwerty() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/Qwerty.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "qwerty")
}
func BuildDvorak() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/Dvorak.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "dvorak")
}
func BuildKeypad() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/Keypad.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "keypad")
}
func BuildMacKeypad() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/MacKeypad.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "mac_keypad")
}
func BuildLeet() AdjacencyGraph {
data, err := zxcvbn_data.Asset("data/L33t.json")
if err != nil {
panic("Can't find asset")
}
return GetAdjancencyGraphFromFile(data, "keypad")
}
func GetAdjancencyGraphFromFile(data []byte, name string) AdjacencyGraph {
var graph AdjacencyGraph
err := json.Unmarshal(data, &graph)
if err != nil {
log.Fatal(err)
}
graph.Name = name
return 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 AdjacencyGraph) 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 char != "" || char != " " {
avg += float64(len(char))
count++
}
}
}
adjGrp.averageDegree = avg / count
return adjGrp.averageDegree
}

444
vendor/github.com/nbutton23/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,756 @@
{
"Graph": {
"0": [
"9(",
null,
null,
"[{",
"lL",
"rR"
],
"1": [
"`~",
null,
null,
"2@",
"'\"",
null
],
"2": [
"1!",
null,
null,
"3#",
",<",
"'\""
],
"3": [
"2@",
null,
null,
"4$",
".>",
",<"
],
"4": [
"3#",
null,
null,
"5%",
"pP",
".>"
],
"5": [
"4$",
null,
null,
"6^",
"yY",
"pP"
],
"6": [
"5%",
null,
null,
"7&",
"fF",
"yY"
],
"7": [
"6^",
null,
null,
"8*",
"gG",
"fF"
],
"8": [
"7&",
null,
null,
"9(",
"cC",
"gG"
],
"9": [
"8*",
null,
null,
"0)",
"rR",
"cC"
],
"!": [
"`~",
null,
null,
"2@",
"'\"",
null
],
"\"": [
null,
"1!",
"2@",
",<",
"aA",
null
],
"#": [
"2@",
null,
null,
"4$",
".>",
",<"
],
"$": [
"3#",
null,
null,
"5%",
"pP",
".>"
],
"%": [
"4$",
null,
null,
"6^",
"yY",
"pP"
],
"&": [
"6^",
null,
null,
"8*",
"gG",
"fF"
],
"'": [
null,
"1!",
"2@",
",<",
"aA",
null
],
"(": [
"8*",
null,
null,
"0)",
"rR",
"cC"
],
")": [
"9(",
null,
null,
"[{",
"lL",
"rR"
],
"*": [
"7&",
null,
null,
"9(",
"cC",
"gG"
],
"+": [
"/?",
"]}",
null,
"\\|",
null,
"-_"
],
",": [
"'\"",
"2@",
"3#",
".>",
"oO",
"aA"
],
"-": [
"sS",
"/?",
"=+",
null,
null,
"zZ"
],
".": [
",<",
"3#",
"4$",
"pP",
"eE",
"oO"
],
"/": [
"lL",
"[{",
"]}",
"=+",
"-_",
"sS"
],
":": [
null,
"aA",
"oO",
"qQ",
null,
null
],
";": [
null,
"aA",
"oO",
"qQ",
null,
null
],
"<": [
"'\"",
"2@",
"3#",
".>",
"oO",
"aA"
],
"=": [
"/?",
"]}",
null,
"\\|",
null,
"-_"
],
">": [
",<",
"3#",
"4$",
"pP",
"eE",
"oO"
],
"?": [
"lL",
"[{",
"]}",
"=+",
"-_",
"sS"
],
"@": [
"1!",
null,
null,
"3#",
",<",
"'\""
],
"A": [
null,
"'\"",
",<",
"oO",
";:",
null
],
"B": [
"xX",
"dD",
"hH",
"mM",
null,
null
],
"C": [
"gG",
"8*",
"9(",
"rR",
"tT",
"hH"
],
"D": [
"iI",
"fF",
"gG",
"hH",
"bB",
"xX"
],
"E": [
"oO",
".>",
"pP",
"uU",
"jJ",
"qQ"
],
"F": [
"yY",
"6^",
"7&",
"gG",
"dD",
"iI"
],
"G": [
"fF",
"7&",
"8*",
"cC",
"hH",
"dD"
],
"H": [
"dD",
"gG",
"cC",
"tT",
"mM",
"bB"
],
"I": [
"uU",
"yY",
"fF",
"dD",
"xX",
"kK"
],
"J": [
"qQ",
"eE",
"uU",
"kK",
null,
null
],
"K": [
"jJ",
"uU",
"iI",
"xX",
null,
null
],
"L": [
"rR",
"0)",
"[{",
"/?",
"sS",
"nN"
],
"M": [
"bB",
"hH",
"tT",
"wW",
null,
null
],
"N": [
"tT",
"rR",
"lL",
"sS",
"vV",
"wW"
],
"O": [
"aA",
",<",
".>",
"eE",
"qQ",
";:"
],
"P": [
".>",
"4$",
"5%",
"yY",
"uU",
"eE"
],
"Q": [
";:",
"oO",
"eE",
"jJ",
null,
null
],
"R": [
"cC",
"9(",
"0)",
"lL",
"nN",
"tT"
],
"S": [
"nN",
"lL",
"/?",
"-_",
"zZ",
"vV"
],
"T": [
"hH",
"cC",
"rR",
"nN",
"wW",
"mM"
],
"U": [
"eE",
"pP",
"yY",
"iI",
"kK",
"jJ"
],
"V": [
"wW",
"nN",
"sS",
"zZ",
null,
null
],
"W": [
"mM",
"tT",
"nN",
"vV",
null,
null
],
"X": [
"kK",
"iI",
"dD",
"bB",
null,
null
],
"Y": [
"pP",
"5%",
"6^",
"fF",
"iI",
"uU"
],
"Z": [
"vV",
"sS",
"-_",
null,
null,
null
],
"[": [
"0)",
null,
null,
"]}",
"/?",
"lL"
],
"\\": [
"=+",
null,
null,
null,
null,
null
],
"]": [
"[{",
null,
null,
null,
"=+",
"/?"
],
"^": [
"5%",
null,
null,
"7&",
"fF",
"yY"
],
"_": [
"sS",
"/?",
"=+",
null,
null,
"zZ"
],
"`": [
null,
null,
null,
"1!",
null,
null
],
"a": [
null,
"'\"",
",<",
"oO",
";:",
null
],
"b": [
"xX",
"dD",
"hH",
"mM",
null,
null
],
"c": [
"gG",
"8*",
"9(",
"rR",
"tT",
"hH"
],
"d": [
"iI",
"fF",
"gG",
"hH",
"bB",
"xX"
],
"e": [
"oO",
".>",
"pP",
"uU",
"jJ",
"qQ"
],
"f": [
"yY",
"6^",
"7&",
"gG",
"dD",
"iI"
],
"g": [
"fF",
"7&",
"8*",
"cC",
"hH",
"dD"
],
"h": [
"dD",
"gG",
"cC",
"tT",
"mM",
"bB"
],
"i": [
"uU",
"yY",
"fF",
"dD",
"xX",
"kK"
],
"j": [
"qQ",
"eE",
"uU",
"kK",
null,
null
],
"k": [
"jJ",
"uU",
"iI",
"xX",
null,
null
],
"l": [
"rR",
"0)",
"[{",
"/?",
"sS",
"nN"
],
"m": [
"bB",
"hH",
"tT",
"wW",
null,
null
],
"n": [
"tT",
"rR",
"lL",
"sS",
"vV",
"wW"
],
"o": [
"aA",
",<",
".>",
"eE",
"qQ",
";:"
],
"p": [
".>",
"4$",
"5%",
"yY",
"uU",
"eE"
],
"q": [
";:",
"oO",
"eE",
"jJ",
null,
null
],
"r": [
"cC",
"9(",
"0)",
"lL",
"nN",
"tT"
],
"s": [
"nN",
"lL",
"/?",
"-_",
"zZ",
"vV"
],
"t": [
"hH",
"cC",
"rR",
"nN",
"wW",
"mM"
],
"u": [
"eE",
"pP",
"yY",
"iI",
"kK",
"jJ"
],
"v": [
"wW",
"nN",
"sS",
"zZ",
null,
null
],
"w": [
"mM",
"tT",
"nN",
"vV",
null,
null
],
"x": [
"kK",
"iI",
"dD",
"bB",
null,
null
],
"y": [
"pP",
"5%",
"6^",
"fF",
"iI",
"uU"
],
"z": [
"vV",
"sS",
"-_",
null,
null,
null
],
"{": [
"0)",
null,
null,
"]}",
"/?",
"lL"
],
"|": [
"=+",
null,
null,
null,
null,
null
],
"}": [
"[{",
null,
null,
null,
"=+",
"/?"
],
"~": [
null,
null,
null,
"1!",
null,
null
]
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,154 @@
{
"Graph": {
"0": [
null,
"1",
"2",
"3",
".",
null,
null,
null
],
"1": [
null,
null,
"4",
"5",
"2",
"0",
null,
null
],
"2": [
"1",
"4",
"5",
"6",
"3",
".",
"0",
null
],
"3": [
"2",
"5",
"6",
null,
null,
null,
".",
"0"
],
"4": [
null,
null,
"7",
"8",
"5",
"2",
"1",
null
],
"5": [
"4",
"7",
"8",
"9",
"6",
"3",
"2",
"1"
],
"6": [
"5",
"8",
"9",
"+",
null,
null,
"3",
"2"
],
"7": [
null,
null,
null,
"/",
"8",
"5",
"4",
null
],
"8": [
"7",
null,
"/",
"*",
"9",
"6",
"5",
"4"
],
"9": [
"8",
"/",
"*",
"-",
"+",
null,
"6",
"5"
],
"*": [
"/",
null,
null,
null,
"-",
"+",
"9",
"8"
],
"+": [
"9",
"*",
"-",
null,
null,
null,
null,
"6"
],
"-": [
"*",
null,
null,
null,
null,
null,
"+",
"9"
],
".": [
"0",
"2",
"3",
null,
null,
null,
null,
null
],
"/": [
null,
null,
null,
null,
"*",
"9",
"8",
"7"
]
}
}

View file

@ -0,0 +1,51 @@
{
"graph": {
"a": [
"4",
"@"
],
"b": [
"8"
],
"c": [
"(",
"{",
"[",
"<"
],
"e": [
"3"
],
"g": [
"6",
"9"
],
"i": [
"1",
"!",
"|"
],
"l": [
"1",
"|",
"7"
],
"o": [
"0"
],
"s": [
"$",
"5"
],
"t": [
"+",
"7"
],
"x": [
"%"
],
"z": [
"2"
]
}
}

View file

@ -0,0 +1,164 @@
{
"Graph": {
"0": [
null,
"1",
"2",
"3",
".",
null,
null,
null
],
"1": [
null,
null,
"4",
"5",
"2",
"0",
null,
null
],
"2": [
"1",
"4",
"5",
"6",
"3",
".",
"0",
null
],
"3": [
"2",
"5",
"6",
"+",
null,
null,
".",
"0"
],
"4": [
null,
null,
"7",
"8",
"5",
"2",
"1",
null
],
"5": [
"4",
"7",
"8",
"9",
"6",
"3",
"2",
"1"
],
"6": [
"5",
"8",
"9",
"-",
"+",
null,
"3",
"2"
],
"7": [
null,
null,
null,
"=",
"8",
"5",
"4",
null
],
"8": [
"7",
null,
"=",
"/",
"9",
"6",
"5",
"4"
],
"9": [
"8",
"=",
"/",
"*",
"-",
"+",
"6",
"5"
],
"*": [
"/",
null,
null,
null,
null,
null,
"-",
"9"
],
"+": [
"6",
"9",
"-",
null,
null,
null,
null,
"3"
],
"-": [
"9",
"/",
"*",
null,
null,
null,
"+",
"6"
],
".": [
"0",
"2",
"3",
null,
null,
null,
null,
null
],
"/": [
"=",
null,
null,
null,
"*",
"-",
"9",
"8"
],
"=": [
null,
null,
null,
null,
"/",
"9",
"8",
"7"
]
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,756 @@
{
"Graph": {
"!": [
"`~",
null,
null,
"2@",
"qQ",
null
],
"\"": [
";:",
"[{",
"]}",
null,
null,
"/?"
],
"#": [
"2@",
null,
null,
"4$",
"eE",
"wW"
],
"$": [
"3#",
null,
null,
"5%",
"rR",
"eE"
],
"%": [
"4$",
null,
null,
"6^",
"tT",
"rR"
],
"&": [
"6^",
null,
null,
"8*",
"uU",
"yY"
],
"'": [
";:",
"[{",
"]}",
null,
null,
"/?"
],
"(": [
"8*",
null,
null,
"0)",
"oO",
"iI"
],
")": [
"9(",
null,
null,
"-_",
"pP",
"oO"
],
"*": [
"7&",
null,
null,
"9(",
"iI",
"uU"
],
"+": [
"-_",
null,
null,
null,
"]}",
"[{"
],
",": [
"mM",
"kK",
"lL",
".>",
null,
null
],
"-": [
"0)",
null,
null,
"=+",
"[{",
"pP"
],
".": [
",<",
"lL",
";:",
"/?",
null,
null
],
"/": [
".>",
";:",
"'\"",
null,
null,
null
],
"0": [
"9(",
null,
null,
"-_",
"pP",
"oO"
],
"1": [
"`~",
null,
null,
"2@",
"qQ",
null
],
"2": [
"1!",
null,
null,
"3#",
"wW",
"qQ"
],
"3": [
"2@",
null,
null,
"4$",
"eE",
"wW"
],
"4": [
"3#",
null,
null,
"5%",
"rR",
"eE"
],
"5": [
"4$",
null,
null,
"6^",
"tT",
"rR"
],
"6": [
"5%",
null,
null,
"7&",
"yY",
"tT"
],
"7": [
"6^",
null,
null,
"8*",
"uU",
"yY"
],
"8": [
"7&",
null,
null,
"9(",
"iI",
"uU"
],
"9": [
"8*",
null,
null,
"0)",
"oO",
"iI"
],
":": [
"lL",
"pP",
"[{",
"'\"",
"/?",
".>"
],
";": [
"lL",
"pP",
"[{",
"'\"",
"/?",
".>"
],
"<": [
"mM",
"kK",
"lL",
".>",
null,
null
],
"=": [
"-_",
null,
null,
null,
"]}",
"[{"
],
">": [
",<",
"lL",
";:",
"/?",
null,
null
],
"?": [
".>",
";:",
"'\"",
null,
null,
null
],
"@": [
"1!",
null,
null,
"3#",
"wW",
"qQ"
],
"A": [
null,
"qQ",
"wW",
"sS",
"zZ",
null
],
"B": [
"vV",
"gG",
"hH",
"nN",
null,
null
],
"C": [
"xX",
"dD",
"fF",
"vV",
null,
null
],
"D": [
"sS",
"eE",
"rR",
"fF",
"cC",
"xX"
],
"E": [
"wW",
"3#",
"4$",
"rR",
"dD",
"sS"
],
"F": [
"dD",
"rR",
"tT",
"gG",
"vV",
"cC"
],
"G": [
"fF",
"tT",
"yY",
"hH",
"bB",
"vV"
],
"H": [
"gG",
"yY",
"uU",
"jJ",
"nN",
"bB"
],
"I": [
"uU",
"8*",
"9(",
"oO",
"kK",
"jJ"
],
"J": [
"hH",
"uU",
"iI",
"kK",
"mM",
"nN"
],
"K": [
"jJ",
"iI",
"oO",
"lL",
",<",
"mM"
],
"L": [
"kK",
"oO",
"pP",
";:",
".>",
",<"
],
"M": [
"nN",
"jJ",
"kK",
",<",
null,
null
],
"N": [
"bB",
"hH",
"jJ",
"mM",
null,
null
],
"O": [
"iI",
"9(",
"0)",
"pP",
"lL",
"kK"
],
"P": [
"oO",
"0)",
"-_",
"[{",
";:",
"lL"
],
"Q": [
null,
"1!",
"2@",
"wW",
"aA",
null
],
"R": [
"eE",
"4$",
"5%",
"tT",
"fF",
"dD"
],
"S": [
"aA",
"wW",
"eE",
"dD",
"xX",
"zZ"
],
"T": [
"rR",
"5%",
"6^",
"yY",
"gG",
"fF"
],
"U": [
"yY",
"7&",
"8*",
"iI",
"jJ",
"hH"
],
"V": [
"cC",
"fF",
"gG",
"bB",
null,
null
],
"W": [
"qQ",
"2@",
"3#",
"eE",
"sS",
"aA"
],
"X": [
"zZ",
"sS",
"dD",
"cC",
null,
null
],
"Y": [
"tT",
"6^",
"7&",
"uU",
"hH",
"gG"
],
"Z": [
null,
"aA",
"sS",
"xX",
null,
null
],
"[": [
"pP",
"-_",
"=+",
"]}",
"'\"",
";:"
],
"\\": [
"]}",
null,
null,
null,
null,
null
],
"]": [
"[{",
"=+",
null,
"\\|",
null,
"'\""
],
"^": [
"5%",
null,
null,
"7&",
"yY",
"tT"
],
"_": [
"0)",
null,
null,
"=+",
"[{",
"pP"
],
"`": [
null,
null,
null,
"1!",
null,
null
],
"a": [
null,
"qQ",
"wW",
"sS",
"zZ",
null
],
"b": [
"vV",
"gG",
"hH",
"nN",
null,
null
],
"c": [
"xX",
"dD",
"fF",
"vV",
null,
null
],
"d": [
"sS",
"eE",
"rR",
"fF",
"cC",
"xX"
],
"e": [
"wW",
"3#",
"4$",
"rR",
"dD",
"sS"
],
"f": [
"dD",
"rR",
"tT",
"gG",
"vV",
"cC"
],
"g": [
"fF",
"tT",
"yY",
"hH",
"bB",
"vV"
],
"h": [
"gG",
"yY",
"uU",
"jJ",
"nN",
"bB"
],
"i": [
"uU",
"8*",
"9(",
"oO",
"kK",
"jJ"
],
"j": [
"hH",
"uU",
"iI",
"kK",
"mM",
"nN"
],
"k": [
"jJ",
"iI",
"oO",
"lL",
",<",
"mM"
],
"l": [
"kK",
"oO",
"pP",
";:",
".>",
",<"
],
"m": [
"nN",
"jJ",
"kK",
",<",
null,
null
],
"n": [
"bB",
"hH",
"jJ",
"mM",
null,
null
],
"o": [
"iI",
"9(",
"0)",
"pP",
"lL",
"kK"
],
"p": [
"oO",
"0)",
"-_",
"[{",
";:",
"lL"
],
"q": [
null,
"1!",
"2@",
"wW",
"aA",
null
],
"r": [
"eE",
"4$",
"5%",
"tT",
"fF",
"dD"
],
"s": [
"aA",
"wW",
"eE",
"dD",
"xX",
"zZ"
],
"t": [
"rR",
"5%",
"6^",
"yY",
"gG",
"fF"
],
"u": [
"yY",
"7&",
"8*",
"iI",
"jJ",
"hH"
],
"v": [
"cC",
"fF",
"gG",
"bB",
null,
null
],
"w": [
"qQ",
"2@",
"3#",
"eE",
"sS",
"aA"
],
"x": [
"zZ",
"sS",
"dD",
"cC",
null,
null
],
"y": [
"tT",
"6^",
"7&",
"uU",
"hH",
"gG"
],
"z": [
null,
"aA",
"sS",
"xX",
null,
null
],
"{": [
"pP",
"-_",
"=+",
"]}",
"'\"",
";:"
],
"|": [
"]}",
null,
null,
null,
null,
null
],
"}": [
"[{",
"=+",
null,
"\\|",
null,
"'\""
],
"~": [
null,
null,
null,
"1!",
null,
null
]
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,215 @@
package entropy
import (
"github.com/nbutton23/zxcvbn-go/adjacency"
"github.com/nbutton23/zxcvbn-go/match"
"github.com/nbutton23/zxcvbn-go/utils/math"
"math"
"regexp"
"unicode"
)
const (
START_UPPER string = `^[A-Z][^A-Z]+$`
END_UPPER string = `^[^A-Z]+[A-Z]$'`
ALL_UPPER string = `^[A-Z]+$`
NUM_YEARS = float64(119) // years match against 1900 - 2019
NUM_MONTHS = float64(12)
NUM_DAYS = float64(31)
)
var (
KEYPAD_STARTING_POSITIONS = len(adjacency.AdjacencyGph["keypad"].Graph)
KEYPAD_AVG_DEGREE = adjacency.AdjacencyGph["keypad"].CalculateAvgDegree()
)
func DictionaryEntropy(match match.Match, rank float64) float64 {
baseEntropy := math.Log2(rank)
upperCaseEntropy := extraUpperCaseEntropy(match)
//TODO: L33t
return baseEntropy + upperCaseEntropy
}
func extraUpperCaseEntropy(match match.Match) float64 {
word := match.Token
allLower := true
for _, char := range word {
if unicode.IsUpper(char) {
allLower = false
break
}
}
if allLower {
return float64(0)
}
//a capitalized word is the most common capitalization scheme,
//so it only doubles the search space (uncapitalized + capitalized): 1 extra bit of entropy.
//allcaps and end-capitalized are common enough too, underestimate as 1 extra bit to be safe.
for _, regex := range []string{START_UPPER, END_UPPER, ALL_UPPER} {
matcher := regexp.MustCompile(regex)
if matcher.MatchString(word) {
return float64(1)
}
}
//Otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters with U uppercase letters or
//less. Or, if there's more uppercase than lower (for e.g. PASSwORD), the number of ways to lowercase U+L letters
//with L lowercase letters or less.
countUpper, countLower := float64(0), float64(0)
for _, char := range word {
if unicode.IsUpper(char) {
countUpper++
} else if unicode.IsLower(char) {
countLower++
}
}
totalLenght := countLower + countUpper
var possibililities float64
for i := float64(0); i <= math.Min(countUpper, countLower); i++ {
possibililities += float64(zxcvbn_math.NChoseK(totalLenght, i))
}
if possibililities < 1 {
return float64(1)
}
return float64(math.Log2(possibililities))
}
func SpatialEntropy(match match.Match, turns int, shiftCount int) float64 {
var s, d float64
if match.DictionaryName == "qwerty" || match.DictionaryName == "dvorak" {
//todo: verify qwerty and dvorak have the same length and degree
s = float64(len(adjacency.BuildQwerty().Graph))
d = adjacency.BuildQwerty().CalculateAvgDegree()
} else {
s = float64(KEYPAD_STARTING_POSITIONS)
d = KEYPAD_AVG_DEGREE
}
possibilities := float64(0)
length := float64(len(match.Token))
//TODO: Should this be <= or just < ?
//Estimate the number of possible patterns w/ length L or less with t turns or less
for i := float64(2); i <= length+1; i++ {
possibleTurns := math.Min(float64(turns), i-1)
for j := float64(1); j <= possibleTurns+1; j++ {
x := zxcvbn_math.NChoseK(i-1, j-1) * s * math.Pow(d, j)
possibilities += x
}
}
entropy := math.Log2(possibilities)
//add extra entropu for shifted keys. ( % instead of 5 A instead of a)
//Math is similar to extra entropy for uppercase letters in dictionary matches.
if S := float64(shiftCount); S > float64(0) {
possibilities = float64(0)
U := length - S
for i := float64(0); i < math.Min(S, U)+1; i++ {
possibilities += zxcvbn_math.NChoseK(S+U, i)
}
entropy += math.Log2(possibilities)
}
return entropy
}
func RepeatEntropy(match match.Match) float64 {
cardinality := CalcBruteForceCardinality(match.Token)
entropy := math.Log2(cardinality * float64(len(match.Token)))
return entropy
}
//TODO: Validate against python
func CalcBruteForceCardinality(password string) float64 {
lower, upper, digits, symbols := float64(0), float64(0), float64(0), float64(0)
for _, char := range password {
if unicode.IsLower(char) {
lower = float64(26)
} else if unicode.IsDigit(char) {
digits = float64(10)
} else if unicode.IsUpper(char) {
upper = float64(26)
} else {
symbols = float64(33)
}
}
cardinality := lower + upper + digits + symbols
return cardinality
}
func SequenceEntropy(match match.Match, dictionaryLength int, ascending bool) float64 {
firstChar := match.Token[0]
baseEntropy := float64(0)
if string(firstChar) == "a" || string(firstChar) == "1" {
baseEntropy = float64(0)
} else {
baseEntropy = math.Log2(float64(dictionaryLength))
//TODO: should this be just the first or any char?
if unicode.IsUpper(rune(firstChar)) {
baseEntropy++
}
}
if !ascending {
baseEntropy++
}
return baseEntropy + math.Log2(float64(len(match.Token)))
}
func ExtraLeetEntropy(match match.Match, password string) float64 {
var subsitutions float64
var unsub float64
subPassword := password[match.I:match.J]
for index, char := range subPassword {
if string(char) != string(match.Token[index]) {
subsitutions++
} else {
//TODO: Make this only true for 1337 chars that are not subs?
unsub++
}
}
var possibilities float64
for i := float64(0); i <= math.Min(subsitutions, unsub)+1; i++ {
possibilities += zxcvbn_math.NChoseK(subsitutions+unsub, i)
}
if possibilities <= 1 {
return float64(1)
}
return math.Log2(possibilities)
}
func YearEntropy(dateMatch match.DateMatch) float64 {
return math.Log2(NUM_YEARS)
}
func DateEntropy(dateMatch match.DateMatch) float64 {
var entropy float64
if dateMatch.Year < 100 {
entropy = math.Log2(NUM_DAYS * NUM_MONTHS * 100)
} else {
entropy = math.Log2(NUM_DAYS * NUM_MONTHS * NUM_YEARS)
}
if dateMatch.Separator != "" {
entropy += 2 //add two bits for separator selection [/,-,.,etc]
}
return entropy
}

View file

@ -0,0 +1,47 @@
package frequency
import (
"encoding/json"
"github.com/nbutton23/zxcvbn-go/data"
"log"
)
type FrequencyList struct {
Name string
List []string
}
var FrequencyLists = make(map[string]FrequencyList)
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")
FrequencyLists["MaleNames"] = GetStringListFromAsset(maleFilePath, "MaleNames")
FrequencyLists["FemaleNames"] = GetStringListFromAsset(femaleFilePath, "FemaleNames")
FrequencyLists["Surname"] = GetStringListFromAsset(surnameFilePath, "Surname")
FrequencyLists["English"] = GetStringListFromAsset(englishFilePath, "English")
FrequencyLists["Passwords"] = GetStringListFromAsset(passwordsFilePath, "Passwords")
}
func getAsset(name string) []byte {
data, err := zxcvbn_data.Asset(name)
if err != nil {
panic("Error getting asset " + name)
}
return data
}
func GetStringListFromAsset(data []byte, name string) FrequencyList {
var tempList FrequencyList
err := json.Unmarshal(data, &tempList)
if err != nil {
log.Fatal(err)
}
tempList.Name = name
return tempList
}

35
vendor/github.com/nbutton23/zxcvbn-go/match/match.go generated vendored Normal file
View file

@ -0,0 +1,35 @@
package match
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
}
}
type Match struct {
Pattern string
I, J int
Token string
DictionaryName string
Entropy float64
}
type DateMatch struct {
Pattern string
I, J int
Token string
Separator string
Day, Month, Year int64
}

View file

@ -0,0 +1,189 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"regexp"
"strconv"
"strings"
)
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
matcher := regexp.MustCompile(DATE_RX_YEAR_SUFFIX)
for _, v := range matcher.FindAllString(password, len(password)) {
splitV := matcher.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)
}
matcher = regexp.MustCompile(DATE_RX_YEAR_PREFIX)
for _, v := range matcher.FindAllString(password, len(password)) {
splitV := matcher.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) {
matcher := regexp.MustCompile(DATE_WITHOUT_SEP_MATCH)
for _, v := range matcher.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,54 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
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 {
length := len(password)
var results []match.Match
pwLower := strings.ToLower(password)
for i := 0; i < length; i++ {
for j := i; j < length; j++ {
word := pwLower[i : j+1]
if val, ok := rankedDict[word]; ok {
matchDic := match.Match{Pattern: "dictionary",
DictionaryName: dictionaryName,
I: i,
J: j,
Token: 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
}

68
vendor/github.com/nbutton23/zxcvbn-go/matching/leet.go generated vendored Normal file
View file

@ -0,0 +1,68 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
func l33tMatch(password string) []match.Match {
substitutions := relevantL33tSubtable(password)
permutations := getAllPermutationsOfLeetSubstitutions(password, substitutions)
var matches []match.Match
for _, permutation := range permutations {
for _, mather := range DICTIONARY_MATCHERS {
matches = append(matches, mather(permutation)...)
}
}
for _, match := range matches {
match.Entropy += entropy.ExtraLeetEntropy(match, password)
match.DictionaryName = match.DictionaryName + "_3117"
}
return matches
}
func getAllPermutationsOfLeetSubstitutions(password string, substitutionsMap map[string][]string) []string {
var permutations []string
for index, char := range password {
for value, splice := range substitutionsMap {
for _, sub := range splice {
if string(char) == sub {
var permutation string
permutation = password[:index] + value + password[index+1:]
permutations = append(permutations, permutation)
if index < len(permutation) {
tempPermutations := getAllPermutationsOfLeetSubstitutions(permutation[index+1:], substitutionsMap)
for _, temp := range tempPermutations {
permutations = append(permutations, permutation[:index+1]+temp)
}
}
}
}
}
}
return permutations
}
func relevantL33tSubtable(password string) map[string][]string {
relevantSubs := make(map[string][]string)
for key, values := range L33T_TABLE.Graph {
for _, value := range values {
if strings.Contains(password, value) {
relevantSubs[key] = append(relevantSubs[key], value)
}
}
}
return relevantSubs
}

View file

@ -0,0 +1,77 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/adjacency"
"github.com/nbutton23/zxcvbn-go/frequency"
"github.com/nbutton23/zxcvbn-go/match"
"sort"
)
var (
DICTIONARY_MATCHERS []func(password string) []match.Match
MATCHERS []func(password string) []match.Match
ADJACENCY_GRAPHS []adjacency.AdjacencyGraph
L33T_TABLE adjacency.AdjacencyGraph
SEQUENCES map[string]string
)
const (
DATE_RX_YEAR_SUFFIX string = `((\d{1,2})(\s|-|\/|\\|_|\.)(\d{1,2})(\s|-|\/|\\|_|\.)(19\d{2}|200\d|201\d|\d{2}))`
DATE_RX_YEAR_PREFIX string = `((19\d{2}|200\d|201\d|\d{2})(\s|-|/|\\|_|\.)(\d{1,2})(\s|-|/|\\|_|\.)(\d{1,2}))`
DATE_WITHOUT_SEP_MATCH string = `\d{4,8}`
)
func init() {
loadFrequencyList()
}
func Omnimatch(password string, userInputs []string) (matches []match.Match) {
//Can I run into the issue where nil is not equal to nil?
if DICTIONARY_MATCHERS == nil || ADJACENCY_GRAPHS == nil {
loadFrequencyList()
}
if userInputs != nil {
userInputMatcher := buildDictMatcher("user_inputs", buildRankedDict(userInputs))
matches = userInputMatcher(password)
}
for _, matcher := range MATCHERS {
matches = append(matches, matcher(password)...)
}
sort.Sort(match.Matches(matches))
return matches
}
func loadFrequencyList() {
for n, list := range frequency.FrequencyLists {
DICTIONARY_MATCHERS = append(DICTIONARY_MATCHERS, buildDictMatcher(n, buildRankedDict(list.List)))
}
L33T_TABLE = adjacency.AdjacencyGph["l33t"]
ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["qwerty"])
ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["dvorak"])
ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["keypad"])
ADJACENCY_GRAPHS = append(ADJACENCY_GRAPHS, adjacency.AdjacencyGph["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, DICTIONARY_MATCHERS...)
MATCHERS = append(MATCHERS, spatialMatch)
MATCHERS = append(MATCHERS, repeatMatch)
MATCHERS = append(MATCHERS, sequenceMatch)
MATCHERS = append(MATCHERS, l33tMatch)
MATCHERS = append(MATCHERS, dateSepMatcher)
MATCHERS = append(MATCHERS, dateWithoutSepMatch)
}

View file

@ -0,0 +1,59 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
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,68 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
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 += 1
}
}
}
i = j
}
return matches
}

View file

@ -0,0 +1,80 @@
package matching
import (
"github.com/nbutton23/zxcvbn-go/adjacency"
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"strings"
)
func spatialMatch(password string) (matches []match.Match) {
for _, graph := range ADJACENCY_GRAPHS {
if graph.Graph != nil {
matches = append(matches, spatialMatchHelper(password, graph)...)
}
}
return matches
}
func spatialMatchHelper(password string, graph adjacency.AdjacencyGraph) (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 += 1
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 += 1
}
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 += 1
lastDirection = foundDirection
}
break
}
}
}
//if the current pattern continued, extend j and try to grow again
if found {
j += 1
} 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,180 @@
package scoring
import (
"fmt"
"github.com/nbutton23/zxcvbn-go/entropy"
"github.com/nbutton23/zxcvbn-go/match"
"github.com/nbutton23/zxcvbn-go/utils/math"
"math"
"sort"
)
const (
START_UPPER string = `^[A-Z][^A-Z]+$`
END_UPPER string = `^[^A-Z]+[A-Z]$'`
ALL_UPPER string = `^[A-Z]+$`
//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!
SINGLE_GUESS float64 = 0.010
NUM_ATTACKERS float64 = 100 //Cores used to make guesses
SECONDS_PER_GUESS float64 = SINGLE_GUESS / NUM_ATTACKERS
)
type MinEntropyMatch struct {
Password string
Entropy float64
MatchSequence []match.Match
CrackTime float64
CrackTimeDisplay string
Score int
CalcTime float64
}
/*
Returns 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)) * SECONDS_PER_GUESS
return crackTime
}
func roundToXDigits(number float64, digits int) float64 {
return zxcvbn_math.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 zxcvbn_math
import "math"
/**
I am surprised that I have to define these. . . Maybe i just didn't look hard enough for a lib.
*/
//http://blog.plover.com/math/choose.html
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
}
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
}

19
vendor/github.com/nbutton23/zxcvbn-go/zxcvbn.go generated vendored Normal file
View file

@ -0,0 +1,19 @@
package zxcvbn
import (
"github.com/nbutton23/zxcvbn-go/matching"
"github.com/nbutton23/zxcvbn-go/scoring"
"github.com/nbutton23/zxcvbn-go/utils/math"
"time"
)
func PasswordStrength(password string, userInputs []string) scoring.MinEntropyMatch {
start := time.Now()
matches := matching.Omnimatch(password, userInputs)
result := scoring.MinimumEntropyMatchSequence(password, matches)
end := time.Now()
calcTime := end.Nanosecond() - start.Nanosecond()
result.CalcTime = zxcvbn_math.Round(float64(calcTime)*time.Nanosecond.Seconds(), .5, 3)
return result
}