Skip to content

Commit 89091cb

Browse files
committed
feat: check duplicated passwords
1 parent b1a4719 commit 89091cb

File tree

6 files changed

+143
-34
lines changed

6 files changed

+143
-34
lines changed

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The passwords will be checked on:
1818
- common keyboards sequences
1919
- l33t substitutions
2020
- username as part of the password
21+
- duplicated passwords
2122
- a custom dictionary can be loaded at runtime
2223

2324
It supports `CSV files` exported from the most popular Password Managers and Browsers:
@@ -36,16 +37,16 @@ To check only one password at a time it can be used in `interactive` mode (passw
3637
$ check-password-strength -i
3738
Enter Username: username
3839
Enter Password:
39-
URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK
40-
------+----------+----------+------------------+--------------------------
41-
| username | p******d | 0 - Really bad | instant
40+
URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK | ALREADY USED
41+
------+----------+----------+------------------+-------------------------+---------------
42+
| username | p******d | 0 - Really bad | instant |
4243
```
4344
or reading from `stdin`:
4445
```
4546
$ echo $PASSWORD | check-password-strength
46-
URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK
47-
------+----------+----------+------------------+--------------------------
48-
| | p******j | 4 - Strong | centuries
47+
URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK | ALREADY USED
48+
------+----------+----------+------------------+-------------------------+---------------
49+
| | p******j | 4 - Strong | centuries |
4950
```
5051
If you need to use it in a script you can use `-q` flag. It will display nothing on stdout and the `exit code` will contain the password score (it works only with single password):
5152
```
@@ -116,7 +117,7 @@ USAGE:
116117
check-password-strength [options]
117118
118119
VERSION:
119-
v0.0.2
120+
v0.0.3
120121
121122
COMMANDS:
122123
help, h Shows a list of commands or help for one command
@@ -125,6 +126,7 @@ GLOBAL OPTIONS:
125126
--filename CSVFILE, -f CSVFILE Check passwords from CSVFILE
126127
--customdict JSONFILE, -c JSONFILE Load custom dictionary from JSONFILE
127128
--interactive, -i enable interactive mode asking data from console (default: false)
129+
--stats, -s display only statistics (default: false)
128130
--quiet, -q return score as exit code (valid only with single password) (default: false)
129131
--debug, -d show debug logs (default: false)
130132
--help, -h show help (default: false)

assets/img/screenshot.jpg

35.9 KB
Loading

cmd/core.go

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package cmd
22

33
import (
4+
"crypto/rand"
5+
"crypto/sha512"
46
"encoding/csv"
57
"encoding/json"
68
"errors"
@@ -40,6 +42,8 @@ type statistics struct {
4042
DuplicateCount int
4143
}
4244

45+
type duplicates map[string][]int
46+
4347
func loadBundleDict() ([]string, error) {
4448

4549
var assetDict []string
@@ -145,32 +149,58 @@ func checkMultiplePassword(csvfile, jsonfile string, interactive, stats bool) er
145149

146150
// initialize statistics
147151
stat := initStats(len(allDict))
152+
duplicate := duplicates{}
153+
154+
// generate seed
155+
seed, err := generateSeed()
156+
if err != nil {
157+
return err
158+
}
148159

149160
lines, order, err := readCsv(csvfile)
150161
if err != nil {
151162
return err
152163
}
153164
log.Debugf("order: %v\n", order)
154165

155-
for _, line := range lines {
166+
for n, line := range lines {
156167
data := csvRow{
157168
URL: line[order["url"]],
158169
Username: line[order["username"]],
159170
Password: line[order["password"]],
160171
}
161172

162173
passwordStength := zxcvbn.PasswordStrength(data.Password, append(allDict, data.Username))
174+
175+
hash := generateHash(seed, data.Password)
176+
177+
// check if password is already used
178+
duplicate[hash] = append(duplicate[hash], n)
179+
163180
data.Password = redactPassword(data.Password)
164181
output = append(output, []string{data.URL, data.Username, data.Password,
165182
fmt.Sprintf("%d", passwordStength.Score),
166183
fmt.Sprintf("%.2f", passwordStength.Entropy),
167-
passwordStength.CrackTimeDisplay})
184+
passwordStength.CrackTimeDisplay,
185+
"",
186+
})
168187

169188
// update statistics
170189
stat.ScoreCount[passwordStength.Score] = stat.ScoreCount[passwordStength.Score] + 1
171190
stat.TotCount = stat.TotCount + 1
172191
}
173192

193+
// add hash to identify duplicated passwords
194+
for h, v := range duplicate {
195+
if len(v) > 1 {
196+
for _, i := range v {
197+
output[i][6] = h
198+
stat.DuplicateCount = stat.DuplicateCount + 1
199+
}
200+
}
201+
}
202+
203+
// show statistics report
174204
if stats {
175205
showStats(stat, colorable.NewColorableStdout())
176206
} else {
@@ -207,7 +237,9 @@ func checkSinglePassword(username, password, jsonfile string, quiet, stats bool)
207237
output = append(output, []string{"", username, password,
208238
fmt.Sprintf("%d", passwordStength.Score),
209239
fmt.Sprintf("%.2f", passwordStength.Entropy),
210-
passwordStength.CrackTimeDisplay})
240+
passwordStength.CrackTimeDisplay,
241+
"",
242+
})
211243

212244
if stats {
213245
showStats(stat, colorable.NewColorableStdout())
@@ -288,6 +320,20 @@ func redactPassword(p string) string {
288320
return fmt.Sprintf("%s******%s", p[0:1], p[len(p)-1:])
289321
}
290322

323+
func generateSeed() ([]byte, error) {
324+
buf := make([]byte, 16)
325+
_, err := rand.Read(buf)
326+
if err != nil {
327+
return nil, err
328+
}
329+
return buf, nil
330+
}
331+
332+
func generateHash(seed []byte, password string) string {
333+
sha1 := sha512.Sum512(append(seed, []byte(password)...))
334+
return fmt.Sprintf("%x", sha1)[:8]
335+
}
336+
291337
func initStats(c int) statistics {
292338
return statistics{
293339
TotCount: 0,
@@ -300,7 +346,7 @@ func initStats(c int) statistics {
300346
func showTable(data [][]string, w io.Writer) {
301347
// writer is a s parameter to pass buffer during tests
302348
table := tablewriter.NewWriter(w)
303-
table.SetHeader([]string{"URL", "Username", "Password", "Score (0-4)", "Estimated time to crack"})
349+
table.SetHeader([]string{"URL", "Username", "Password", "Score (0-4)", "Estimated time to crack", "Already used"})
304350
table.SetBorder(false)
305351
table.SetAlignment(tablewriter.ALIGN_LEFT)
306352

@@ -326,7 +372,7 @@ func showTable(data [][]string, w io.Writer) {
326372
scoreColor = tablewriter.BgGreenColor
327373
}
328374

329-
colorRow := []string{row[0], row[1], row[2], score, row[5]}
375+
colorRow := []string{row[0], row[1], row[2], score, row[5], row[6]}
330376
table.Rich(colorRow, []tablewriter.Colors{nil, nil, nil, {scoreColor}})
331377

332378
}
@@ -339,13 +385,12 @@ func showStats(stat statistics, w io.Writer) {
339385
table := tablewriter.NewWriter(w)
340386
table.SetHeader([]string{"Description", "Count"})
341387
table.SetBorder(false)
342-
//table.SetAlignment(tablewriter.ALIGN_LEFT)
343388

344389
data := [][]string{
345390
{"Password checked", fmt.Sprintf("%d", stat.TotCount)},
346391
{"Words in dictionaries", fmt.Sprintf("%d", stat.WordsCount)},
392+
{"Duplicated passwords", fmt.Sprintf("%d", stat.DuplicateCount)},
347393
{"Really bad passwords", fmt.Sprintf("%d", stat.ScoreCount[0])},
348-
//{"Duplicated passwords", fmt.Sprintf("%d", stat.DuplicateCount)},
349394
{"Bad passwords", fmt.Sprintf("%d", stat.ScoreCount[1])},
350395
{"Weak passwords", fmt.Sprintf("%d", stat.ScoreCount[2])},
351396
{"Good passwords", fmt.Sprintf("%d", stat.ScoreCount[3])},

cmd/core_test.go

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,47 @@ func TestRedactPassword(t *testing.T) {
4646
})
4747
}
4848
}
49+
50+
func TestGenerateHash(t *testing.T) {
51+
52+
tests := []struct {
53+
name string
54+
seed []byte
55+
password string
56+
out string
57+
}{
58+
{
59+
name: "first password",
60+
seed: []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
61+
password: "password",
62+
out: "c7f42603",
63+
},
64+
{
65+
name: "same seed and different password",
66+
seed: []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
67+
password: "otherpassword",
68+
out: "1413a414",
69+
},
70+
{
71+
name: "same password but different seed",
72+
seed: []byte{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
73+
password: "password",
74+
out: "4b124f90",
75+
},
76+
}
77+
78+
for _, tt := range tests {
79+
t.Run(tt.name, func(t *testing.T) {
80+
h := generateHash(tt.seed, tt.password)
81+
82+
if !reflect.DeepEqual(tt.out, h) {
83+
t.Fatalf("got %s, expected %s", h, tt.out)
84+
}
85+
86+
})
87+
}
88+
}
89+
4990
func TestReadCsv(t *testing.T) {
5091

5192
tests := []struct {
@@ -149,29 +190,47 @@ func TestShowTable(t *testing.T) {
149190
out string
150191
}{
151192
{
152-
name: "One row",
153-
in: [][]string{{"url1", "user1", "p******1", "1", "5.00", "instant"}},
154-
out: ` URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK
155-
-------+----------+----------+------------------+--------------------------
156-
url1 | user1 | p******1 | [101m 1 - Bad [0m | instant
193+
name: "Single row",
194+
in: [][]string{{"url1", "user1", "p******1", "1", "5.00", "instant", ""}},
195+
out: ` URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK | ALREADY USED
196+
-------+----------+----------+------------------+-------------------------+---------------
197+
url1 | user1 | p******1 | [101m 1 - Bad [0m | instant |
157198
`,
158199
},
159200
{
160-
name: "Five rows with different colors",
201+
name: "Multiple rows no duplicates",
161202
in: [][]string{
162-
{"url0", "user0", "p******0", "0", "5.00", "instant"},
163-
{"url1", "user1", "p******1", "1", "5.00", "instant"},
164-
{"url2", "user2", "p******2", "2", "5.00", "instant"},
165-
{"url3", "user3", "p******3", "3", "5.00", "instant"},
166-
{"url4", "user4", "p******4", "4", "5.00", "instant"},
203+
{"url0", "user0", "p******0", "0", "5.00", "instant", ""},
204+
{"url1", "user1", "p******1", "1", "5.00", "instant", ""},
205+
{"url2", "user2", "p******2", "2", "5.00", "instant", ""},
206+
{"url3", "user3", "p******3", "3", "5.00", "instant", ""},
207+
{"url4", "user4", "p******4", "4", "5.00", "instant", ""},
167208
},
168-
out: ` URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK
169-
-------+----------+----------+------------------+--------------------------
170-
url0 | user0 | p******0 |  0 - Really bad  | instant
171-
url1 | user1 | p******1 |  1 - Bad  | instant
172-
url2 | user2 | p******2 |  2 - Weak  | instant
173-
url3 | user3 | p******3 |  3 - Good  | instant
174-
url4 | user4 | p******4 |  4 - Strong  | instant
209+
out: ` URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK | ALREADY USED
210+
-------+----------+----------+------------------+-------------------------+---------------
211+
url0 | user0 | p******0 |  0 - Really bad  | instant |
212+
url1 | user1 | p******1 |  1 - Bad  | instant |
213+
url2 | user2 | p******2 |  2 - Weak  | instant |
214+
url3 | user3 | p******3 |  3 - Good  | instant |
215+
url4 | user4 | p******4 |  4 - Strong  | instant |
216+
`,
217+
},
218+
{
219+
name: "Multiple rows with duplicates",
220+
in: [][]string{
221+
{"url0", "user0", "p******0", "0", "5.00", "instant", "aaaaaaaa"},
222+
{"url1", "user1", "p******1", "1", "5.00", "instant", "bbbbbbbb"},
223+
{"url2", "user2", "p******0", "0", "5.00", "instant", "aaaaaaaa"},
224+
{"url3", "user3", "p******3", "3", "5.00", "instant", ""},
225+
{"url4", "user4", "p******1", "1", "5.00", "instant", "bbbbbbbb"},
226+
},
227+
out: ` URL | USERNAME | PASSWORD | SCORE (0-4) | ESTIMATED TIME TO CRACK | ALREADY USED
228+
-------+----------+----------+------------------+-------------------------+---------------
229+
url0 | user0 | p******0 |  0 - Really bad  | instant | aaaaaaaa
230+
url1 | user1 | p******1 |  1 - Bad  | instant | bbbbbbbb
231+
url2 | user2 | p******0 |  0 - Really bad  | instant | aaaaaaaa
232+
url3 | user3 | p******3 |  3 - Good  | instant |
233+
url4 | user4 | p******1 |  1 - Bad  | instant | bbbbbbbb
175234
`,
176235
},
177236
}
@@ -209,6 +268,7 @@ func TestShowStats(t *testing.T) {
209268
------------------------+--------
210269
Password checked | 1
211270
Words in dictionaries | 59824
271+
Duplicated passwords | 0
212272
Really bad passwords | 0
213273
Bad passwords | 0
214274
Weak passwords | 0
@@ -222,12 +282,13 @@ func TestShowStats(t *testing.T) {
222282
TotCount: 9,
223283
WordsCount: 59824,
224284
ScoreCount: []int{3, 1, 2, 1, 2},
225-
DuplicateCount: 0,
285+
DuplicateCount: 2,
226286
},
227287
out: ` DESCRIPTION | COUNT
228288
------------------------+--------
229289
Password checked | 9
230290
Words in dictionaries | 59824
291+
Duplicated passwords | 2
231292
Really bad passwords | 3
232293
Bad passwords | 1
233294
Weak passwords | 2

cmd/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package cmd
22

33
// Version to display
4-
var Version = "v0.0.2"
4+
var Version = "v0.0.3"

test/example.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
"www.example.com","user3","house-new-cow"
99
"www.example.com","user3","polygon-approve-entire-coexist"
1010
"www.example.com","user2","Gsg#H4k#*966Dx"
11+
"www.example.com","user2","polygon-approve-entire-coexist"

0 commit comments

Comments
 (0)