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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user