pax_global_header00006660000000000000000000000064133661530300014512gustar00rootroot0000000000000052 comment=ee5032c9b35b57a43c16bfbab8196051a5bcb1d9 golang-github-muesli-crunchy-0.2/000077500000000000000000000000001336615303000170475ustar00rootroot00000000000000golang-github-muesli-crunchy-0.2/.gitignore000066400000000000000000000004231336615303000210360ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ golang-github-muesli-crunchy-0.2/.travis.yml000066400000000000000000000014001336615303000211530ustar00rootroot00000000000000language: go os: - linux go: - 1.2.x - 1.3.x - 1.4 - 1.5.x - 1.6.x - 1.7.x - 1.8.x - 1.9.x - 1.10.x - 1.11.x - tip matrix: exclude: - os: osx go: 1.2.x - os: osx go: 1.3.x - os: osx go: 1.4 - os: osx go: 1.5.x - os: osx go: 1.6.x sudo: required before_install: - if [[ $TRAVIS_GO_VERSION == 1.9* ]]; then go get github.com/axw/gocov/gocov github.com/mattn/goveralls; fi - sudo apt-get update -qq - sudo apt-get install -y cracklib-runtime script: - go test -v -tags ci ./... - if [[ $TRAVIS_GO_VERSION == 1.9* ]]; then $GOPATH/bin/goveralls -service=travis-ci; fi notifications: email: on_success: change on_failure: always golang-github-muesli-crunchy-0.2/LICENSE000066400000000000000000000020671336615303000200610ustar00rootroot00000000000000MIT License Copyright (c) 2017 Christian Muehlhaeuser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-muesli-crunchy-0.2/README.md000066400000000000000000000070221336615303000203270ustar00rootroot00000000000000crunchy ======= Finds common flaws in passwords. Like cracklib, but written in Go. Detects: - Empty passwords: `ErrEmpty` - Too short passwords: `ErrTooShort` - Too few different characters, like "aabbccdd": `ErrTooFewChars` - Systematic passwords, like "abcdefgh" or "87654321": `ErrTooSystematic` - Passwords from a dictionary / wordlist: `ErrDictionary` - Mangled / reversed passwords, like "p@ssw0rd" or "drowssap": `ErrMangledDictionary` - Hashed dictionary words, like "5f4dcc3b5aa765d61d8327deb882cf99" (the md5sum of "password"): `ErrHashedDictionary` Your system dictionaries from `/usr/share/dict` will be indexed. If no dictionaries were found, crunchy only relies on the regular sanity checks (`ErrEmpty`, `ErrTooShort`, `ErrTooFewChars` and `ErrTooSystematic`). On Ubuntu it is recommended to install the wordlists distributed with `cracklib-runtime`, on macOS you can install `cracklib-words` from brew. You could also install various other language dictionaries or wordlists, e.g. from skullsecurity.org. crunchy uses the WagnerFischer algorithm to find mangled passwords in your dictionaries. ## Installation Make sure you have a working Go environment (Go 1.2 or higher is required). See the [install instructions](http://golang.org/doc/install.html). To install crunchy, simply run: go get github.com/muesli/crunchy To compile it from source: cd $GOPATH/src/github.com/muesli/crunchy go get -u -v go build && go test -v ## Example ```go package main import ( "github.com/muesli/crunchy" "fmt" ) func main() { validator := crunchy.NewValidator() err := validator.Check("12345678") if err != nil { fmt.Printf("The password '12345678' is considered unsafe: %v\n", err) } err = validator.Check("p@ssw0rd") if dicterr, ok := err.(*crunchy.DictionaryError); ok { fmt.Printf("The password 'p@ssw0rd' is too similar to dictionary word '%s' (distance %d)\n", dicterr.Word, dicterr.Distance) } err = validator.Check("d1924ce3d0510b2b2b4604c99453e2e1") if err == nil { // Password is considered acceptable ... } } ``` ## Custom Options ```go package main import ( "github.com/muesli/crunchy" "fmt" ) func main() { validator := crunchy.NewValidatorWithOpts(crunchy.Options{ // MinLength is the minimum length required for a valid password // (must be >= 1, default is 8) MinLength: 10, // MinDiff is the minimum amount of unique characters required for a valid password // (must be >= 1, default is 5) MinDiff: 8, // MinDist is the minimum WagnerFischer distance for mangled password dictionary lookups // (must be >= 0, default is 3) MinDist: 4, // Hashers will be used to find hashed passwords in dictionaries Hashers: []hash.Hash{md5.New(), sha1.New(), sha256.New(), sha512.New()}, // DictionaryPath contains all the dictionaries that will be parsed // (default is /usr/share/dict) DictionaryPath: "/var/my/own/dicts", }) ... } ``` ## Development [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/muesli/crunchy) [![Build Status](https://travis-ci.org/muesli/crunchy.svg?branch=master)](https://travis-ci.org/muesli/crunchy) [![Coverage Status](https://coveralls.io/repos/github/muesli/crunchy/badge.svg?branch=master)](https://coveralls.io/github/muesli/crunchy?branch=master) [![Go ReportCard](http://goreportcard.com/badge/muesli/crunchy)](http://goreportcard.com/report/muesli/crunchy) golang-github-muesli-crunchy-0.2/crunchy.go000066400000000000000000000133671336615303000210630ustar00rootroot00000000000000/* * crunchy - find common flaws in passwords * Copyright (c) 2017-2018, Christian Muehlhaeuser * * For license see LICENSE */ package crunchy import ( "hash" "io/ioutil" "path/filepath" "strings" "sync" "unicode" "unicode/utf8" "github.com/xrash/smetrics" ) // Validator is used to setup a new password validator with options and dictionaries type Validator struct { options Options once sync.Once wordsMaxLen int // length of longest word in dictionaries words map[string]struct{} // map to index parsed dictionaries hashedWords map[string]string // maps hash-sum to password } // Options contains all the settings for a Validator type Options struct { // MinLength is the minimum length required for a valid password (>=1, default is 8) MinLength int // MinDiff is the minimum amount of unique characters required for a valid password (>=1, default is 5) MinDiff int // MinDist is the minimum WagnerFischer distance for mangled password dictionary lookups (>=0, default is 3) MinDist int // Hashers will be used to find hashed passwords in dictionaries Hashers []hash.Hash // DictionaryPath contains all the dictionaries that will be parsed (default is /usr/share/dict) DictionaryPath string } // NewValidator returns a new password validator with default settings func NewValidator() *Validator { return NewValidatorWithOpts(Options{ MinDist: -1, DictionaryPath: "/usr/share/dict", }) } // NewValidatorWithOpts returns a new password validator with custom settings func NewValidatorWithOpts(options Options) *Validator { if options.MinLength <= 0 { options.MinLength = 8 } if options.MinDiff <= 0 { options.MinDiff = 5 } if options.MinDist < 0 { options.MinDist = 3 } return &Validator{ options: options, words: make(map[string]struct{}), hashedWords: make(map[string]string), } } // indexDictionaries parses dictionaries/wordlists func (v *Validator) indexDictionaries() { if v.options.DictionaryPath == "" { return } dicts, err := filepath.Glob(filepath.Join(v.options.DictionaryPath, "*")) if err != nil { return } for _, dict := range dicts { buf, err := ioutil.ReadFile(dict) if err != nil { continue } for _, word := range strings.Split(string(buf), "\n") { nw := normalize(word) nwlen := len(nw) if nwlen > v.wordsMaxLen { v.wordsMaxLen = nwlen } // if a word is smaller than the minimum length minus the minimum distance // then any collisons would have been rejected by pre-dictionary checks if nwlen >= v.options.MinLength-v.options.MinDist { v.words[nw] = struct{}{} } for _, hasher := range v.options.Hashers { v.hashedWords[hashsum(nw, hasher)] = nw } } } } // foundInDictionaries returns whether a (mangled) string exists in the indexed dictionaries func (v *Validator) foundInDictionaries(s string) error { v.once.Do(v.indexDictionaries) pw := normalize(s) // normalized password revpw := reverse(pw) // reversed password pwlen := len(pw) // let's check perfect matches first // we can skip this if the pw is longer than the longest word in our dictionary if pwlen <= v.wordsMaxLen { if _, ok := v.words[pw]; ok { return &DictionaryError{ErrDictionary, pw, 0} } if _, ok := v.words[revpw]; ok { return &DictionaryError{ErrMangledDictionary, revpw, 0} } } // find hashed dictionary entries if word, ok := v.hashedWords[pw]; ok { return &HashedDictionaryError{ErrHashedDictionary, word} } // find mangled / reversed passwords // we can skip this if the pw is longer than the longest word plus our minimum distance if pwlen <= v.wordsMaxLen+v.options.MinDist { for word := range v.words { if dist := smetrics.WagnerFischer(word, pw, 1, 1, 1); dist <= v.options.MinDist { return &DictionaryError{ErrMangledDictionary, word, dist} } if dist := smetrics.WagnerFischer(word, revpw, 1, 1, 1); dist <= v.options.MinDist { return &DictionaryError{ErrMangledDictionary, word, dist} } } } return nil } // Check validates a password for common flaws // It returns nil if the password is considered acceptable. func (v *Validator) Check(password string) error { if strings.TrimSpace(password) == "" { return ErrEmpty } if len(password) < v.options.MinLength { return ErrTooShort } if countUniqueChars(password) < v.options.MinDiff { return ErrTooFewChars } // Inspired by cracklib maxrepeat := 3.0 + (0.09 * float64(len(password))) if countSystematicChars(password) > int(maxrepeat) { return ErrTooSystematic } return v.foundInDictionaries(password) } // Rate grades a password's strength from 0 (weak) to 100 (strong). func (v *Validator) Rate(password string) (uint, error) { if err := v.Check(password); err != nil { return 0, err } l := len(password) systematics := countSystematicChars(password) repeats := l - countUniqueChars(password) var letters, uLetters, numbers, symbols int for len(password) > 0 { r, size := utf8.DecodeRuneInString(password) password = password[size:] if unicode.IsLetter(r) { if unicode.IsUpper(r) { uLetters++ } else { letters++ } } else if unicode.IsNumber(r) { numbers++ } else { symbols++ } } // ADD: number of characters n := l * 4 // ADD: uppercase letters if uLetters > 0 { n += (l - uLetters) * 2 } // ADD: lowercase letters if letters > 0 { n += (l - letters) * 2 } // ADD: numbers n += numbers * 4 // ADD: symbols n += symbols * 6 // REM: letters only if l == letters+uLetters { n -= letters + uLetters } // REM: numbers only if l == numbers { n -= numbers * 4 } // REM: repeat characters (case insensitive) n -= repeats * 4 // REM: systematic characters n -= systematics * 3 if n < 0 { n = 0 } else if n > 100 { n = 100 } return uint(n), nil } golang-github-muesli-crunchy-0.2/crunchy_test.go000066400000000000000000000051171336615303000221140ustar00rootroot00000000000000/* * crunchy - find common flaws in passwords * Copyright (c) 2017-2018, Christian Muehlhaeuser * * For license see LICENSE */ package crunchy import ( "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "hash" "strconv" "testing" ) var ( pws = []struct { pw string expected error rating uint }{ // valid passwords {"d1924ce3d0510b2b2b4604c99453e2e1", nil, 100}, {"aCgIknPv", nil, 40}, {"1347902586", nil, 37}, {"aEc!1Edek?", nil, 71}, {"aEc!1Edek?f", nil, 77}, {"aEc!1Edek?f_", nil, 91}, {"aEc!1Edek?f_0", nil, 100}, // invalid passwords {"", ErrEmpty, 0}, {" ", ErrEmpty, 0}, {"crunchy", ErrTooShort, 0}, {"aaaaaaaa", ErrTooFewChars, 0}, {"aabbccdd", ErrTooFewChars, 0}, {"aAbBcCdD", ErrTooFewChars, 0}, {"12345678", ErrTooSystematic, 0}, {"87654321", ErrTooSystematic, 0}, {"abcdefgh", ErrTooSystematic, 0}, {"hgfedcba", ErrTooSystematic, 0}, {"password", ErrDictionary, 0}, {"intoxicate", ErrDictionary, 0}, {"p@ssw0rd", ErrMangledDictionary, 0}, // dictionary with mangling {"!pass@word?", ErrMangledDictionary, 0}, // dictionary with mangling {"drowssap", ErrMangledDictionary, 0}, // reversed dictionary {"?drow@ssap!", ErrMangledDictionary, 0}, // reversed dictionary with mangling // md5 dictionary lookup {"5f4dcc3b5aa765d61d8327deb882cf99", ErrHashedDictionary, 0}, // sha1 dictionary lookup {"5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", ErrHashedDictionary, 0}, // sha256 dictionary lookup {"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", ErrHashedDictionary, 0}, // sha512 dictionary lookup {"b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86", ErrHashedDictionary, 0}, } ) func TestValidatePassword(t *testing.T) { v := NewValidatorWithOpts(Options{ MinDist: -1, Hashers: []hash.Hash{md5.New(), sha1.New(), sha256.New(), sha512.New()}, DictionaryPath: "/usr/share/dict", }) for _, pw := range pws { r, err := v.Rate(pw.pw) if dicterr, ok := err.(*DictionaryError); ok { err = dicterr.Err } else if hasherr, ok := err.(*HashedDictionaryError); ok { err = hasherr.Err } if r != pw.rating { t.Errorf("Expected rating %d for password '%s', got %d", pw.rating, pw.pw, r) } if err != pw.expected { t.Errorf("Expected %v for password '%s', got %v", pw.expected, pw.pw, err) } } } func BenchmarkValidatePassword(b *testing.B) { v := NewValidator() s := hashsum(strconv.Itoa(b.N), md5.New()) for n := 0; n < b.N; n++ { v.Check(s) } } golang-github-muesli-crunchy-0.2/errors.go000066400000000000000000000033401336615303000207120ustar00rootroot00000000000000/* * crunchy - find common flaws in passwords * Copyright (c) 2017, Christian Muehlhaeuser * * For license see LICENSE */ package crunchy import ( "errors" ) // DictionaryError wraps an ErrMangledDictionary with contextual information type DictionaryError struct { Err error Word string Distance int } // HashedDictionaryError wraps an ErrHashedDictionary with contextual information type HashedDictionaryError struct { Err error Word string } func (e *DictionaryError) Error() string { return e.Err.Error() } func (e *HashedDictionaryError) Error() string { return e.Err.Error() } var ( // ErrEmpty gets returned when the password is empty or all whitespace ErrEmpty = errors.New("Password is empty or all whitespace") // ErrTooShort gets returned when the password is not long enough ErrTooShort = errors.New("Password is too short") // ErrTooFewChars gets returned when the password does not contain enough unique characters ErrTooFewChars = errors.New("Password does not contain enough different/unique characters") // ErrTooSystematic gets returned when the password is too systematic (e.g. 123456, abcdef) ErrTooSystematic = errors.New("Password is too systematic") // ErrDictionary gets returned when the password is found in a dictionary ErrDictionary = errors.New("Password is too common / from a dictionary") // ErrMangledDictionary gets returned when the password is mangled, but found in a dictionary ErrMangledDictionary = errors.New("Password is mangled, but too common / from a dictionary") // ErrHashedDictionary gets returned when the password is hashed, but found in a dictionary ErrHashedDictionary = errors.New("Password is hashed, but too common / from a dictionary") ) golang-github-muesli-crunchy-0.2/stringutils.go000066400000000000000000000025161336615303000217710ustar00rootroot00000000000000/* * crunchy - find common flaws in passwords * Copyright (c) 2017, Christian Muehlhaeuser * * For license see LICENSE */ package crunchy import ( "encoding/hex" "hash" "strings" "unicode" "unicode/utf8" ) // countUniqueChars returns the amount of unique runes in a string func countUniqueChars(s string) int { m := make(map[rune]struct{}) for _, c := range s { c = unicode.ToLower(c) if _, ok := m[c]; !ok { m[c] = struct{}{} } } return len(m) } // countSystematicChars returns how many runes in a string are part of a sequence ('abcdef', '654321') func countSystematicChars(s string) int { var x int rs := []rune(s) for i, c := range rs { if i == 0 { continue } if c == rs[i-1]+1 || c == rs[i-1]-1 { x++ } } return x } // reverse returns the reversed form of a string func reverse(s string) string { var rs []rune for len(s) > 0 { r, size := utf8.DecodeLastRuneInString(s) s = s[:len(s)-size] rs = append(rs, r) } return string(rs) } // normalize returns the trimmed and lowercase version of a string func normalize(s string) string { return strings.TrimSpace(strings.ToLower(s)) } // hashsum returns the hashed sum of a string func hashsum(s string, hasher hash.Hash) string { hasher.Reset() hasher.Write([]byte(s)) return hex.EncodeToString(hasher.Sum(nil)) }