Initial QSfera import
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.vscode
|
||||
/fuzz/workdir
|
||||
/fuzz/*.zip
|
||||
+13
@@ -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
@@ -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
@@ -0,0 +1,15 @@
|
||||
|
||||
[](https://godoc.org/github.com/trustelem/zxcvbn)
|
||||
[](https://travis-ci.org/trustelem/zxcvbn)
|
||||
[](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
@@ -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
@@ -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`},
|
||||
}
|
||||
+10
File diff suppressed because one or more lines are too long
+51
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+38
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user