Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
package matching
import (
"strconv"
"github.com/dlclark/regexp2"
"github.com/trustelem/zxcvbn/internal/mathutils"
"github.com/trustelem/zxcvbn/match"
"github.com/trustelem/zxcvbn/scoring"
)
const dateMaxYear = 2050
const dateMinYear = 1000
var dateSplits = map[int][]struct{ k, l int }{
4: { // for length-4 strings, eg 1191 or 9111, two ways to split:
{1, 2}, // 1 1 91 (2nd split starts at index 1, 3rd at index 2)
{2, 3}, // 91 1 1
},
5: {
{1, 3}, // 1 11 91
{2, 3}, // 11 1 91
},
6: {
{1, 2}, // 1 1 1991
{2, 4}, // 11 11 91
{4, 5}, // 1991 1 1
},
7: {
{1, 3}, // 1 11 1991
{2, 3}, // 11 1 1991
{4, 5}, // 1991 1 11
{4, 6}, // 1991 11 1
},
8: {
{2, 4}, // 11 11 1991
{4, 6}, // 1991 11 11
},
}
var maybeDateNoSeparator = regexp2.MustCompile(
`^\d{4,8}$`, 0)
var maybeDateWithSeparator = regexp2.MustCompile(
`^(\d{1,4})([\s/\\_.-])(\d{1,2})\2(\d{1,4})$`, 0)
// a "date" is recognized as:
// any 3-tuple that starts or ends with a 2- or 4-digit year,
// with 2 or 0 separator chars (1.1.91 or 1191),
// maybe zero-padded (01-01-91 vs 1-1-91),
// a month between 1 and 12,
// a day between 1 and 31.
//
// note: this isn't true date parsing in that "feb 31st" is allowed,
// this doesn't check for leap years, etc.
//
// recipe:
// start with regex to find maybe-dates, then attempt to map the integers
// onto month-day-year to filter the maybe-dates into dates.
// finally, remove matches that are substrings of other matches to reduce noise.
//
// note: instead of using a lazy or greedy regex to find many dates over the full string,
// this uses a ^...$ regex against every substring of the password -- less performant but leads
// to every possible date match.
type dateMatchCandidate struct {
Day int
Month int
Year int
}
type dateMatch struct{}
func (dm dateMatch) Matches(password string) []*match.Match {
matches := []*match.Match{}
// dates without separators are between length 4 '1191' and 8 '11111991'
for i := 0; i <= len(password)-4; i++ {
for j := i + 3; j <= i+7; j++ {
if j >= len(password) {
break
}
token := password[i : j+1]
if m, err := maybeDateNoSeparator.MatchString(token); !m || err != nil {
continue
}
var candidates []*dateMatchCandidate
for _, s := range dateSplits[len(token)] {
s1, s2, s3 := token[0:s.k], token[s.k:s.l], token[s.l:]
if dmy := mapIntsToDMY(s1, s2, s3); dmy != nil {
candidates = append(candidates, dmy)
}
}
if len(candidates) == 0 {
continue
}
// at this point: different possible dmy mappings for the same i,j substring.
// match the candidate date that likely takes the fewest guesses: a year closest to 2000.
// (scoring.REFERENCE_YEAR).
//
// ie, considering '111504', prefer 11-15-04 to 1-1-1504
// (interpreting '04' as 2004)
bestCandidate := candidates[0]
minDistance := dateMatchMetric(candidates[0])
for _, candidate := range candidates[1:] {
distance := dateMatchMetric(candidate)
if distance < minDistance {
bestCandidate = candidate
minDistance = distance
}
}
matches = append(matches, &match.Match{
Pattern: "date",
Token: token,
I: i,
J: j,
Separator: "",
Year: bestCandidate.Year,
Month: bestCandidate.Month,
Day: bestCandidate.Day,
})
}
}
// dates with separators are between length 6 '1/1/91' and 10 '11/11/1991'
for i := 0; i <= len(password)-6; i++ {
for j := i + 5; j <= i+9; j++ {
if j >= len(password) {
break
}
token := password[i : j+1]
m, err := maybeDateWithSeparator.FindStringMatch(token)
if m == nil || err != nil {
continue
}
dmy := mapIntsToDMY(
m.GroupByNumber(1).String(),
m.GroupByNumber(3).String(),
m.GroupByNumber(4).String(),
)
if dmy != nil {
matches = append(matches, &match.Match{
Pattern: "date",
Token: token,
I: i,
J: j,
Separator: m.GroupByNumber(2).String(),
Year: dmy.Year,
Month: dmy.Month,
Day: dmy.Day,
})
}
}
}
// matches now contains all valid date strings in a way that is tricky to capture
// with regexes only. while thorough, it will contain some unintuitive noise:
//
// '2015_06_04', in addition to matching 2015_06_04, will also contain
// 5(!) other date matches: 15_06_04, 5_06_04, ..., even 2015 (matched as 5/1/2020)
//
// to reduce noise, remove date matches that are strict substrings of others
var filteredMatches []*match.Match
for _, m := range matches {
isSubmatch := false
for _, o := range matches {
if m == o {
continue
}
if o.I <= m.I && o.J >= m.J {
isSubmatch = true
break
}
}
if !isSubmatch {
filteredMatches = append(filteredMatches, m)
}
}
match.Sort(filteredMatches)
return filteredMatches
}
func dateMatchMetric(c *dateMatchCandidate) int {
return mathutils.Abs(c.Year - scoring.ReferenceYear)
}
func mapIntsToDMY(s1, s2, s3 string) *dateMatchCandidate {
// given a 3-tuple, discard if:
// middle int is over 31 (for all dmy formats, years are never allowed in the middle)
// middle int is zero
// any int is over the max allowable year
// any int is over two digits but under the min allowable year
// 2 ints are over 31, the max allowable day
// 2 ints are zero
// all ints are over 12, the max allowable month
i1, _ := strconv.Atoi(s1)
i2, _ := strconv.Atoi(s2)
i3, _ := strconv.Atoi(s3)
if i2 > 31 || i2 <= 0 {
return nil
}
over12 := 0
over31 := 0
under1 := 0
for _, i := range [3]int{i1, i2, i3} {
if (i > 99 && i < dateMinYear) || i > dateMaxYear {
return nil
}
if i > 31 {
over31++
}
if i > 12 {
over12++
}
if i <= 0 {
under1++
}
}
if over31 >= 2 || over12 == 3 || under1 >= 2 {
return nil
}
// first look for a four digit year: yyyy + daymonth or daymonth + yyyy
possibleYearSplits := [][3]int{
{i3, i1, i2}, // year last
{i1, i2, i3}, // year first
}
for _, split := range possibleYearSplits {
y := split[0]
if dateMinYear <= y && y <= dateMaxYear {
// for a candidate that includes a four-digit year,
// when the remaining ints don't match to a day and month,
// it is not a date.
return mapIntsToDM(split[1], split[2], y)
}
}
// given no four-digit year, two digit years are the most flexible int to match, so
// try to parse a day-month out of ints[0..1] or ints[1..0]
for _, split := range possibleYearSplits {
y := split[0]
dm := mapIntsToDM(split[1], split[2], y)
if dm != nil {
dm.Year = twoToFourDigitYear(dm.Year)
return dm
}
}
return nil
}
func mapIntsToDM(i1, i2 int, year int) *dateMatchCandidate {
if i1 <= 31 && i2 <= 12 {
return &dateMatchCandidate{
Day: i1,
Month: i2,
Year: year,
}
}
if i2 <= 31 && i1 <= 12 {
return &dateMatchCandidate{
Day: i2,
Month: i1,
Year: year,
}
}
return nil
}
func twoToFourDigitYear(year int) int {
if year > 99 {
return year
} else if year > 50 {
// 87 -> 1987
return year + 1900
} else {
// 15 -> 2015
return year + 2000
}
}
+61
View File
@@ -0,0 +1,61 @@
package matching
import (
"strings"
"github.com/trustelem/zxcvbn/match"
)
type dictionaryMatch struct {
rankedDictionaries map[string]rankedDictionnary
}
func (dm dictionaryMatch) Matches(password string) []*match.Match {
var results []*match.Match
for dictionaryName, rankedDict := range dm.rankedDictionaries {
for i := range password {
for delta := range password[i:] {
j := i + delta
word := strings.ToLower(password[i : j+1])
if val, ok := rankedDict[word]; ok {
matchDic := &match.Match{
Pattern: "dictionary",
I: i,
J: j,
Token: password[i : j+1],
MatchedWord: word,
Rank: val,
DictionaryName: dictionaryName,
}
results = append(results, matchDic)
}
}
}
}
match.Sort(results)
return results
}
func (dm dictionaryMatch) withDict(name string, d rankedDictionnary) dictionaryMatch {
rd2 := make(map[string]rankedDictionnary, len(dm.rankedDictionaries)+1)
for k, v := range dm.rankedDictionaries {
rd2[k] = v
}
rd2[name] = d
return dictionaryMatch{rankedDictionaries: rd2}
}
type rankedDictionnary map[string]int
func buildRankedDict(unrankedList []string) rankedDictionnary {
result := make(rankedDictionnary)
for i, v := range unrankedList {
result[strings.ToLower(v)] = i + 1
}
return result
}
+168
View File
@@ -0,0 +1,168 @@
package matching
import (
"bytes"
// "github.com/trustelem/zxcvbn/entropy"
"github.com/trustelem/zxcvbn/match"
"sort"
"strings"
)
type l33tMatch struct {
dm dictionaryMatch
table map[string][]string
}
func (lm l33tMatch) Matches(password string) []*match.Match {
matches := []*match.Match{}
substitutions := relevantSubtable(password, lm.table)
for _, sub := range enumerateLeetSubs(substitutions) {
if len(sub) == 0 {
break
}
subbedPassword := translate(password, sub)
for _, m := range lm.dm.Matches(subbedPassword) {
token := password[m.I : m.J+1]
if len(token) <= 1 {
// filter single-character l33t matches to reduce noise.
// otherwise '1' matches 'i', '4' matches 'a', both very common English words
continue
}
if strings.ToLower(token) == m.MatchedWord {
continue // only return the matches that return an actual substitution
}
m.Sub = make(map[string]string)
for subbed, chr := range sub {
if strings.Contains(token, subbed) {
m.Sub[subbed] = chr
}
}
m.L33t = true
m.Token = token
matches = append(matches, m)
}
}
match.Sort(matches)
return matches
}
func translate(password string, sub map[string]string) string {
var res string
for _, s := range password {
if v, ok := sub[string(s)]; ok {
res = res + v
} else {
res = res + string(s)
}
}
return res
}
type kv struct {
k string
v string
}
func dedup(subs [][]kv) [][]kv {
var res [][]kv
var b bytes.Buffer
members := make(map[string]bool)
for _, sub := range subs {
sort.SliceStable(sub, func(i, j int) bool {
return sub[i].k < sub[j].k
})
b.Reset()
for _, x := range sub {
b.WriteString(x.k)
b.WriteString(",")
b.WriteString(x.v)
}
key := b.String()
if !members[key] {
res = append(res, sub)
members[key] = true
}
}
return res
}
// enumerateLeetSubs returns the list of possible 1337 replacement dictionaries for a given password
func enumerateLeetSubs(table map[string][]string) []map[string]string {
var keys []string
for k := range table {
keys = append(keys, k)
}
sort.Strings(keys)
var subs = [][]kv{[]kv{}}
var helper func(keys []string)
helper = func(keys []string) {
if len(keys) == 0 {
return
}
firstKey := keys[0]
restKeys := keys[1:]
var nextSubs [][]kv
for _, l33tChr := range table[firstKey] {
for _, sub := range subs {
dupL33tIndex := -1
for i := 0; i < len(sub); i++ {
if sub[i].k == l33tChr {
dupL33tIndex = i
break
}
}
if dupL33tIndex == -1 {
subExtension := append(sub, kv{k: l33tChr, v: firstKey})
nextSubs = append(nextSubs, subExtension)
} else {
subAlternative := make([]kv, 0, len(sub))
subAlternative = append(subAlternative, sub[0:dupL33tIndex]...)
subAlternative = append(subAlternative, sub[dupL33tIndex+1:]...)
subAlternative = append(subAlternative, kv{k: l33tChr, v: firstKey})
// subAlternative := make([]kv, 0, len(sub))
// subAlternative = append(subAlternative, sub)
// subAlternative[dupL33tIndex] = {k:l33tChr,v:firstKey}
nextSubs = append(nextSubs, sub)
nextSubs = append(nextSubs, subAlternative)
}
}
}
subs = dedup(nextSubs)
helper(restKeys)
}
helper(keys)
var subDicts []map[string]string
for _, sub := range subs {
subDict := make(map[string]string)
for _, x := range sub {
subDict[x.k] = x.v
}
subDicts = append(subDicts, subDict)
}
return subDicts
}
func relevantSubtable(password string, table map[string][]string) map[string][]string {
passwordChars := make(map[rune]bool)
for _, chr := range password {
passwordChars[chr] = true
}
relevantSubs := make(map[string][]string)
for key, values := range table {
for _, value := range values {
if passwordChars[rune(value[0])] {
relevantSubs[key] = append(relevantSubs[key], value)
}
}
}
return relevantSubs
}
+77
View File
@@ -0,0 +1,77 @@
package matching
import (
"github.com/trustelem/zxcvbn/adjacency"
"github.com/trustelem/zxcvbn/frequency"
"github.com/trustelem/zxcvbn/match"
"regexp"
)
func Omnimatch(password string, userInputs []string) (matches []*match.Match) {
dictMatcher := defaultRankedDictionnaries.withDict("user_inputs", buildRankedDict(userInputs))
matchers := []match.Matcher{
dictMatcher,
reverseDictionnaryMatch{dm: dictMatcher},
l33tMatch{dm: dictMatcher, table: l33tTable},
spatialMatch{graphs: defaultGraphs},
repeatMatch{},
sequenceMatch{},
regexpMatch{regexes: defaultRegexpMatch},
dateMatch{},
}
for _, m := range matchers {
matches = append(matches, m.Matches(password)...)
}
match.Sort(matches)
return matches
}
var (
defaultRankedDictionnaries = loadDefaultDictionnaries()
defaultGraphs = loadDefaultAdjacencyGraphs()
defaultRegexpMatch = []struct {
Name string
Regexp *regexp.Regexp
}{
{
Name: "recent_year",
Regexp: regexp.MustCompile(`19\d\d|200\d|201\d`),
},
}
l33tTable = map[string][]string{
"a": {"4", "@"},
"b": {"8"},
"c": {"(", "{", "[", "<"},
"e": {"3"},
"g": {"6", "9"},
"i": {"1", "!", "|"},
"l": {"1", "|", "7"},
"o": {"0"},
"s": {"$", "5"},
"t": {"+", "7"},
"x": {"%"},
"z": {"2"},
}
)
func loadDefaultDictionnaries() dictionaryMatch {
rd := make(map[string]rankedDictionnary)
for n, list := range frequency.FrequencyLists {
rd[n] = buildRankedDict(list)
}
return dictionaryMatch{
rankedDictionaries: rd,
}
}
func loadDefaultAdjacencyGraphs() []*adjacency.Graph {
return []*adjacency.Graph{
adjacency.Graphs["qwerty"],
adjacency.Graphs["dvorak"],
adjacency.Graphs["keypad"],
adjacency.Graphs["mac_keypad"],
}
}
+31
View File
@@ -0,0 +1,31 @@
package matching
import (
"github.com/trustelem/zxcvbn/match"
"regexp"
)
type regexpMatch struct {
regexes []struct {
Name string
Regexp *regexp.Regexp
}
}
func (r regexpMatch) Matches(password string) []*match.Match {
var matches []*match.Match
for _, rx := range r.regexes {
for _, indexes := range rx.Regexp.FindAllStringIndex(password, -1) {
token := password[indexes[0]:indexes[1]]
matches = append(matches, &match.Match{
Pattern: "regex",
Token: token,
I: indexes[0],
J: indexes[1] - 1,
RegexName: rx.Name,
})
}
}
match.Sort(matches)
return matches
}
+85
View File
@@ -0,0 +1,85 @@
package matching
import (
"github.com/dlclark/regexp2"
"github.com/trustelem/zxcvbn/match"
"github.com/trustelem/zxcvbn/scoring"
)
type repeatMatch struct{}
var greedy = regexp2.MustCompile(`(.+)\1+`, 0)
var lazy = regexp2.MustCompile(`(.+?)\1+`, 0)
var lazyAnchored = regexp2.MustCompile(`^(.+?)\1+$`, 0)
func runeToStringIndex(index int, password string) int {
runes := 0
for i := range password {
if runes == index {
return i
}
runes++
}
//shouldn't really get here
return len(password)
}
func (repeatMatch) Matches(password string) []*match.Match {
var matches []*match.Match
lastIndex := 0
for lastIndex < len(password) {
greedyMatch, err := greedy.FindStringMatchStartingAt(password, lastIndex)
if err != nil || greedyMatch == nil {
break
}
lazyMatch, _ := lazy.FindStringMatchStartingAt(password, lastIndex)
var rmatch *regexp2.Match
var baseToken string
if greedyMatch.Captures[0].Length > lazyMatch.Captures[0].Length {
// greedy beats lazy for 'aabaab'
// greedy: [aabaab, aab]
// lazy: [aa, a]
rmatch = greedyMatch
// greedy's repeated string might itself be repeated, eg.
// aabaab in aabaabaabaab.
// run an anchored lazy match on greedy's repeated string
// to find the shortest repeated string
if m, err := lazyAnchored.FindStringMatch(rmatch.Captures[0].String()); err == nil {
baseToken = m.GroupByNumber(1).String()
}
} else {
// lazy beats greedy for 'aaaaa'
// greedy: [aaaa, aa]
// lazy: [aaaaa, a]
rmatch = lazyMatch
baseToken = rmatch.GroupByNumber(1).String()
}
// FindStringMatchStartingAt takes an index into the string (basically an offset
// into a byte array). rmatch indices will be rune offsets and so need to be converted
// to string offsets
i := runeToStringIndex(rmatch.Index, password)
j := runeToStringIndex(rmatch.Index+rmatch.Captures[0].Length-1, password)
// recursively match and score the base string
baseAnalysis := scoring.MostGuessableMatchSequence(
baseToken,
Omnimatch(baseToken, nil),
false,
)
matches = append(matches, &match.Match{
Pattern: "repeat",
I: i,
J: j,
Token: rmatch.Captures[0].String(),
BaseToken: baseToken,
BaseGuesses: baseAnalysis.Guesses,
BaseMatches: baseAnalysis.Sequence,
RepeatCount: rmatch.Captures[0].Length / len(baseToken),
})
lastIndex = j + 1
}
return matches
}
@@ -0,0 +1,38 @@
package matching
import (
"github.com/trustelem/zxcvbn/match"
)
type reverseDictionnaryMatch struct {
dm dictionaryMatch
}
func (rdm reverseDictionnaryMatch) Matches(password string) []*match.Match {
reversedPassword := reverse(password)
matches := rdm.dm.Matches(reversedPassword)
for _, m := range matches {
m.Token = reverse(m.Token)
m.Reversed = true
m.I, m.J = len(password)-1-m.J, len(password)-1-m.I
}
match.Sort(matches)
return matches
}
func reverse(input string) string {
// Get Unicode code points.
n := 0
rune := make([]rune, len(input))
for _, r := range input {
rune[n] = r
n++
}
rune = rune[0:n]
// Reverse
for i := 0; i < n/2; i++ {
rune[i], rune[n-1-i] = rune[n-1-i], rune[i]
}
// Convert back to UTF-8.
return string(rune)
}
+78
View File
@@ -0,0 +1,78 @@
package matching
import (
"regexp"
"github.com/trustelem/zxcvbn/match"
)
type sequenceMatch struct{}
const maxDelta = 5
func abs(a int) int {
if a < 0 {
return -a
}
return a
}
var reLower = regexp.MustCompile(`^[a-z]+$`)
var reUpper = regexp.MustCompile(`^[A-Z]+$`)
var reDigits = regexp.MustCompile(`^\d+$`)
func (sequenceMatch) Matches(password string) []*match.Match {
matches := []*match.Match{}
if len(password) == 1 {
return matches
}
update := func(i, j, delta int) {
absDelta := abs(delta)
if j-i > 1 || absDelta == 1 {
if absDelta > 0 && absDelta <= maxDelta {
token := password[i : j+1]
// conservatively stick with roman alphabet size.
// (this could be improved)
seqName := "unicode"
seqSpace := 26
if reLower.MatchString(token) {
seqName = "lower"
} else if reUpper.MatchString(token) {
seqName = "upper"
} else if reDigits.MatchString(token) {
seqName = "digits"
seqSpace = 10
}
matches = append(matches, &match.Match{
Pattern: "sequence",
I: i,
J: j,
Token: password[i : j+1],
SequenceName: seqName,
SequenceSpace: seqSpace,
Ascending: delta > 0,
})
}
}
}
i := 0
lastDelta := 0 // null
for k := 1; k <= len(password)-1; k++ {
delta := int(password[k]) - int(password[k-1])
if k == 1 {
lastDelta = delta
}
if delta == lastDelta {
continue
}
j := k - 1
update(i, j, lastDelta)
i = j
lastDelta = delta
}
update(i, len(password)-1, lastDelta)
return matches
}
+109
View File
@@ -0,0 +1,109 @@
package matching
import (
"strings"
"github.com/trustelem/zxcvbn/adjacency"
"github.com/trustelem/zxcvbn/match"
)
type spatialMatch struct {
graphs []*adjacency.Graph
}
func (s spatialMatch) Matches(password string) (matches []*match.Match) {
for _, graph := range s.graphs {
if graph.Graph != nil {
matches = append(matches, spatialMatchHelper(password, graph)...)
}
}
match.Sort(matches)
return matches
}
var shiftedChars = map[string]map[byte]bool{
"qwerty": stringToSet(`~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?`),
"dvorak": stringToSet(`~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?`),
}
func stringToSet(s string) map[byte]bool {
set := make(map[byte]bool)
for i := 0; i < len(s); i++ {
set[s[i]] = true
}
return set
}
func spatialMatchHelper(password string, graph *adjacency.Graph) (matches []*match.Match) {
shifted := shiftedChars[graph.Name]
i := 0
for i < len(password)-1 {
j := i + 1
lastDirection := -99
turns := 0
shiftedCount := 0
if shifted[password[i]] {
shiftedCount = 1
}
for {
prevChar := password[j-1]
found := false
foundDirection := -1
curDirection := -1
adjacents := graph.Graph[string(prevChar)]
// Consider growing pattern by one character if j hasn't gone over the edge
if j < len(password) {
curChar := password[j]
for _, adj := range adjacents {
curDirection++
if idx := strings.Index(adj, string(curChar)); idx != -1 {
found = true
foundDirection = curDirection
if idx == 1 {
// index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
// for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
shiftedCount++
}
if lastDirection != foundDirection {
// adding a turn is correct even in the initial case when last_direction is null:
// every spatial pattern starts with a turn.
turns++
lastDirection = foundDirection
}
break
}
}
}
// if the current pattern continued, extend j and try to grow again
if found {
j++
} else {
// otherwise push the pattern discovered so far, if any...
if j-i > 2 {
// don't consider length 1 or 2 chains.
matchSpc := &match.Match{
Pattern: "spatial",
I: i,
J: j - 1,
Token: password[i:j],
Graph: graph.Name,
Turns: turns,
ShiftedCount: shiftedCount,
}
matches = append(matches, matchSpc)
}
//. . . and then start a new search from the rest of the password
i = j
break
}
}
}
return matches
}