-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpasswordsim.go
143 lines (126 loc) · 3.79 KB
/
passwordsim.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// Package passwordsim lets you search for passwords similar to
// your specified password in any passwords dataset.
// The similarity metric used is the Damerau-Levenshtein distance.
package passwordsim
import (
"bufio"
"fmt"
"math"
"os"
"runtime"
"sort"
"strconv"
"sync"
"github.com/fatih/color"
tdl "github.com/lmas/Damerau-Levenshtein"
)
type results []result
func (u results) Len() int {
return len(u)
}
func (u results) Swap(i, j int) {
u[i], u[j] = u[j], u[i]
}
func (u results) Less(i, j int) bool {
if u[i].score < u[j].score {
return true
}
if u[i].score == u[j].score {
return u[i].password < u[j].password
}
return false
}
type result struct {
password string
score float64
}
// bufferSize is buffer capacity of message channel
//
// CPU-bound receivers are slower at calculating distances
// than I/O sender at sending out passwords, so bufferSize
// has to be high enough.
const bufferSize = 512
// estimatedMaxPasswordLength sets an optimal initial size for package tdl's distance calculator
// to reduce chances of having to resize (i.e. most passwords do not exceed 64 characters).
const estimatedMaxPasswordLength = 64
// CheckPasswords scans passwords dataset for passwords similar to passwordToCheck,
// where the normalised Damerau-Levenshtein distance is no higher than threshold, and writes the passwords and their
// respective normalised Damerau-Levenshtein distance scores to file output.
func CheckPasswords(passwords string, output string, passwordToCheck string, threshold float64) {
var numReceivers = runtime.NumCPU()
var wg sync.WaitGroup
var similarPasswords sync.Map
if threshold < 0 {
threshold = 0
}
if threshold > 1 {
threshold = 1
}
message := make(chan string, bufferSize)
go send(message, passwords)
for i := 0; i < numReceivers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
t := tdl.New(estimatedMaxPasswordLength)
for password := range message {
score := t.Distance(password, passwordToCheck)
normalisedScore := float64(score) / float64(math.Max(float64(len(password)), float64(len(passwordToCheck))))
if normalisedScore <= threshold {
// Ensure each map key (password) is written to only once
similarPasswords.LoadOrStore(password, normalisedScore)
}
}
}()
}
wg.Wait()
writeFile, err := os.OpenFile(output, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
color.HiRed("Error: %s", err)
os.Exit(1)
}
defer writeFile.Close()
res := results{}
similarPasswords.Range(func(k, v interface{}) bool {
res = append(res, result{password: k.(string), score: v.(float64)})
return true
})
sort.Sort(res)
save(res, writeFile)
var passwordCount string
var clr *color.Color
if len(res) == 0 {
passwordCount = "No similar passwords found"
clr = color.New(color.Bold, color.FgHiGreen)
} else if len(res) == 1 {
passwordCount = "1 similar password found"
clr = color.New(color.Bold, color.FgRed)
} else {
passwordCount = strconv.Itoa(len(res)) + " similar passwords found"
clr = color.New(color.Bold, color.FgRed)
}
clr.Print(passwordCount + " in '" + passwords +
"'. Threshold: " + strconv.FormatFloat(threshold, 'f', 2, 64) + "\n")
color.White("Results saved to file '%s'.\n", output)
}
// save saves passwords and their normalised Damerau-Levenshtein distance scores to file opened at writeFile
func save(res results, writeFile *os.File) {
for _, r := range res {
fmt.Fprintln(writeFile, r.password, r.score)
}
}
// send streams passwords from passwords dataset to channel ch
func send(ch chan<- string, passwords string) {
readFile, err := os.Open(passwords)
if err != nil {
color.HiRed("Error: %s", err)
os.Exit(1)
}
defer readFile.Close()
reader := bufio.NewScanner(readFile)
color.HiYellow("Searching for similar passwords...")
for reader.Scan() {
ch <- reader.Text()
}
close(ch)
}