Skip to content

Commit

Permalink
feat: custom dictionaries can be in json or txt
Browse files Browse the repository at this point in the history
  • Loading branch information
camandel committed Sep 5, 2024
1 parent db3b647 commit 345461c
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 40 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The passwords will be checked on:
- l33t substitutions
- username as part of the password
- duplicated passwords
- a custom dictionary can be loaded at runtime
- a custom dictionary (json or txt) can be loaded at runtime

It supports `CSV files` exported from the most popular Password Managers and Browsers:

Expand Down Expand Up @@ -123,14 +123,14 @@ USAGE:
check-password-strength [options]
VERSION:
v0.0.6
v0.0.7
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--filename CSVFILE, -f CSVFILE Check passwords from CSVFILE
--customdict JSONFILE, -c JSONFILE Load custom dictionary from JSONFILE
--customdict FILE, -c FILE Load custom dictionary from FILE (json, txt or lst)
--interactive, -i enable interactive mode asking data from console (default: false)
--stats, -s display only statistics (default: false)
--quiet, -q return score as exit code (valid only with single password) (default: false)
Expand All @@ -141,7 +141,7 @@ GLOBAL OPTIONS:
```

## How to add custom dictionary
If you need to add your custom dictionary to the integrated ones, create one json file in the following format:
If you need to add your custom dictionary to the integrated ones, create a `json` file in the following format:

```json
{
Expand All @@ -152,6 +152,12 @@ If you need to add your custom dictionary to the integrated ones, create one jso
]
}
```
or a `txt` file like this one:
```
foo
bar
baz
```
and load it at runtime with the `-c` flag:
```
$ check-password-strength -c customdict.json -f password.csv
Expand Down
16 changes: 8 additions & 8 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func Execute() {
&cli.StringFlag{
Name: "customdict",
Aliases: []string{"c"},
Usage: "Load custom dictionary from `JSONFILE`",
Usage: "Load custom dictionary from `FILE` (json, txt or lst)",
Destination: &customDict,
},
&cli.BoolFlag{
Expand Down Expand Up @@ -85,22 +85,22 @@ func Execute() {
log.Debugf("password from pipe: %s", redactPassword(password))

if filename != "" && interactive {
return errors.New("Can not use '-f' and '-i' flags at the same time")
return errors.New("can not use '-f' and '-i' flags at the same time")
}
if filename != "" && password != "" {
return errors.New("Can not use '-f' flag and read from stdin")
return errors.New("can not use '-f' flag and read from stdin")
}
if interactive && password != "" {
return errors.New("Can not use '-i' flag and read from stdin")
return errors.New("can not use '-i' flag and read from stdin")
}
if quiet && filename != "" {
return errors.New("Flag '-q' can be used only with '-i' flag or read from stdin")
return errors.New("flag '-q' can be used only with '-i' flag or read from stdin")
}
if interactive && c.IsSet("limit") {
return errors.New("Flag '-l' can be used only with '-f' flag")
return errors.New("flag '-l' can be used only with '-f' flag")
}
if c.IsSet("limit") && (limit < 0 || limit > 4) {
return errors.New("Show only passwords with score less than value (must be between 0 and 4)")
return errors.New("show only passwords with score less than value (must be between 0 and 4)")
}
if interactive {
username, password, err = askUsernamePassword()
Expand All @@ -110,7 +110,7 @@ func Execute() {
}

if filename != "" {
return checkMultiplePassword(filename, customDict, interactive, stats, limit)
return checkMultiplePassword(filename, customDict, stats, limit)
}
return checkSinglePassword(username, password, customDict, quiet, stats)

Expand Down
56 changes: 36 additions & 20 deletions cmd/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"syscall"
Expand All @@ -19,7 +21,7 @@ import (
colorable "github.com/mattn/go-colorable"
"github.com/nbutton23/zxcvbn-go"
"github.com/olekukonko/tablewriter"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
)

type csvHeader map[string]*[]string
Expand Down Expand Up @@ -73,6 +75,8 @@ func loadCustomDict(filename string) ([]string, error) {

var customDict []string
var d jsonData
var supportedExtensions = []string{".json", ".txt", ".lst"}
var lineBreakRegExp = regexp.MustCompile(`\r?\n`)

log.Debugf("custom dict filename: %s", filename)

Expand All @@ -82,30 +86,42 @@ func loadCustomDict(filename string) ([]string, error) {
}
defer f.Close()

data, err := ioutil.ReadFile(filename)
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

err = json.Unmarshal(data, &d)
if err != nil {
return nil, err
// check file type by extension
ext := filepath.Ext(filename)
if !slices.Contains(supportedExtensions, ext) {
err := fmt.Sprintf("%s is not valid, only %v are supported", ext, supportedExtensions)
return nil, errors.New(err)
}

if len(d.Words) == 0 {
return nil, errors.New("Object 'words' is empty, custom dictionary not loaded")
if ext == ".json" {
err = json.Unmarshal(data, &d)
if err != nil {
return nil, err
}
if len(d.Words) == 0 {
return nil, errors.New("object 'words' is empty, custom dictionary not loaded")
}
customDict = append(customDict, d.Words...)
} else {
// .txt or .lst
if len(data) == 0 {
return nil, errors.New("dictionary file is empty")
}
customDict = append(customDict, lineBreakRegExp.Split(string(data), -1)...)
}

customDict = append(customDict, d.Words...)

return customDict, nil
}

func loadAllDict(filename string) ([]string, error) {
// load bundle dictionaries
assetDict, err := loadBundleDict()
if err != nil {
log.Debug("errore loading bundled dictionaries")
log.Debug("error loading bundled dictionaries")
return nil, err
}

Expand All @@ -129,7 +145,7 @@ func askUsernamePassword() (string, string, error) {
fmt.Print("Enter Username: ")
fmt.Scanln(&username)
fmt.Print("Enter Password: ")
password, err := terminal.ReadPassword(int(syscall.Stdin))
password, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()

if err != nil {
Expand All @@ -139,12 +155,12 @@ func askUsernamePassword() (string, string, error) {
return username, string(password), nil
}

func checkMultiplePassword(csvfile, jsonfile string, interactive, stats bool, limit int) error {
func checkMultiplePassword(csvfile, dictfile string, stats bool, limit int) error {

var output [][]string

// load all dictionaries
allDict, err := loadAllDict(jsonfile)
allDict, err := loadAllDict(dictfile)
if err != nil {
return err
}
Expand Down Expand Up @@ -277,7 +293,7 @@ func readCsv(filename string) ([][]string, csvHeaderOrder, error) {
}

if len(lines) == 0 {
return nil, nil, errors.New("File empty")
return nil, nil, errors.New("csv file is empty")
}
header := lines[0]

Expand Down Expand Up @@ -312,7 +328,7 @@ func checkCSVHeader(header []string) (csvHeaderOrder, error) {
for _, v := range *h {
if strings.ToLower(fieldFromFile) == v {
if _, ok := order[k]; ok {
return nil, errors.New("Header not valid")
return nil, errors.New("header not valid")
}
order[k] = position
}
Expand All @@ -321,7 +337,7 @@ func checkCSVHeader(header []string) (csvHeaderOrder, error) {
}

if len(order) != 3 {
return nil, errors.New("Header not valid")
return nil, errors.New("header not valid")
}
return order, nil
}
Expand Down Expand Up @@ -514,10 +530,10 @@ func getPwdStdin() (string, error) {
}

if info.Mode()&os.ModeCharDevice != 0 {
return "", errors.New("Pipe error on stdin")
return "", errors.New("pipe error on stdin")
}

stdinBytes, err := ioutil.ReadAll(os.Stdin)
stdinBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", err
}
Expand Down
74 changes: 67 additions & 7 deletions cmd/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,25 +141,25 @@ func TestReadCsv(t *testing.T) {
name: "Missing field in csv header",
in: []string{"Username", "Url"},
out: csvHeaderOrder{},
err: errors.New("Header not valid"),
err: errors.New("header not valid"),
},
{
name: "Duplicate fields in csv header",
in: []string{"Username", "Password", "Url", "login_username"},
out: csvHeaderOrder{},
err: errors.New("Header not valid"),
err: errors.New("header not valid"),
},
{
name: "More fields with similar name in csv header",
in: []string{"Username", "Password", "Url", "login_username"},
out: csvHeaderOrder{},
err: errors.New("Header not valid"),
err: errors.New("header not valid"),
},
{
name: "No header",
in: []string{},
out: csvHeaderOrder{},
err: errors.New("Header not valid"),
err: errors.New("header not valid"),
},
}

Expand Down Expand Up @@ -435,7 +435,7 @@ func TestReadCSV(t *testing.T) {
out: CSVData{
row: [][]string{},
order: csvHeaderOrder{},
err: errors.New("File empty"),
err: errors.New("csv file is empty"),
},
},
{
Expand Down Expand Up @@ -514,10 +514,10 @@ func TestReadJSON(t *testing.T) {
name: "Wrong json file",
in: testdir + "wrong-name.json",
out: nil,
err: errors.New("Object 'words' is empty, custom dictionary not loaded"),
err: errors.New("object 'words' is empty, custom dictionary not loaded"),
},
{
name: "Empty csv file",
name: "Empty json file",
in: testdir + "empty.json",
out: nil,
err: errors.New("unexpected end of JSON input"),
Expand Down Expand Up @@ -555,3 +555,63 @@ func TestReadJSON(t *testing.T) {
})
}
}

func TestReadText(t *testing.T) {

testdir := fmt.Sprintf("..%ctest%c", os.PathSeparator, os.PathSeparator)
filenotfound := fmt.Sprintf("open %snot-exists.txt: no such file or directory", testdir)
if runtime.GOOS == "windows" {
filenotfound = fmt.Sprintf("open %snot-exists.txt: The system cannot find the file specified.", testdir)
}

tests := []struct {
name string
in string
out []string
err error
}{
{
name: "Correct txt file",
in: testdir + "words.txt",
out: []string{"polygon-approve-entire-coexist", "Gsg#H4k#*966Dx"},
err: nil,
},
{
name: "Empty txt file",
in: testdir + "empty.txt",
out: nil,
err: errors.New("dictionary file is empty"),
},
{
name: "Not existing txt file",
in: testdir + "not-exists.txt",
out: nil,
err: errors.New(filenotfound),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

out, err := loadCustomDict(tt.in)

if err != nil {
if tt.err == nil {
t.Fatalf("got error: %v, want nil error", err)
}
if err.Error() != tt.err.Error() {
t.Fatalf("got error: %v, want error: %v", err, tt.err)
}
return
}

if tt.err != nil {
t.Fatalf("got nil error, want error: %v", tt.err)
}

if !reflect.DeepEqual(tt.out, out) {
t.Fatalf("got %v, expected %v", out, tt.out)
}
})
}
}
2 changes: 1 addition & 1 deletion cmd/version.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package cmd

// Version to display
var Version = "v0.0.6"
var Version = "v0.0.7"
Empty file added test/empty.txt
Empty file.
2 changes: 2 additions & 0 deletions test/words.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
polygon-approve-entire-coexist
Gsg#H4k#*966Dx

0 comments on commit 345461c

Please sign in to comment.