Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.vscode
/fuzz/workdir
/fuzz/*.zip
+13
View File
@@ -0,0 +1,13 @@
language: go
sudo: false
go:
- 1.8
- 1.9
- tip
before_install:
- go get github.com/mattn/goveralls
- go get -t -v ./...
script:
- $GOPATH/bin/goveralls -service=travis-ci
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2018 Vincent Vanackere and Trustelem
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+15
View File
@@ -0,0 +1,15 @@
[![GoDoc](https://godoc.org/github.com/trustelem/zxcvbn?status.svg)](https://godoc.org/github.com/trustelem/zxcvbn)
[![Build
Status](https://travis-ci.org/trustelem/zxcvbn.svg?branch=master)](https://travis-ci.org/trustelem/zxcvbn)
[![Coverage Status](https://coveralls.io/repos/github/trustelem/zxcvbn/badge.svg?branch=master)](https://coveralls.io/github/trustelem/zxcvbn?branch=master)
This is a go port of [zxcvbn](https://github.com/dropbox/zxcvbn), a password strength estimator inspired by password crackers. Through pattern matching and conservative estimation, it recognizes and weighs 30k common passwords, common names and surnames according to US census data, popular English words from Wikipedia and US television and movies, and other common patterns like dates, repeats (aaa), sequences (abcd), keyboard patterns (qwertyuiop), and l33t speak.
This port aims to be fully compatible (i.e. give the same results for a given password using the same set of dictionnaries) with the upstream coffeescript libray from Dropbox: all unit tests from the upstream library have been ported (and even more tests have been added) to ensure that this holds.
------------------------------------------------------------------------
Current status:
- this library should be 100% compatible (score, sequence and number of guesses) with [release 4.4.2](https://github.com/dropbox/zxcvbn/releases/tag/v4.4.2) of the coffeescript library.
- feedback messages are missing
+40
View File
@@ -0,0 +1,40 @@
package adjacency
type Graph struct {
Graph map[string][]string
Name string
AverageDegree float64
}
// Graphs exports the adjacency graphs by name
var Graphs = make(map[string]*Graph)
func init() {
initGraph("qwerty", adjacencyGraphQwerty)
initGraph("dvorak", adjacencyGraphDvorak)
initGraph("keypad", adjacencyGraphKeypad)
initGraph("mac_keypad", adjacencyGraphMacKeypad)
}
func initGraph(name string, data map[string][]string) {
g := &Graph{
Name: name,
Graph: data,
AverageDegree: calculateAvgDegree(data),
}
Graphs[name] = g
}
func calculateAvgDegree(g map[string][]string) float64 {
var avg float64
for _, value := range g {
for _, chars := range value {
if chars != "" {
avg += float64(1)
}
}
}
return avg / float64(len(g))
}
+233
View File
@@ -0,0 +1,233 @@
package adjacency
// generated by scripts/build_keyboard_adjacency_graphs.py
var adjacencyGraphQwerty = map[string][]string{
`$`: {`3#`, ``, ``, `5%`, `rR`, `eE`},
`(`: {`8*`, ``, ``, `0)`, `oO`, `iI`},
`,`: {`mM`, `kK`, `lL`, `.>`, ``, ``},
`0`: {`9(`, ``, ``, `-_`, `pP`, `oO`},
`4`: {`3#`, ``, ``, `5%`, `rR`, `eE`},
`8`: {`7&`, ``, ``, `9(`, `iI`, `uU`},
`<`: {`mM`, `kK`, `lL`, `.>`, ``, ``},
`@`: {`1!`, ``, ``, `3#`, `wW`, `qQ`},
`D`: {`sS`, `eE`, `rR`, `fF`, `cC`, `xX`},
`H`: {`gG`, `yY`, `uU`, `jJ`, `nN`, `bB`},
`L`: {`kK`, `oO`, `pP`, `;:`, `.>`, `,<`},
`P`: {`oO`, `0)`, `-_`, `[{`, `;:`, `lL`},
`T`: {`rR`, `5%`, `6^`, `yY`, `gG`, `fF`},
`X`: {`zZ`, `sS`, `dD`, `cC`, ``, ``},
`\`: {`]}`, ``, ``, ``, ``, ``},
"`": {``, ``, ``, `1!`, ``, ``},
`d`: {`sS`, `eE`, `rR`, `fF`, `cC`, `xX`},
`h`: {`gG`, `yY`, `uU`, `jJ`, `nN`, `bB`},
`l`: {`kK`, `oO`, `pP`, `;:`, `.>`, `,<`},
`p`: {`oO`, `0)`, `-_`, `[{`, `;:`, `lL`},
`t`: {`rR`, `5%`, `6^`, `yY`, `gG`, `fF`},
`x`: {`zZ`, `sS`, `dD`, `cC`, ``, ``},
`|`: {`]}`, ``, ``, ``, ``, ``},
`#`: {`2@`, ``, ``, `4$`, `eE`, `wW`},
`'`: {`;:`, `[{`, `]}`, ``, ``, `/?`},
`+`: {`-_`, ``, ``, ``, `]}`, `[{`},
`/`: {`.>`, `;:`, `'"`, ``, ``, ``},
`3`: {`2@`, ``, ``, `4$`, `eE`, `wW`},
`7`: {`6^`, ``, ``, `8*`, `uU`, `yY`},
`;`: {`lL`, `pP`, `[{`, `'"`, `/?`, `.>`},
`?`: {`.>`, `;:`, `'"`, ``, ``, ``},
`C`: {`xX`, `dD`, `fF`, `vV`, ``, ``},
`G`: {`fF`, `tT`, `yY`, `hH`, `bB`, `vV`},
`K`: {`jJ`, `iI`, `oO`, `lL`, `,<`, `mM`},
`O`: {`iI`, `9(`, `0)`, `pP`, `lL`, `kK`},
`S`: {`aA`, `wW`, `eE`, `dD`, `xX`, `zZ`},
`W`: {`qQ`, `2@`, `3#`, `eE`, `sS`, `aA`},
`[`: {`pP`, `-_`, `=+`, `]}`, `'"`, `;:`},
`_`: {`0)`, ``, ``, `=+`, `[{`, `pP`},
`c`: {`xX`, `dD`, `fF`, `vV`, ``, ``},
`g`: {`fF`, `tT`, `yY`, `hH`, `bB`, `vV`},
`k`: {`jJ`, `iI`, `oO`, `lL`, `,<`, `mM`},
`o`: {`iI`, `9(`, `0)`, `pP`, `lL`, `kK`},
`s`: {`aA`, `wW`, `eE`, `dD`, `xX`, `zZ`},
`w`: {`qQ`, `2@`, `3#`, `eE`, `sS`, `aA`},
`{`: {`pP`, `-_`, `=+`, `]}`, `'"`, `;:`},
`"`: {`;:`, `[{`, `]}`, ``, ``, `/?`},
`&`: {`6^`, ``, ``, `8*`, `uU`, `yY`},
`*`: {`7&`, ``, ``, `9(`, `iI`, `uU`},
`.`: {`,<`, `lL`, `;:`, `/?`, ``, ``},
`2`: {`1!`, ``, ``, `3#`, `wW`, `qQ`},
`6`: {`5%`, ``, ``, `7&`, `yY`, `tT`},
`:`: {`lL`, `pP`, `[{`, `'"`, `/?`, `.>`},
`>`: {`,<`, `lL`, `;:`, `/?`, ``, ``},
`B`: {`vV`, `gG`, `hH`, `nN`, ``, ``},
`F`: {`dD`, `rR`, `tT`, `gG`, `vV`, `cC`},
`J`: {`hH`, `uU`, `iI`, `kK`, `mM`, `nN`},
`N`: {`bB`, `hH`, `jJ`, `mM`, ``, ``},
`R`: {`eE`, `4$`, `5%`, `tT`, `fF`, `dD`},
`V`: {`cC`, `fF`, `gG`, `bB`, ``, ``},
`Z`: {``, `aA`, `sS`, `xX`, ``, ``},
`^`: {`5%`, ``, ``, `7&`, `yY`, `tT`},
`b`: {`vV`, `gG`, `hH`, `nN`, ``, ``},
`f`: {`dD`, `rR`, `tT`, `gG`, `vV`, `cC`},
`j`: {`hH`, `uU`, `iI`, `kK`, `mM`, `nN`},
`n`: {`bB`, `hH`, `jJ`, `mM`, ``, ``},
`r`: {`eE`, `4$`, `5%`, `tT`, `fF`, `dD`},
`v`: {`cC`, `fF`, `gG`, `bB`, ``, ``},
`z`: {``, `aA`, `sS`, `xX`, ``, ``},
`~`: {``, ``, ``, `1!`, ``, ``},
`!`: {"`~", ``, ``, `2@`, `qQ`, ``},
`%`: {`4$`, ``, ``, `6^`, `tT`, `rR`},
`)`: {`9(`, ``, ``, `-_`, `pP`, `oO`},
`-`: {`0)`, ``, ``, `=+`, `[{`, `pP`},
`1`: {"`~", ``, ``, `2@`, `qQ`, ``},
`5`: {`4$`, ``, ``, `6^`, `tT`, `rR`},
`9`: {`8*`, ``, ``, `0)`, `oO`, `iI`},
`=`: {`-_`, ``, ``, ``, `]}`, `[{`},
`A`: {``, `qQ`, `wW`, `sS`, `zZ`, ``},
`E`: {`wW`, `3#`, `4$`, `rR`, `dD`, `sS`},
`I`: {`uU`, `8*`, `9(`, `oO`, `kK`, `jJ`},
`M`: {`nN`, `jJ`, `kK`, `,<`, ``, ``},
`Q`: {``, `1!`, `2@`, `wW`, `aA`, ``},
`U`: {`yY`, `7&`, `8*`, `iI`, `jJ`, `hH`},
`Y`: {`tT`, `6^`, `7&`, `uU`, `hH`, `gG`},
`]`: {`[{`, `=+`, ``, `\|`, ``, `'"`},
`a`: {``, `qQ`, `wW`, `sS`, `zZ`, ``},
`e`: {`wW`, `3#`, `4$`, `rR`, `dD`, `sS`},
`i`: {`uU`, `8*`, `9(`, `oO`, `kK`, `jJ`},
`m`: {`nN`, `jJ`, `kK`, `,<`, ``, ``},
`q`: {``, `1!`, `2@`, `wW`, `aA`, ``},
`u`: {`yY`, `7&`, `8*`, `iI`, `jJ`, `hH`},
`y`: {`tT`, `6^`, `7&`, `uU`, `hH`, `gG`},
`}`: {`[{`, `=+`, ``, `\|`, ``, `'"`},
}
var adjacencyGraphDvorak = map[string][]string{
`$`: {`3#`, ``, ``, `5%`, `pP`, `.>`},
`(`: {`8*`, ``, ``, `0)`, `rR`, `cC`},
`,`: {`'"`, `2@`, `3#`, `.>`, `oO`, `aA`},
`0`: {`9(`, ``, ``, `[{`, `lL`, `rR`},
`4`: {`3#`, ``, ``, `5%`, `pP`, `.>`},
`8`: {`7&`, ``, ``, `9(`, `cC`, `gG`},
`<`: {`'"`, `2@`, `3#`, `.>`, `oO`, `aA`},
`@`: {`1!`, ``, ``, `3#`, `,<`, `'"`},
`D`: {`iI`, `fF`, `gG`, `hH`, `bB`, `xX`},
`H`: {`dD`, `gG`, `cC`, `tT`, `mM`, `bB`},
`L`: {`rR`, `0)`, `[{`, `/?`, `sS`, `nN`},
`P`: {`.>`, `4$`, `5%`, `yY`, `uU`, `eE`},
`T`: {`hH`, `cC`, `rR`, `nN`, `wW`, `mM`},
`X`: {`kK`, `iI`, `dD`, `bB`, ``, ``},
`\`: {`=+`, ``, ``, ``, ``, ``},
"`": {``, ``, ``, `1!`, ``, ``},
`d`: {`iI`, `fF`, `gG`, `hH`, `bB`, `xX`},
`h`: {`dD`, `gG`, `cC`, `tT`, `mM`, `bB`},
`l`: {`rR`, `0)`, `[{`, `/?`, `sS`, `nN`},
`p`: {`.>`, `4$`, `5%`, `yY`, `uU`, `eE`},
`t`: {`hH`, `cC`, `rR`, `nN`, `wW`, `mM`},
`x`: {`kK`, `iI`, `dD`, `bB`, ``, ``},
`|`: {`=+`, ``, ``, ``, ``, ``},
`#`: {`2@`, ``, ``, `4$`, `.>`, `,<`},
`'`: {``, `1!`, `2@`, `,<`, `aA`, ``},
`+`: {`/?`, `]}`, ``, `\|`, ``, `-_`},
`/`: {`lL`, `[{`, `]}`, `=+`, `-_`, `sS`},
`3`: {`2@`, ``, ``, `4$`, `.>`, `,<`},
`7`: {`6^`, ``, ``, `8*`, `gG`, `fF`},
`;`: {``, `aA`, `oO`, `qQ`, ``, ``},
`?`: {`lL`, `[{`, `]}`, `=+`, `-_`, `sS`},
`C`: {`gG`, `8*`, `9(`, `rR`, `tT`, `hH`},
`G`: {`fF`, `7&`, `8*`, `cC`, `hH`, `dD`},
`K`: {`jJ`, `uU`, `iI`, `xX`, ``, ``},
`O`: {`aA`, `,<`, `.>`, `eE`, `qQ`, `;:`},
`S`: {`nN`, `lL`, `/?`, `-_`, `zZ`, `vV`},
`W`: {`mM`, `tT`, `nN`, `vV`, ``, ``},
`[`: {`0)`, ``, ``, `]}`, `/?`, `lL`},
`_`: {`sS`, `/?`, `=+`, ``, ``, `zZ`},
`c`: {`gG`, `8*`, `9(`, `rR`, `tT`, `hH`},
`g`: {`fF`, `7&`, `8*`, `cC`, `hH`, `dD`},
`k`: {`jJ`, `uU`, `iI`, `xX`, ``, ``},
`o`: {`aA`, `,<`, `.>`, `eE`, `qQ`, `;:`},
`s`: {`nN`, `lL`, `/?`, `-_`, `zZ`, `vV`},
`w`: {`mM`, `tT`, `nN`, `vV`, ``, ``},
`{`: {`0)`, ``, ``, `]}`, `/?`, `lL`},
`"`: {``, `1!`, `2@`, `,<`, `aA`, ``},
`&`: {`6^`, ``, ``, `8*`, `gG`, `fF`},
`*`: {`7&`, ``, ``, `9(`, `cC`, `gG`},
`.`: {`,<`, `3#`, `4$`, `pP`, `eE`, `oO`},
`2`: {`1!`, ``, ``, `3#`, `,<`, `'"`},
`6`: {`5%`, ``, ``, `7&`, `fF`, `yY`},
`:`: {``, `aA`, `oO`, `qQ`, ``, ``},
`>`: {`,<`, `3#`, `4$`, `pP`, `eE`, `oO`},
`B`: {`xX`, `dD`, `hH`, `mM`, ``, ``},
`F`: {`yY`, `6^`, `7&`, `gG`, `dD`, `iI`},
`J`: {`qQ`, `eE`, `uU`, `kK`, ``, ``},
`N`: {`tT`, `rR`, `lL`, `sS`, `vV`, `wW`},
`R`: {`cC`, `9(`, `0)`, `lL`, `nN`, `tT`},
`V`: {`wW`, `nN`, `sS`, `zZ`, ``, ``},
`Z`: {`vV`, `sS`, `-_`, ``, ``, ``},
`^`: {`5%`, ``, ``, `7&`, `fF`, `yY`},
`b`: {`xX`, `dD`, `hH`, `mM`, ``, ``},
`f`: {`yY`, `6^`, `7&`, `gG`, `dD`, `iI`},
`j`: {`qQ`, `eE`, `uU`, `kK`, ``, ``},
`n`: {`tT`, `rR`, `lL`, `sS`, `vV`, `wW`},
`r`: {`cC`, `9(`, `0)`, `lL`, `nN`, `tT`},
`v`: {`wW`, `nN`, `sS`, `zZ`, ``, ``},
`z`: {`vV`, `sS`, `-_`, ``, ``, ``},
`~`: {``, ``, ``, `1!`, ``, ``},
`!`: {"`~", ``, ``, `2@`, `'"`, ``},
`%`: {`4$`, ``, ``, `6^`, `yY`, `pP`},
`)`: {`9(`, ``, ``, `[{`, `lL`, `rR`},
`-`: {`sS`, `/?`, `=+`, ``, ``, `zZ`},
`1`: {"`~", ``, ``, `2@`, `'"`, ``},
`5`: {`4$`, ``, ``, `6^`, `yY`, `pP`},
`9`: {`8*`, ``, ``, `0)`, `rR`, `cC`},
`=`: {`/?`, `]}`, ``, `\|`, ``, `-_`},
`A`: {``, `'"`, `,<`, `oO`, `;:`, ``},
`E`: {`oO`, `.>`, `pP`, `uU`, `jJ`, `qQ`},
`I`: {`uU`, `yY`, `fF`, `dD`, `xX`, `kK`},
`M`: {`bB`, `hH`, `tT`, `wW`, ``, ``},
`Q`: {`;:`, `oO`, `eE`, `jJ`, ``, ``},
`U`: {`eE`, `pP`, `yY`, `iI`, `kK`, `jJ`},
`Y`: {`pP`, `5%`, `6^`, `fF`, `iI`, `uU`},
`]`: {`[{`, ``, ``, ``, `=+`, `/?`},
`a`: {``, `'"`, `,<`, `oO`, `;:`, ``},
`e`: {`oO`, `.>`, `pP`, `uU`, `jJ`, `qQ`},
`i`: {`uU`, `yY`, `fF`, `dD`, `xX`, `kK`},
`m`: {`bB`, `hH`, `tT`, `wW`, ``, ``},
`q`: {`;:`, `oO`, `eE`, `jJ`, ``, ``},
`u`: {`eE`, `pP`, `yY`, `iI`, `kK`, `jJ`},
`y`: {`pP`, `5%`, `6^`, `fF`, `iI`, `uU`},
`}`: {`[{`, ``, ``, ``, `=+`, `/?`},
}
var adjacencyGraphKeypad = map[string][]string{
`+`: {`9`, `*`, `-`, ``, ``, ``, ``, `6`},
`*`: {`/`, ``, ``, ``, `-`, `+`, `9`, `8`},
`-`: {`*`, ``, ``, ``, ``, ``, `+`, `9`},
`/`: {``, ``, ``, ``, `*`, `9`, `8`, `7`},
`.`: {`0`, `2`, `3`, ``, ``, ``, ``, ``},
`1`: {``, ``, `4`, `5`, `2`, `0`, ``, ``},
`0`: {``, `1`, `2`, `3`, `.`, ``, ``, ``},
`3`: {`2`, `5`, `6`, ``, ``, ``, `.`, `0`},
`2`: {`1`, `4`, `5`, `6`, `3`, `.`, `0`, ``},
`5`: {`4`, `7`, `8`, `9`, `6`, `3`, `2`, `1`},
`4`: {``, ``, `7`, `8`, `5`, `2`, `1`, ``},
`7`: {``, ``, ``, `/`, `8`, `5`, `4`, ``},
`6`: {`5`, `8`, `9`, `+`, ``, ``, `3`, `2`},
`9`: {`8`, `/`, `*`, `-`, `+`, ``, `6`, `5`},
`8`: {`7`, ``, `/`, `*`, `9`, `6`, `5`, `4`},
}
var adjacencyGraphMacKeypad = map[string][]string{
`+`: {`6`, `9`, `-`, ``, ``, ``, ``, `3`},
`*`: {`/`, ``, ``, ``, ``, ``, `-`, `9`},
`-`: {`9`, `/`, `*`, ``, ``, ``, `+`, `6`},
`/`: {`=`, ``, ``, ``, `*`, `-`, `9`, `8`},
`.`: {`0`, `2`, `3`, ``, ``, ``, ``, ``},
`1`: {``, ``, `4`, `5`, `2`, `0`, ``, ``},
`0`: {``, `1`, `2`, `3`, `.`, ``, ``, ``},
`3`: {`2`, `5`, `6`, `+`, ``, ``, `.`, `0`},
`2`: {`1`, `4`, `5`, `6`, `3`, `.`, `0`, ``},
`5`: {`4`, `7`, `8`, `9`, `6`, `3`, `2`, `1`},
`4`: {``, ``, `7`, `8`, `5`, `2`, `1`, ``},
`7`: {``, ``, ``, `=`, `8`, `5`, `4`, ``},
`6`: {`5`, `8`, `9`, `-`, `+`, ``, `3`, `2`},
`9`: {`8`, `=`, `/`, `*`, `-`, `+`, `6`, `5`},
`8`: {`7`, ``, `=`, `/`, `9`, `6`, `5`, `4`},
`=`: {``, ``, ``, ``, `/`, `9`, `8`, `7`},
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
package mathutils
func Min(a, b int) int {
if a < b {
return a
}
return b
}
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func Abs(a int) int {
if a < 0 {
return -a
}
return a
}
func NCk(n int, k int) float64 {
// http://blog.plover.com/math/choose.html
if k > n {
return 0
}
if k == 0 {
return 1
}
r := float64(1)
for d := 1; d <= k; d++ {
r *= float64(n)
r /= float64(d)
n--
}
return r
}
func Factorial(n int) float64 {
// unoptimized, called only on small n
if n < 2 {
return 1
}
f := float64(1)
for i := 2; i <= n; i++ {
f *= float64(i)
}
return f
}
+57
View File
@@ -0,0 +1,57 @@
package match
import (
"encoding/json"
)
type Match struct {
Pattern string `json:"pattern"`
I int `json:"i"`
J int `json:"j"`
Token string `json:"token"`
// Dictionary
Reversed bool `json:"reversed,omitempty"`
UppercaseVariations float64 `json:"uppercase_variations,omitempty"`
L33tVariations float64 `json:"l33t_variations,omitempty"`
MatchedWord string `json:"matched_word,omitempty"`
Rank int `json:"rank,omitempty"`
DictionaryName string `json:"dictionary_name,omitempty"`
L33t bool `json:"l33t,omitempty"`
Sub map[string]string `json:"sub,omitempty"`
// Sequence
Graph string `json:"graph,omitempty"`
SequenceName string `json:"sequence_name,omitempty"`
SequenceSpace int `json:"sequence_space,omitempty"`
Ascending bool `json:"ascending,omitempty"`
Turns int `json:"turns,omitempty"`
ShiftedCount int `json:"shifted_count,omitempty"`
// Repeat
BaseToken string `json:"base_token,omitempty"`
BaseGuesses float64 `json:"base_guesses,omitempty"`
BaseMatches []*Match `json:"base_matches,omitempty"`
RepeatCount int `json:"repeat_count,omitempty"`
// Regexp
RegexName string `json:"regex_name,omitempty"`
// Date
Year int `json:"year,omitempty"`
Month int `json:"month,omitempty"`
Day int `json:"day,omitempty"`
Separator string `json:"separator,omitempty"`
Entropy float64 `json:"entropy,omitempty"`
Guesses float64 `json:"guesses,omitempty"`
}
type Matcher interface {
Matches(password string) []*Match
}
// ToString returns a string representation of a sequence of matches
func ToString(matches []*Match) string {
b, _ := json.Marshal(matches)
return string(b)
}
+27
View File
@@ -0,0 +1,27 @@
package match
import (
"sort"
)
type matchesByIJ []*Match
func (s matchesByIJ) Len() int {
return len(s)
}
func (s matchesByIJ) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s matchesByIJ) 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
}
}
func Sort(matches []*Match) {
sort.Stable(matchesByIJ(matches))
}
+280
View File
@@ -0,0 +1,280 @@
package matching
import (
"strconv"
"github.com/dlclark/regexp2"
"github.com/trustelem/zxcvbn/internal/mathutils"
"github.com/trustelem/zxcvbn/match"
"github.com/trustelem/zxcvbn/scoring"
)
const dateMaxYear = 2050
const dateMinYear = 1000
var dateSplits = map[int][]struct{ k, l int }{
4: { // for length-4 strings, eg 1191 or 9111, two ways to split:
{1, 2}, // 1 1 91 (2nd split starts at index 1, 3rd at index 2)
{2, 3}, // 91 1 1
},
5: {
{1, 3}, // 1 11 91
{2, 3}, // 11 1 91
},
6: {
{1, 2}, // 1 1 1991
{2, 4}, // 11 11 91
{4, 5}, // 1991 1 1
},
7: {
{1, 3}, // 1 11 1991
{2, 3}, // 11 1 1991
{4, 5}, // 1991 1 11
{4, 6}, // 1991 11 1
},
8: {
{2, 4}, // 11 11 1991
{4, 6}, // 1991 11 11
},
}
var maybeDateNoSeparator = regexp2.MustCompile(
`^\d{4,8}$`, 0)
var maybeDateWithSeparator = regexp2.MustCompile(
`^(\d{1,4})([\s/\\_.-])(\d{1,2})\2(\d{1,4})$`, 0)
// a "date" is recognized as:
// any 3-tuple that starts or ends with a 2- or 4-digit year,
// with 2 or 0 separator chars (1.1.91 or 1191),
// maybe zero-padded (01-01-91 vs 1-1-91),
// a month between 1 and 12,
// a day between 1 and 31.
//
// note: this isn't true date parsing in that "feb 31st" is allowed,
// this doesn't check for leap years, etc.
//
// recipe:
// start with regex to find maybe-dates, then attempt to map the integers
// onto month-day-year to filter the maybe-dates into dates.
// finally, remove matches that are substrings of other matches to reduce noise.
//
// note: instead of using a lazy or greedy regex to find many dates over the full string,
// this uses a ^...$ regex against every substring of the password -- less performant but leads
// to every possible date match.
type dateMatchCandidate struct {
Day int
Month int
Year int
}
type dateMatch struct{}
func (dm dateMatch) Matches(password string) []*match.Match {
matches := []*match.Match{}
// dates without separators are between length 4 '1191' and 8 '11111991'
for i := 0; i <= len(password)-4; i++ {
for j := i + 3; j <= i+7; j++ {
if j >= len(password) {
break
}
token := password[i : j+1]
if m, err := maybeDateNoSeparator.MatchString(token); !m || err != nil {
continue
}
var candidates []*dateMatchCandidate
for _, s := range dateSplits[len(token)] {
s1, s2, s3 := token[0:s.k], token[s.k:s.l], token[s.l:]
if dmy := mapIntsToDMY(s1, s2, s3); dmy != nil {
candidates = append(candidates, dmy)
}
}
if len(candidates) == 0 {
continue
}
// at this point: different possible dmy mappings for the same i,j substring.
// match the candidate date that likely takes the fewest guesses: a year closest to 2000.
// (scoring.REFERENCE_YEAR).
//
// ie, considering '111504', prefer 11-15-04 to 1-1-1504
// (interpreting '04' as 2004)
bestCandidate := candidates[0]
minDistance := dateMatchMetric(candidates[0])
for _, candidate := range candidates[1:] {
distance := dateMatchMetric(candidate)
if distance < minDistance {
bestCandidate = candidate
minDistance = distance
}
}
matches = append(matches, &match.Match{
Pattern: "date",
Token: token,
I: i,
J: j,
Separator: "",
Year: bestCandidate.Year,
Month: bestCandidate.Month,
Day: bestCandidate.Day,
})
}
}
// dates with separators are between length 6 '1/1/91' and 10 '11/11/1991'
for i := 0; i <= len(password)-6; i++ {
for j := i + 5; j <= i+9; j++ {
if j >= len(password) {
break
}
token := password[i : j+1]
m, err := maybeDateWithSeparator.FindStringMatch(token)
if m == nil || err != nil {
continue
}
dmy := mapIntsToDMY(
m.GroupByNumber(1).String(),
m.GroupByNumber(3).String(),
m.GroupByNumber(4).String(),
)
if dmy != nil {
matches = append(matches, &match.Match{
Pattern: "date",
Token: token,
I: i,
J: j,
Separator: m.GroupByNumber(2).String(),
Year: dmy.Year,
Month: dmy.Month,
Day: dmy.Day,
})
}
}
}
// matches now contains all valid date strings in a way that is tricky to capture
// with regexes only. while thorough, it will contain some unintuitive noise:
//
// '2015_06_04', in addition to matching 2015_06_04, will also contain
// 5(!) other date matches: 15_06_04, 5_06_04, ..., even 2015 (matched as 5/1/2020)
//
// to reduce noise, remove date matches that are strict substrings of others
var filteredMatches []*match.Match
for _, m := range matches {
isSubmatch := false
for _, o := range matches {
if m == o {
continue
}
if o.I <= m.I && o.J >= m.J {
isSubmatch = true
break
}
}
if !isSubmatch {
filteredMatches = append(filteredMatches, m)
}
}
match.Sort(filteredMatches)
return filteredMatches
}
func dateMatchMetric(c *dateMatchCandidate) int {
return mathutils.Abs(c.Year - scoring.ReferenceYear)
}
func mapIntsToDMY(s1, s2, s3 string) *dateMatchCandidate {
// given a 3-tuple, discard if:
// middle int is over 31 (for all dmy formats, years are never allowed in the middle)
// middle int is zero
// any int is over the max allowable year
// any int is over two digits but under the min allowable year
// 2 ints are over 31, the max allowable day
// 2 ints are zero
// all ints are over 12, the max allowable month
i1, _ := strconv.Atoi(s1)
i2, _ := strconv.Atoi(s2)
i3, _ := strconv.Atoi(s3)
if i2 > 31 || i2 <= 0 {
return nil
}
over12 := 0
over31 := 0
under1 := 0
for _, i := range [3]int{i1, i2, i3} {
if (i > 99 && i < dateMinYear) || i > dateMaxYear {
return nil
}
if i > 31 {
over31++
}
if i > 12 {
over12++
}
if i <= 0 {
under1++
}
}
if over31 >= 2 || over12 == 3 || under1 >= 2 {
return nil
}
// first look for a four digit year: yyyy + daymonth or daymonth + yyyy
possibleYearSplits := [][3]int{
{i3, i1, i2}, // year last
{i1, i2, i3}, // year first
}
for _, split := range possibleYearSplits {
y := split[0]
if dateMinYear <= y && y <= dateMaxYear {
// for a candidate that includes a four-digit year,
// when the remaining ints don't match to a day and month,
// it is not a date.
return mapIntsToDM(split[1], split[2], y)
}
}
// given no four-digit year, two digit years are the most flexible int to match, so
// try to parse a day-month out of ints[0..1] or ints[1..0]
for _, split := range possibleYearSplits {
y := split[0]
dm := mapIntsToDM(split[1], split[2], y)
if dm != nil {
dm.Year = twoToFourDigitYear(dm.Year)
return dm
}
}
return nil
}
func mapIntsToDM(i1, i2 int, year int) *dateMatchCandidate {
if i1 <= 31 && i2 <= 12 {
return &dateMatchCandidate{
Day: i1,
Month: i2,
Year: year,
}
}
if i2 <= 31 && i1 <= 12 {
return &dateMatchCandidate{
Day: i2,
Month: i1,
Year: year,
}
}
return nil
}
func twoToFourDigitYear(year int) int {
if year > 99 {
return year
} else if year > 50 {
// 87 -> 1987
return year + 1900
} else {
// 15 -> 2015
return year + 2000
}
}
+61
View File
@@ -0,0 +1,61 @@
package matching
import (
"strings"
"github.com/trustelem/zxcvbn/match"
)
type dictionaryMatch struct {
rankedDictionaries map[string]rankedDictionnary
}
func (dm dictionaryMatch) Matches(password string) []*match.Match {
var results []*match.Match
for dictionaryName, rankedDict := range dm.rankedDictionaries {
for i := range password {
for delta := range password[i:] {
j := i + delta
word := strings.ToLower(password[i : j+1])
if val, ok := rankedDict[word]; ok {
matchDic := &match.Match{
Pattern: "dictionary",
I: i,
J: j,
Token: password[i : j+1],
MatchedWord: word,
Rank: val,
DictionaryName: dictionaryName,
}
results = append(results, matchDic)
}
}
}
}
match.Sort(results)
return results
}
func (dm dictionaryMatch) withDict(name string, d rankedDictionnary) dictionaryMatch {
rd2 := make(map[string]rankedDictionnary, len(dm.rankedDictionaries)+1)
for k, v := range dm.rankedDictionaries {
rd2[k] = v
}
rd2[name] = d
return dictionaryMatch{rankedDictionaries: rd2}
}
type rankedDictionnary map[string]int
func buildRankedDict(unrankedList []string) rankedDictionnary {
result := make(rankedDictionnary)
for i, v := range unrankedList {
result[strings.ToLower(v)] = i + 1
}
return result
}
+168
View File
@@ -0,0 +1,168 @@
package matching
import (
"bytes"
// "github.com/trustelem/zxcvbn/entropy"
"github.com/trustelem/zxcvbn/match"
"sort"
"strings"
)
type l33tMatch struct {
dm dictionaryMatch
table map[string][]string
}
func (lm l33tMatch) Matches(password string) []*match.Match {
matches := []*match.Match{}
substitutions := relevantSubtable(password, lm.table)
for _, sub := range enumerateLeetSubs(substitutions) {
if len(sub) == 0 {
break
}
subbedPassword := translate(password, sub)
for _, m := range lm.dm.Matches(subbedPassword) {
token := password[m.I : m.J+1]
if len(token) <= 1 {
// filter single-character l33t matches to reduce noise.
// otherwise '1' matches 'i', '4' matches 'a', both very common English words
continue
}
if strings.ToLower(token) == m.MatchedWord {
continue // only return the matches that return an actual substitution
}
m.Sub = make(map[string]string)
for subbed, chr := range sub {
if strings.Contains(token, subbed) {
m.Sub[subbed] = chr
}
}
m.L33t = true
m.Token = token
matches = append(matches, m)
}
}
match.Sort(matches)
return matches
}
func translate(password string, sub map[string]string) string {
var res string
for _, s := range password {
if v, ok := sub[string(s)]; ok {
res = res + v
} else {
res = res + string(s)
}
}
return res
}
type kv struct {
k string
v string
}
func dedup(subs [][]kv) [][]kv {
var res [][]kv
var b bytes.Buffer
members := make(map[string]bool)
for _, sub := range subs {
sort.SliceStable(sub, func(i, j int) bool {
return sub[i].k < sub[j].k
})
b.Reset()
for _, x := range sub {
b.WriteString(x.k)
b.WriteString(",")
b.WriteString(x.v)
}
key := b.String()
if !members[key] {
res = append(res, sub)
members[key] = true
}
}
return res
}
// enumerateLeetSubs returns the list of possible 1337 replacement dictionaries for a given password
func enumerateLeetSubs(table map[string][]string) []map[string]string {
var keys []string
for k := range table {
keys = append(keys, k)
}
sort.Strings(keys)
var subs = [][]kv{[]kv{}}
var helper func(keys []string)
helper = func(keys []string) {
if len(keys) == 0 {
return
}
firstKey := keys[0]
restKeys := keys[1:]
var nextSubs [][]kv
for _, l33tChr := range table[firstKey] {
for _, sub := range subs {
dupL33tIndex := -1
for i := 0; i < len(sub); i++ {
if sub[i].k == l33tChr {
dupL33tIndex = i
break
}
}
if dupL33tIndex == -1 {
subExtension := append(sub, kv{k: l33tChr, v: firstKey})
nextSubs = append(nextSubs, subExtension)
} else {
subAlternative := make([]kv, 0, len(sub))
subAlternative = append(subAlternative, sub[0:dupL33tIndex]...)
subAlternative = append(subAlternative, sub[dupL33tIndex+1:]...)
subAlternative = append(subAlternative, kv{k: l33tChr, v: firstKey})
// subAlternative := make([]kv, 0, len(sub))
// subAlternative = append(subAlternative, sub)
// subAlternative[dupL33tIndex] = {k:l33tChr,v:firstKey}
nextSubs = append(nextSubs, sub)
nextSubs = append(nextSubs, subAlternative)
}
}
}
subs = dedup(nextSubs)
helper(restKeys)
}
helper(keys)
var subDicts []map[string]string
for _, sub := range subs {
subDict := make(map[string]string)
for _, x := range sub {
subDict[x.k] = x.v
}
subDicts = append(subDicts, subDict)
}
return subDicts
}
func relevantSubtable(password string, table map[string][]string) map[string][]string {
passwordChars := make(map[rune]bool)
for _, chr := range password {
passwordChars[chr] = true
}
relevantSubs := make(map[string][]string)
for key, values := range table {
for _, value := range values {
if passwordChars[rune(value[0])] {
relevantSubs[key] = append(relevantSubs[key], value)
}
}
}
return relevantSubs
}
+77
View File
@@ -0,0 +1,77 @@
package matching
import (
"github.com/trustelem/zxcvbn/adjacency"
"github.com/trustelem/zxcvbn/frequency"
"github.com/trustelem/zxcvbn/match"
"regexp"
)
func Omnimatch(password string, userInputs []string) (matches []*match.Match) {
dictMatcher := defaultRankedDictionnaries.withDict("user_inputs", buildRankedDict(userInputs))
matchers := []match.Matcher{
dictMatcher,
reverseDictionnaryMatch{dm: dictMatcher},
l33tMatch{dm: dictMatcher, table: l33tTable},
spatialMatch{graphs: defaultGraphs},
repeatMatch{},
sequenceMatch{},
regexpMatch{regexes: defaultRegexpMatch},
dateMatch{},
}
for _, m := range matchers {
matches = append(matches, m.Matches(password)...)
}
match.Sort(matches)
return matches
}
var (
defaultRankedDictionnaries = loadDefaultDictionnaries()
defaultGraphs = loadDefaultAdjacencyGraphs()
defaultRegexpMatch = []struct {
Name string
Regexp *regexp.Regexp
}{
{
Name: "recent_year",
Regexp: regexp.MustCompile(`19\d\d|200\d|201\d`),
},
}
l33tTable = map[string][]string{
"a": {"4", "@"},
"b": {"8"},
"c": {"(", "{", "[", "<"},
"e": {"3"},
"g": {"6", "9"},
"i": {"1", "!", "|"},
"l": {"1", "|", "7"},
"o": {"0"},
"s": {"$", "5"},
"t": {"+", "7"},
"x": {"%"},
"z": {"2"},
}
)
func loadDefaultDictionnaries() dictionaryMatch {
rd := make(map[string]rankedDictionnary)
for n, list := range frequency.FrequencyLists {
rd[n] = buildRankedDict(list)
}
return dictionaryMatch{
rankedDictionaries: rd,
}
}
func loadDefaultAdjacencyGraphs() []*adjacency.Graph {
return []*adjacency.Graph{
adjacency.Graphs["qwerty"],
adjacency.Graphs["dvorak"],
adjacency.Graphs["keypad"],
adjacency.Graphs["mac_keypad"],
}
}
+31
View File
@@ -0,0 +1,31 @@
package matching
import (
"github.com/trustelem/zxcvbn/match"
"regexp"
)
type regexpMatch struct {
regexes []struct {
Name string
Regexp *regexp.Regexp
}
}
func (r regexpMatch) Matches(password string) []*match.Match {
var matches []*match.Match
for _, rx := range r.regexes {
for _, indexes := range rx.Regexp.FindAllStringIndex(password, -1) {
token := password[indexes[0]:indexes[1]]
matches = append(matches, &match.Match{
Pattern: "regex",
Token: token,
I: indexes[0],
J: indexes[1] - 1,
RegexName: rx.Name,
})
}
}
match.Sort(matches)
return matches
}
+85
View File
@@ -0,0 +1,85 @@
package matching
import (
"github.com/dlclark/regexp2"
"github.com/trustelem/zxcvbn/match"
"github.com/trustelem/zxcvbn/scoring"
)
type repeatMatch struct{}
var greedy = regexp2.MustCompile(`(.+)\1+`, 0)
var lazy = regexp2.MustCompile(`(.+?)\1+`, 0)
var lazyAnchored = regexp2.MustCompile(`^(.+?)\1+$`, 0)
func runeToStringIndex(index int, password string) int {
runes := 0
for i := range password {
if runes == index {
return i
}
runes++
}
//shouldn't really get here
return len(password)
}
func (repeatMatch) Matches(password string) []*match.Match {
var matches []*match.Match
lastIndex := 0
for lastIndex < len(password) {
greedyMatch, err := greedy.FindStringMatchStartingAt(password, lastIndex)
if err != nil || greedyMatch == nil {
break
}
lazyMatch, _ := lazy.FindStringMatchStartingAt(password, lastIndex)
var rmatch *regexp2.Match
var baseToken string
if greedyMatch.Captures[0].Length > lazyMatch.Captures[0].Length {
// greedy beats lazy for 'aabaab'
// greedy: [aabaab, aab]
// lazy: [aa, a]
rmatch = greedyMatch
// greedy's repeated string might itself be repeated, eg.
// aabaab in aabaabaabaab.
// run an anchored lazy match on greedy's repeated string
// to find the shortest repeated string
if m, err := lazyAnchored.FindStringMatch(rmatch.Captures[0].String()); err == nil {
baseToken = m.GroupByNumber(1).String()
}
} else {
// lazy beats greedy for 'aaaaa'
// greedy: [aaaa, aa]
// lazy: [aaaaa, a]
rmatch = lazyMatch
baseToken = rmatch.GroupByNumber(1).String()
}
// FindStringMatchStartingAt takes an index into the string (basically an offset
// into a byte array). rmatch indices will be rune offsets and so need to be converted
// to string offsets
i := runeToStringIndex(rmatch.Index, password)
j := runeToStringIndex(rmatch.Index+rmatch.Captures[0].Length-1, password)
// recursively match and score the base string
baseAnalysis := scoring.MostGuessableMatchSequence(
baseToken,
Omnimatch(baseToken, nil),
false,
)
matches = append(matches, &match.Match{
Pattern: "repeat",
I: i,
J: j,
Token: rmatch.Captures[0].String(),
BaseToken: baseToken,
BaseGuesses: baseAnalysis.Guesses,
BaseMatches: baseAnalysis.Sequence,
RepeatCount: rmatch.Captures[0].Length / len(baseToken),
})
lastIndex = j + 1
}
return matches
}
@@ -0,0 +1,38 @@
package matching
import (
"github.com/trustelem/zxcvbn/match"
)
type reverseDictionnaryMatch struct {
dm dictionaryMatch
}
func (rdm reverseDictionnaryMatch) Matches(password string) []*match.Match {
reversedPassword := reverse(password)
matches := rdm.dm.Matches(reversedPassword)
for _, m := range matches {
m.Token = reverse(m.Token)
m.Reversed = true
m.I, m.J = len(password)-1-m.J, len(password)-1-m.I
}
match.Sort(matches)
return matches
}
func reverse(input string) string {
// Get Unicode code points.
n := 0
rune := make([]rune, len(input))
for _, r := range input {
rune[n] = r
n++
}
rune = rune[0:n]
// Reverse
for i := 0; i < n/2; i++ {
rune[i], rune[n-1-i] = rune[n-1-i], rune[i]
}
// Convert back to UTF-8.
return string(rune)
}
+78
View File
@@ -0,0 +1,78 @@
package matching
import (
"regexp"
"github.com/trustelem/zxcvbn/match"
)
type sequenceMatch struct{}
const maxDelta = 5
func abs(a int) int {
if a < 0 {
return -a
}
return a
}
var reLower = regexp.MustCompile(`^[a-z]+$`)
var reUpper = regexp.MustCompile(`^[A-Z]+$`)
var reDigits = regexp.MustCompile(`^\d+$`)
func (sequenceMatch) Matches(password string) []*match.Match {
matches := []*match.Match{}
if len(password) == 1 {
return matches
}
update := func(i, j, delta int) {
absDelta := abs(delta)
if j-i > 1 || absDelta == 1 {
if absDelta > 0 && absDelta <= maxDelta {
token := password[i : j+1]
// conservatively stick with roman alphabet size.
// (this could be improved)
seqName := "unicode"
seqSpace := 26
if reLower.MatchString(token) {
seqName = "lower"
} else if reUpper.MatchString(token) {
seqName = "upper"
} else if reDigits.MatchString(token) {
seqName = "digits"
seqSpace = 10
}
matches = append(matches, &match.Match{
Pattern: "sequence",
I: i,
J: j,
Token: password[i : j+1],
SequenceName: seqName,
SequenceSpace: seqSpace,
Ascending: delta > 0,
})
}
}
}
i := 0
lastDelta := 0 // null
for k := 1; k <= len(password)-1; k++ {
delta := int(password[k]) - int(password[k-1])
if k == 1 {
lastDelta = delta
}
if delta == lastDelta {
continue
}
j := k - 1
update(i, j, lastDelta)
i = j
lastDelta = delta
}
update(i, len(password)-1, lastDelta)
return matches
}
+109
View File
@@ -0,0 +1,109 @@
package matching
import (
"strings"
"github.com/trustelem/zxcvbn/adjacency"
"github.com/trustelem/zxcvbn/match"
)
type spatialMatch struct {
graphs []*adjacency.Graph
}
func (s spatialMatch) Matches(password string) (matches []*match.Match) {
for _, graph := range s.graphs {
if graph.Graph != nil {
matches = append(matches, spatialMatchHelper(password, graph)...)
}
}
match.Sort(matches)
return matches
}
var shiftedChars = map[string]map[byte]bool{
"qwerty": stringToSet(`~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?`),
"dvorak": stringToSet(`~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?`),
}
func stringToSet(s string) map[byte]bool {
set := make(map[byte]bool)
for i := 0; i < len(s); i++ {
set[s[i]] = true
}
return set
}
func spatialMatchHelper(password string, graph *adjacency.Graph) (matches []*match.Match) {
shifted := shiftedChars[graph.Name]
i := 0
for i < len(password)-1 {
j := i + 1
lastDirection := -99
turns := 0
shiftedCount := 0
if shifted[password[i]] {
shiftedCount = 1
}
for {
prevChar := password[j-1]
found := false
foundDirection := -1
curDirection := -1
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 idx := strings.Index(adj, string(curChar)); idx != -1 {
found = true
foundDirection = curDirection
if idx == 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...
if j-i > 2 {
// don't consider length 1 or 2 chains.
matchSpc := &match.Match{
Pattern: "spatial",
I: i,
J: j - 1,
Token: password[i:j],
Graph: graph.Name,
Turns: turns,
ShiftedCount: shiftedCount,
}
matches = append(matches, matchSpc)
}
//. . . and then start a new search from the rest of the password
i = j
break
}
}
}
return matches
}
+276
View File
@@ -0,0 +1,276 @@
package scoring
import (
"math"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/trustelem/zxcvbn/adjacency"
"github.com/trustelem/zxcvbn/internal/mathutils"
"github.com/trustelem/zxcvbn/match"
)
const (
BruteforceCardinality = 10
MinGuessesBeforeGrowingSequence = 10000
MinSubmatchGuessesSingleChar = 10
MinSubmatchGuessesMultiChar = 50
)
func EstimateGuesses(m *match.Match, password string) float64 {
if m.Guesses > 0 {
// a match's guess estimate doesn't change. cache it.
return m.Guesses
}
minGuesses := float64(1)
if len(m.Token) < len(password) {
if len(m.Token) == 1 {
minGuesses = MinSubmatchGuessesSingleChar
} else {
minGuesses = MinSubmatchGuessesMultiChar
}
}
var guesses float64
switch m.Pattern {
case "bruteforce":
guesses = BruteforceGuesses(m)
case "dictionary":
guesses = DictionaryGuesses(m)
case "spatial":
guesses = SpatialGuesses(m)
case "repeat":
guesses = RepeatGuesses(m)
case "sequence":
guesses = SequenceGuesses(m)
case "regex":
guesses = RegexGuesses(m)
case "date":
guesses = DateGuesses(m)
default:
// panic("unknown pattern " + m.Pattern)
}
m.Guesses = guesses
if m.Guesses < minGuesses {
m.Guesses = minGuesses
}
return m.Guesses
}
func BruteforceGuesses(m *match.Match) float64 {
runeCount := utf8.RuneCountInString(m.Token)
guesses := math.Pow(BruteforceCardinality, float64(runeCount))
/* if guesses == Number.POSITIVE_INFINITY {
guesses = math.MaxInt;
}*/
// small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
// submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
minGuesses := float64(0)
if runeCount == 1 {
minGuesses = MinSubmatchGuessesSingleChar + 1
} else {
minGuesses = MinSubmatchGuessesMultiChar + 1
}
if guesses < minGuesses {
return minGuesses
}
return guesses
}
func DictionaryGuesses(m *match.Match) float64 {
m.BaseGuesses = float64(m.Rank)
m.UppercaseVariations = UppercaseVariations(m.Token)
m.L33tVariations = L33tVariations(m)
reversedVariations := 1
if m.Reversed {
reversedVariations = 2
}
return float64(m.BaseGuesses) * float64(m.UppercaseVariations) * float64(m.L33tVariations) * float64(reversedVariations)
}
var reStartUpper = regexp.MustCompile(`^[A-Z][^A-Z]+$`)
var reEndUpper = regexp.MustCompile(`^[^A-Z]+[A-Z]$`)
var reAllUpper = regexp.MustCompile(`^[^a-z]+$`)
var reAllLower = regexp.MustCompile(`^[^A-Z]+$`)
func UppercaseVariations(w string) float64 {
if reAllLower.MatchString(w) || strings.ToLower(w) == w {
return 1
}
// a capitalized word is the most common capitalization scheme,
// so it only doubles the search space (uncapitalized + capitalized).
// allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe.
if reStartUpper.MatchString(w) ||
reEndUpper.MatchString(w) ||
reAllUpper.MatchString(w) {
return 2
}
// 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 eg. PASSwORD),
// the number of ways to lowercase U+L letters with L lowercase letters or less.
u := 0
l := 0
for _, c := range w {
if c >= 'A' && c <= 'Z' {
u++
continue
}
if c >= 'a' && c <= 'z' {
l++
}
}
variations := float64(0)
for i := 1; i <= u && i <= l; i++ {
variations += mathutils.NCk(u+l, i)
}
return variations
}
func L33tVariations(m *match.Match) float64 {
if !m.L33t {
return 1
}
// lower-case match.token before calculating: capitalization shouldn't affect l33t calc.
chrs := strings.ToLower(m.Token)
variations := float64(1)
for subbed, unsubbed := range m.Sub {
s := 0 // num of subbed chars
u := 0 // num of unsubbed chars
for _, c := range chrs {
if string(c) == subbed {
s++
}
if string(c) == unsubbed {
u++
}
}
if s == 0 || u == 0 {
// for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
// treat that as doubling the space (attacker needs to try fully subbed chars in addition to
// unsubbed.)
variations *= 2
} else {
// this case is similar to capitalization:
// with aa44a, U = 3, S = 2, attacker needs to try unsubbed + one sub + two subs
p := mathutils.Min(u, s)
possibilities := float64(0)
for i := 1; i <= p; i++ {
possibilities += mathutils.NCk(u+s, i)
}
variations *= possibilities
}
}
return variations
}
func SpatialGuesses(m *match.Match) float64 {
s := float64(0)
d := float64(0)
switch m.Graph {
case "qwerty", "dvorak":
s = float64(len(adjacency.Graphs["qwerty"].Graph))
d = adjacency.Graphs["qwerty"].AverageDegree
default:
s = float64(len(adjacency.Graphs["keypad"].Graph))
d = adjacency.Graphs["keypad"].AverageDegree
}
guesses := float64(0)
runeCount := utf8.RuneCountInString(m.Token)
l := runeCount
t := m.Turns
// estimate the number of possible patterns w/ length L or less with t turns or less.
for i := 2; i <= l; i++ {
possibleTurns := mathutils.Min(t, i-1)
for j := 1; j <= possibleTurns; j++ {
guesses += mathutils.NCk(i-1, j-1) * s * math.Pow(d, float64(j))
}
}
// add extra guesses for shifted keys. (% instead of 5, A instead of a.)
// math is similar to extra guesses of l33t substitutions in dictionary matches.
if m.ShiftedCount > 0 {
s := m.ShiftedCount
u := runeCount - m.ShiftedCount // unshifted count
if s == 0 || u == 0 {
guesses *= 2
} else {
shiftedVariations := float64(0)
for i := 1; i <= mathutils.Min(s, u); i++ {
shiftedVariations += mathutils.NCk(s+u, i)
}
guesses *= shiftedVariations
}
}
return (guesses)
}
func RepeatGuesses(m *match.Match) float64 {
return float64(m.BaseGuesses) * float64(m.RepeatCount)
}
func SequenceGuesses(m *match.Match) float64 {
firstChr := m.Token[0]
// lower guesses for obvious starting points
baseGuesses := 0
switch firstChr {
case 'a', 'A', 'z', 'Z', '0', '1', '9':
baseGuesses = 4
default:
if firstChr >= '0' && firstChr <= '9' {
baseGuesses = 10 // digits
} else {
// could give a higher base for uppercase,
// assigning 26 to both upper and lower sequences is more conservative.
baseGuesses = 26
}
}
if !m.Ascending {
// need to try a descending sequence in addition to every ascending sequence ->
// 2x guesses
baseGuesses *= 2
}
return float64(baseGuesses * len(m.Token))
}
func RegexGuesses(m *match.Match) float64 {
switch m.RegexName {
case "alpha_lower":
return math.Pow(26, float64(len(m.Token)))
case "alpha_upper":
return math.Pow(26, float64(len(m.Token)))
case "alpha":
return math.Pow(52, float64(len(m.Token)))
case "alphanumeric":
return math.Pow(62, float64(len(m.Token)))
case "digits":
return math.Pow(10, float64(len(m.Token)))
case "symbols":
return math.Pow(33, float64(len(m.Token)))
case "recent_year":
// conservative estimate of year space: num years from REFERENCE_YEAR.
// if year is close to REFERENCE_YEAR, estimate a year space of MIN_YEAR_SPACE.
year, _ := strconv.Atoi(m.Token)
yearSpace := mathutils.Abs(year - ReferenceYear)
yearSpace = mathutils.Max(yearSpace, MinYearSpace)
return float64(yearSpace)
default:
return 0
}
}
const MinYearSpace = 20
var ReferenceYear = time.Now().Year()
func DateGuesses(m *match.Match) float64 {
// base guesses: (year distance from ReferenceYear) * num_days * num_years
yearSpace := mathutils.Max(mathutils.Abs(m.Year-ReferenceYear), MinYearSpace)
guesses := yearSpace * 365
// add factor of 4 for separator selection (one of ~4 choices)
if m.Separator != "" {
guesses *= 4
}
return float64(guesses)
}
+243
View File
@@ -0,0 +1,243 @@
package scoring
import (
"math"
"sort"
"github.com/trustelem/zxcvbn/internal/mathutils"
"github.com/trustelem/zxcvbn/match"
)
type Result struct {
Password string
Guesses float64
Sequence []*match.Match
}
// ------------------------------------------------------------------------------
// search --- most guessable match sequence -------------------------------------
// ------------------------------------------------------------------------------
// MostGuessableMatchSequence takes a sequence of overlapping matches, returns the non-overlapping sequence with
// minimum guesses. the following is a O(l_max * (n + m)) dynamic programming algorithm
// for a length-n password with m candidate matches. l_max is the maximum optimal
// sequence length spanning each prefix of the password. In practice it rarely exceeds 5 and the
// search terminates rapidly.
//
// the optimal "minimum guesses" sequence is here defined to be the sequence that
// minimizes the following function:
//
// g = l! * Product(m.guesses for m in sequence) + D^(l - 1)
//
// where l is the length of the sequence.
//
// the factorial term is the number of ways to order l patterns.
//
// the D^(l-1) term is another length penalty, roughly capturing the idea that an
// attacker will try lower-length sequences first before trying length-l sequences.
//
// for example, consider a sequence that is date-repeat-dictionary.
// - an attacker would need to try other date-repeat-dictionary combinations,
// hence the product term.
// - an attacker would need to try repeat-date-dictionary, dictionary-repeat-date,
// ..., hence the factorial term.
// - an attacker would also likely try length-1 (dictionary) and length-2 (dictionary-date)
// sequences before length-3. assuming at minimum D guesses per pattern type,
// D^(l-1) approximates Sum(D^i for i in [1..l-1]
//
func MostGuessableMatchSequence(password string, matches []*match.Match, excludeAdditive bool) (result Result) {
n := len(password)
validIndexes := make([]bool, n)
for i := range password {
validIndexes[i] = true
}
// partition matches into sublists according to ending index j
matchesByJ := make([][]*match.Match, n)
for _, m := range matches {
if m.J < 0 || m.J > n {
// panic(fmt.Sprintf("Invalid match %#v", m))
continue
}
matchesByJ[m.J] = append(matchesByJ[m.J], m)
}
// small detail: for deterministic output, sort each sublist by i.
for _, matches := range matchesByJ {
sort.SliceStable(matches, func(a, b int) bool {
return matches[a].I < matches[b].I
})
}
var optimal struct {
// optimal.m[k][l] holds final match in the best length-l match sequence covering the
// password prefix up to k, inclusive.
// if there is no length-l sequence that scores better (fewer guesses) than
// a shorter match sequence spanning the same prefix, optimal.m[k][l] is undefined.
m []map[int]*match.Match
// same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence).
// optimal.pi allows for fast (non-looping) updates to the minimization function.
pi []map[int]float64
// same structure as optimal.m -- holds the overall metric.
g []map[int]float64
}
optimal.m = make([]map[int]*match.Match, n)
for i := 0; i < n; i++ {
optimal.m[i] = make(map[int]*match.Match)
}
optimal.pi = make([]map[int]float64, n)
for i := 0; i < n; i++ {
optimal.pi[i] = make(map[int]float64)
}
optimal.g = make([]map[int]float64, n)
for i := 0; i < n; i++ {
optimal.g[i] = make(map[int]float64)
}
// helper: considers whether a length-l sequence ending at match m is better (fewer guesses)
// than previously encountered sequences, updating state if so.
update := func(m *match.Match, l int) {
k := m.J
pi := EstimateGuesses(m, password)
if l > 1 {
// we're considering a length-l sequence ending with match m:
// obtain the product term in the minimization function by multiplying m's guesses
// by the product of the length-(l-1) sequence ending just before m, at m.i - 1.
pi *= optimal.pi[m.I-1][l-1]
}
// calculate the minimization func
g := mathutils.Factorial(l) * pi
if !excludeAdditive {
g += math.Pow(MinGuessesBeforeGrowingSequence, float64(l-1))
}
// update state if new best.
// first see if any competing sequences covering this prefix, with l or fewer matches,
// fare better than this sequence. if so, skip it and return.
if k >= 0 && k < len(optimal.g) {
for competingL, competingG := range optimal.g[k] {
if competingL <= l && competingG <= g {
return
}
}
}
// this sequence might be part of the final optimal sequence.
optimal.g[k][l] = g
optimal.m[k][l] = m
optimal.pi[k][l] = pi
}
// helper: evaluate bruteforce matches ending at k.
bruteforceUpdate := func(k int) {
// see if a single bruteforce match spanning the k-prefix is optimal.
m := makeBruteforceMatch(0, k, password)
update(m, 1)
previous := 0
for i := 1; i <= k; i++ {
if !validIndexes[i] {
continue
}
prev := previous
previous = i
// generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k).
// see if adding these new matches to any of the sequences in optimal[i-1]
// leads to new bests.
m := makeBruteforceMatch(i, k, password)
for l := 0; l < n; l++ {
lastM, ok := optimal.m[prev][l]
if !ok {
continue
}
// corner: an optimal sequence will never have two adjacent bruteforce matches.
// it is strictly better to have a single bruteforce match spanning the same region:
// same contribution to the guess product with a lower length.
// --> safe to skip those cases.
if lastM.Pattern == "bruteforce" {
continue
}
// try adding m to this length-l sequence.
update(m, l+1)
}
}
}
// helper: step backwards through optimal.m starting at the end,
// constructing the final optimal match sequence.
unwind := func(n int) []*match.Match {
var optimalMatchSequence []*match.Match
k := n - 1
// find the final best sequence length and score
l := -1 // = undefined
var g float64 = -1 // = Infinity
if k >= 0 && k < len(optimal.g) {
first := true
for candidateL, candidateG := range optimal.g[k] {
if first || candidateG < g || candidateG == g && candidateL > l {
l = candidateL
g = candidateG
first = false
}
}
for k >= 0 {
m, ok := optimal.m[k][l]
l--
if !ok {
//we're counting down through the potential keys (l). It's possible that
//the keys are non-contiguous so we need to skip values of l which aren't
//valid keys
continue
}
optimalMatchSequence = append([]*match.Match{m}, optimalMatchSequence...)
k = m.I - 1
}
}
return optimalMatchSequence
}
for k := 0; k < n; k++ {
for _, m := range matchesByJ[k] {
if m.I > 0 {
for l := 0; l < n; l++ {
if optimal.m[m.I-1][l] != nil {
update(m, l+1)
}
}
} else {
update(m, 1)
}
}
bruteforceUpdate(k)
}
optimalMatchSequence := unwind(n)
optimalL := len(optimalMatchSequence)
var guesses float64
// corner: empty password
if len(password) == 0 {
guesses = 1
} else {
guesses = optimal.g[n-1][optimalL]
}
// final result object
result.Password = password
result.Guesses = guesses
result.Sequence = optimalMatchSequence
return
}
// helper: make bruteforce match objects spanning i to j, inclusive.
func makeBruteforceMatch(i int, j int, password string) *match.Match {
return &match.Match{
Pattern: "bruteforce",
Token: password[i : j+1],
I: i,
J: j,
}
}
+104
View File
@@ -0,0 +1,104 @@
package zxcvbn
import (
"fmt"
"math"
)
type EstimatedTimes struct {
CrackTimesSeconds map[string]float64 `json:"crack_times_seconds"`
CrashTimesDisplay map[string]string `json:"crack_times_display"`
Score int
}
func estimateAttackTimes(guesses float64) (t EstimatedTimes) {
// crack_times_seconds
t.CrackTimesSeconds = make(map[string]float64)
t.CrackTimesSeconds["online_throttling_100_per_hour"] = guesses / (100 / 3600)
t.CrackTimesSeconds["online_no_throttling_10_per_second"] = guesses / 10
t.CrackTimesSeconds["offline_slow_hashing_1e4_per_second"] = guesses / 1e4
t.CrackTimesSeconds["offline_fast_hashing_1e10_per_second"] = guesses / 1e10
t.CrashTimesDisplay = make(map[string]string)
for scenario, seconds := range t.CrackTimesSeconds {
t.CrashTimesDisplay[scenario] = displayTime(seconds)
}
t.Score = guessesToScore(guesses)
return
}
func guessesToScore(guesses float64) int {
const DELTA = 5
if guesses < 1e3+DELTA {
// risky password: "too guessable"
return 0
}
if guesses < 1e6+DELTA {
// modest protection from throttled online attacks: "very guessable"
return 1
}
if guesses < 1e8+DELTA {
// modest protection from unthrottled online attacks: "somewhat guessable"
return 2
}
if guesses < 1e10+DELTA {
// modest protection from offline attacks: "safely unguessable"
// assuming a salted, slow hash function like bcrypt, scrypt, PBKDF2, argon, etc
return 3
}
// strong protection from offline attacks under same scenario: "very unguessable"
return 4
}
func displayTime(seconds float64) string {
minute := float64(60)
hour := minute * 60
day := hour * 24
month := day * 31
year := month * 12
century := year * 100
if seconds < 1 {
return "less than a second"
}
if seconds < minute {
return strCount(seconds, "second")
} else if seconds < hour {
return strCount(seconds/minute, "minute")
} else if seconds < day {
return strCount(seconds/hour, "hour")
} else if seconds < month {
return strCount(seconds/day, "day")
} else if seconds < year {
return strCount(seconds/month, "month")
} else if seconds < century {
return strCount(seconds/year, "year")
} else {
return "centuries"
}
}
func strCount(count float64, base string) string {
c := int(round(count, 0.5, 0))
str := fmt.Sprintf("%d %s", c, base)
if c > 1 {
str += "s"
}
return str
}
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
}
+36
View File
@@ -0,0 +1,36 @@
package zxcvbn
import (
"github.com/trustelem/zxcvbn/match"
"time"
"unicode/utf8"
"github.com/trustelem/zxcvbn/matching"
"github.com/trustelem/zxcvbn/scoring"
)
type Result struct {
Guesses float64
Sequence []*match.Match
Score int
CalcTime float64
}
func PasswordStrength(password string, userInputs []string) Result {
start := time.Now()
var result Result
if !utf8.ValidString(password) {
// Do not evaluate passwords containing invalid utf8
// => those will be reported as weak passwords
return result
}
matches := matching.Omnimatch(password, userInputs)
seq := scoring.MostGuessableMatchSequence(password, matches, false)
end := time.Now()
calcTime := end.Nanosecond() - start.Nanosecond()
result.CalcTime = round(float64(calcTime)*time.Nanosecond.Seconds(), .5, 3)
result.Sequence = seq.Sequence
result.Guesses = seq.Guesses
result.Score = guessesToScore(seq.Guesses)
return result
}