Initial QSfera import
This commit is contained in:
+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
|
||||
}
|
||||
Reference in New Issue
Block a user