├── go.mod ├── Makefile ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── tests.yml ├── entropy_test.go ├── entropy.go ├── LICENSE ├── validate_test.go ├── base_test.go ├── base.go ├── validate.go ├── length.go ├── length_test.go └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wagslane/go-password-validator 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test ./... 3 | 4 | fmt: 5 | go fmt ./... 6 | 7 | vet: 8 | go vet ./... 9 | 10 | install-lint: 11 | GO111MODULE=off go get -u golang.org/x/lint/golint 12 | GO111MODULE=off go list -f {{.Target}} golang.org/x/lint/golint 13 | 14 | lint: 15 | go list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status 16 | 17 | install-staticcheck: 18 | GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck 19 | 20 | staticcheck: 21 | staticcheck -f stylish ./... 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Please provide an example of a failing function call with the inputs that produce it 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots or console output that helps explain the situation 21 | 22 | **Environment (please complete the following information):** 23 | - OS: [e.g. Linux Ubuntu] 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | env: 13 | GOFLAGS: -mod=vendor 14 | GOPROXY: "off" 15 | 16 | steps: 17 | 18 | - name: Set up Go 1.15 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.15 22 | id: go 23 | 24 | - name: Check out code into the Go module directory 25 | uses: actions/checkout@v2 26 | 27 | - name: Format 28 | run: make fmt 29 | 30 | - name: Vet 31 | run: make vet 32 | 33 | - name: Lint 34 | run: | 35 | make install-lint 36 | make lint 37 | 38 | - name: Staticcheck 39 | run: | 40 | make install-staticcheck 41 | make staticcheck 42 | 43 | - name: Test 44 | run: make test 45 | -------------------------------------------------------------------------------- /entropy_test.go: -------------------------------------------------------------------------------- 1 | package passwordvalidator 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestLogPow(t *testing.T) { 9 | expected := math.Round(math.Log2(math.Pow(7, 8))) 10 | actual := math.Round(logPow(7, 8, 2)) 11 | if actual != expected { 12 | t.Errorf("Expected %v, got %v", expected, actual) 13 | } 14 | 15 | expected = math.Round(math.Log2(math.Pow(10, 11))) 16 | actual = math.Round(logPow(10, 11, 2)) 17 | if actual != expected { 18 | t.Errorf("Expected %v, got %v", expected, actual) 19 | } 20 | 21 | expected = math.Round(math.Log2(math.Pow(11, 17))) 22 | actual = math.Round(logPow(11, 17, 2)) 23 | if actual != expected { 24 | t.Errorf("Expected %v, got %v", expected, actual) 25 | } 26 | 27 | expected = math.Round(math.Log10(math.Pow(13, 21))) 28 | actual = math.Round(logPow(13, 21, 10)) 29 | if actual != expected { 30 | t.Errorf("Expected %v, got %v", expected, actual) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /entropy.go: -------------------------------------------------------------------------------- 1 | package passwordvalidator 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // GetEntropy returns the entropy in bits for the given password 8 | // See the ReadMe for more information 9 | func GetEntropy(password string) float64 { 10 | return getEntropy(password) 11 | } 12 | 13 | func getEntropy(password string) float64 { 14 | base := getBase(password) 15 | length := getLength(password) 16 | 17 | // calculate log2(base^length) 18 | return logPow(float64(base), length, 2) 19 | } 20 | 21 | func logX(base, n float64) float64 { 22 | if base == 0 { 23 | return 0 24 | } 25 | // change of base formulae 26 | return math.Log2(n) / math.Log2(base) 27 | } 28 | 29 | // logPow calculates log_base(x^y) 30 | // without leaving logspace for each multiplication step 31 | // this makes it take less space in memory 32 | func logPow(expBase float64, pow int, logBase float64) float64 { 33 | // logb (MN) = logb M + logb N 34 | total := 0.0 35 | for i := 0; i < pow; i++ { 36 | total += logX(logBase, expBase) 37 | } 38 | return total 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lane Wagner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /validate_test.go: -------------------------------------------------------------------------------- 1 | package passwordvalidator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestValidate(t *testing.T) { 8 | err := Validate("mypass", 50) 9 | expectedError := "insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password" 10 | if err.Error() != expectedError { 11 | t.Errorf("Wanted %v, got %v", expectedError, err) 12 | } 13 | 14 | err = Validate("MYPASS", 50) 15 | expectedError = "insecure password, try including more special characters, using lowercase letters, using numbers or using a longer password" 16 | if err.Error() != expectedError { 17 | t.Errorf("Wanted %v, got %v", expectedError, err) 18 | } 19 | 20 | err = Validate("mypassword", 4) 21 | if err != nil { 22 | t.Errorf("Err should be nil") 23 | } 24 | 25 | err = Validate("aGoo0dMi#oFChaR2", 80) 26 | if err != nil { 27 | t.Errorf("Err should be nil") 28 | } 29 | 30 | expectedError = "insecure password, try including more special characters, using lowercase letters, using uppercase letters or using a longer password" 31 | err = Validate("123", 60) 32 | if err.Error() != expectedError { 33 | t.Errorf("Wanted %v, got %v", expectedError, err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /base_test.go: -------------------------------------------------------------------------------- 1 | package passwordvalidator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetBase(t *testing.T) { 8 | actual := getBase("abcd") 9 | expected := len(lowerChars) 10 | if actual != expected { 11 | t.Errorf("Wanted %v, got %v", expected, actual) 12 | } 13 | 14 | actual = getBase("abcdA") 15 | expected = len(lowerChars) + len(upperChars) 16 | if actual != expected { 17 | t.Errorf("Wanted %v, got %v", expected, actual) 18 | } 19 | 20 | actual = getBase("A") 21 | expected = len(upperChars) 22 | if actual != expected { 23 | t.Errorf("Wanted %v, got %v", expected, actual) 24 | } 25 | 26 | actual = getBase("^_") 27 | expected = len(otherSpecialChars) + len(sepChars) 28 | if actual != expected { 29 | t.Errorf("Wanted %v, got %v", expected, actual) 30 | } 31 | 32 | actual = getBase("^") 33 | expected = len(otherSpecialChars) 34 | if actual != expected { 35 | t.Errorf("Wanted %v, got %v", expected, actual) 36 | } 37 | 38 | actual = getBase("!") 39 | expected = len(replaceChars) 40 | if actual != expected { 41 | t.Errorf("Wanted %v, got %v", expected, actual) 42 | } 43 | 44 | actual = getBase("123") 45 | expected = len(digitsChars) 46 | if actual != expected { 47 | t.Errorf("Wanted %v, got %v", expected, actual) 48 | } 49 | 50 | actual = getBase("123ü") 51 | expected = len(digitsChars) + 1 52 | if actual != expected { 53 | t.Errorf("Wanted %v, got %v", expected, actual) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | package passwordvalidator 2 | 3 | import "strings" 4 | 5 | const ( 6 | replaceChars = `!@$&*` 7 | sepChars = `_-., ` 8 | otherSpecialChars = `"#%'()+/:;<=>?[\]^{|}~` 9 | lowerChars = `abcdefghijklmnopqrstuvwxyz` 10 | upperChars = `ABCDEFGHIJKLMNOPQRSTUVWXYZ` 11 | digitsChars = `0123456789` 12 | ) 13 | 14 | func getBase(password string) int { 15 | chars := map[rune]struct{}{} 16 | for _, c := range password { 17 | chars[c] = struct{}{} 18 | } 19 | 20 | hasReplace := false 21 | hasSep := false 22 | hasOtherSpecial := false 23 | hasLower := false 24 | hasUpper := false 25 | hasDigits := false 26 | base := 0 27 | 28 | for c := range chars { 29 | switch { 30 | case strings.ContainsRune(replaceChars, c): 31 | hasReplace = true 32 | case strings.ContainsRune(sepChars, c): 33 | hasSep = true 34 | case strings.ContainsRune(otherSpecialChars, c): 35 | hasOtherSpecial = true 36 | case strings.ContainsRune(lowerChars, c): 37 | hasLower = true 38 | case strings.ContainsRune(upperChars, c): 39 | hasUpper = true 40 | case strings.ContainsRune(digitsChars, c): 41 | hasDigits = true 42 | default: 43 | base++ 44 | } 45 | } 46 | 47 | if hasReplace { 48 | base += len(replaceChars) 49 | } 50 | if hasSep { 51 | base += len(sepChars) 52 | } 53 | if hasOtherSpecial { 54 | base += len(otherSpecialChars) 55 | } 56 | if hasLower { 57 | base += len(lowerChars) 58 | } 59 | if hasUpper { 60 | base += len(upperChars) 61 | } 62 | if hasDigits { 63 | base += len(digitsChars) 64 | } 65 | return base 66 | } 67 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package passwordvalidator 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Validate returns nil if the password has greater than or 10 | // equal to the minimum entropy. If not, an error is returned 11 | // that explains how the password can be strengthened. This error 12 | // is safe to show the client 13 | func Validate(password string, minEntropy float64) error { 14 | entropy := getEntropy(password) 15 | if entropy >= minEntropy { 16 | return nil 17 | } 18 | 19 | hasReplace := false 20 | hasSep := false 21 | hasOtherSpecial := false 22 | hasLower := false 23 | hasUpper := false 24 | hasDigits := false 25 | 26 | for _, c := range password { 27 | switch { 28 | case strings.ContainsRune(replaceChars, c): 29 | hasReplace = true 30 | case strings.ContainsRune(sepChars, c): 31 | hasSep = true 32 | case strings.ContainsRune(otherSpecialChars, c): 33 | hasOtherSpecial = true 34 | case strings.ContainsRune(lowerChars, c): 35 | hasLower = true 36 | case strings.ContainsRune(upperChars, c): 37 | hasUpper = true 38 | case strings.ContainsRune(digitsChars, c): 39 | hasDigits = true 40 | } 41 | } 42 | 43 | allMessages := []string{} 44 | 45 | if !hasOtherSpecial || !hasSep || !hasReplace { 46 | allMessages = append(allMessages, "including more special characters") 47 | } 48 | if !hasLower { 49 | allMessages = append(allMessages, "using lowercase letters") 50 | } 51 | if !hasUpper { 52 | allMessages = append(allMessages, "using uppercase letters") 53 | } 54 | if !hasDigits { 55 | allMessages = append(allMessages, "using numbers") 56 | } 57 | 58 | if len(allMessages) > 0 { 59 | return fmt.Errorf( 60 | "insecure password, try %v or using a longer password", 61 | strings.Join(allMessages, ", "), 62 | ) 63 | } 64 | 65 | return errors.New("insecure password, try using a longer password") 66 | } 67 | -------------------------------------------------------------------------------- /length.go: -------------------------------------------------------------------------------- 1 | package passwordvalidator 2 | 3 | const ( 4 | seqNums = "0123456789" 5 | seqKeyboard0 = "qwertyuiop" 6 | seqKeyboard1 = "asdfghjkl" 7 | seqKeyboard2 = "zxcvbnm" 8 | seqAlphabet = "abcdefghijklmnopqrstuvwxyz" 9 | ) 10 | 11 | func removeMoreThanTwoFromSequence(s, seq string) string { 12 | seqRunes := []rune(seq) 13 | runes := []rune(s) 14 | matches := 0 15 | for i := 0; i < len(runes); i++ { 16 | for j := 0; j < len(seqRunes); j++ { 17 | if i >= len(runes) { 18 | break 19 | } 20 | r := runes[i] 21 | r2 := seqRunes[j] 22 | if r != r2 { 23 | matches = 0 24 | continue 25 | } 26 | // found a match, advance the counter 27 | matches++ 28 | if matches > 2 { 29 | runes = deleteRuneAt(runes, i) 30 | } else { 31 | i++ 32 | } 33 | } 34 | } 35 | return string(runes) 36 | } 37 | 38 | func deleteRuneAt(runes []rune, i int) []rune { 39 | if i >= len(runes) || 40 | i < 0 { 41 | return runes 42 | } 43 | copy(runes[i:], runes[i+1:]) 44 | runes[len(runes)-1] = 0 45 | runes = runes[:len(runes)-1] 46 | return runes 47 | } 48 | 49 | func getReversedString(s string) string { 50 | n := 0 51 | rune := make([]rune, len(s)) 52 | for _, r := range s { 53 | rune[n] = r 54 | n++ 55 | } 56 | rune = rune[0:n] 57 | // Reverse 58 | for i := 0; i < n/2; i++ { 59 | rune[i], rune[n-1-i] = rune[n-1-i], rune[i] 60 | } 61 | // Convert back to UTF-8. 62 | return string(rune) 63 | } 64 | 65 | func removeMoreThanTwoRepeatingChars(s string) string { 66 | var prevPrev rune 67 | var prev rune 68 | runes := []rune(s) 69 | for i := 0; i < len(runes); i++ { 70 | r := runes[i] 71 | if r == prev && r == prevPrev { 72 | runes = deleteRuneAt(runes, i) 73 | i-- 74 | } 75 | prevPrev = prev 76 | prev = r 77 | } 78 | return string(runes) 79 | } 80 | 81 | func getLength(password string) int { 82 | password = removeMoreThanTwoRepeatingChars(password) 83 | password = removeMoreThanTwoFromSequence(password, seqNums) 84 | password = removeMoreThanTwoFromSequence(password, seqKeyboard0) 85 | password = removeMoreThanTwoFromSequence(password, seqKeyboard1) 86 | password = removeMoreThanTwoFromSequence(password, seqKeyboard2) 87 | password = removeMoreThanTwoFromSequence(password, seqAlphabet) 88 | password = removeMoreThanTwoFromSequence(password, getReversedString(seqNums)) 89 | password = removeMoreThanTwoFromSequence(password, getReversedString(seqKeyboard0)) 90 | password = removeMoreThanTwoFromSequence(password, getReversedString(seqKeyboard1)) 91 | password = removeMoreThanTwoFromSequence(password, getReversedString(seqKeyboard2)) 92 | password = removeMoreThanTwoFromSequence(password, getReversedString(seqAlphabet)) 93 | return len(password) 94 | } 95 | -------------------------------------------------------------------------------- /length_test.go: -------------------------------------------------------------------------------- 1 | package passwordvalidator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRemoveMoreThanTwoFromSequence(t *testing.T) { 8 | actual := removeMoreThanTwoFromSequence("12345678", "0123456789") 9 | expected := "12" 10 | if actual != expected { 11 | t.Errorf("Wanted %v, got %v", expected, actual) 12 | } 13 | 14 | actual = removeMoreThanTwoFromSequence("abcqwertyabc", "qwertyuiop") 15 | expected = "abcqwabc" 16 | if actual != expected { 17 | t.Errorf("Wanted %v, got %v", expected, actual) 18 | } 19 | 20 | actual = removeMoreThanTwoFromSequence("", "") 21 | expected = "" 22 | if actual != expected { 23 | t.Errorf("Wanted %v, got %v", expected, actual) 24 | } 25 | 26 | actual = removeMoreThanTwoFromSequence("", "12345") 27 | expected = "" 28 | if actual != expected { 29 | t.Errorf("Wanted %v, got %v", expected, actual) 30 | } 31 | } 32 | 33 | func TestGetReversedString(t *testing.T) { 34 | actual := getReversedString("abcd") 35 | expected := "dcba" 36 | if actual != expected { 37 | t.Errorf("Wanted %v, got %v", expected, actual) 38 | } 39 | 40 | actual = getReversedString("1234") 41 | expected = "4321" 42 | if actual != expected { 43 | t.Errorf("Wanted %v, got %v", expected, actual) 44 | } 45 | } 46 | 47 | func TestRemoveRepeatingChars(t *testing.T) { 48 | actual := removeMoreThanTwoRepeatingChars("aaaa") 49 | expected := "aa" 50 | if actual != expected { 51 | t.Errorf("Wanted %v, got %v", expected, actual) 52 | } 53 | 54 | actual = removeMoreThanTwoRepeatingChars("bbbbbbbaaaaaaaaa") 55 | expected = "bbaa" 56 | if actual != expected { 57 | t.Errorf("Wanted %v, got %v", expected, actual) 58 | } 59 | 60 | actual = removeMoreThanTwoRepeatingChars("ab") 61 | expected = "ab" 62 | if actual != expected { 63 | t.Errorf("Wanted %v, got %v", expected, actual) 64 | } 65 | 66 | actual = removeMoreThanTwoRepeatingChars("") 67 | expected = "" 68 | if actual != expected { 69 | t.Errorf("Wanted %v, got %v", expected, actual) 70 | } 71 | } 72 | 73 | func TestGetLength(t *testing.T) { 74 | actual := getLength("aaaa") 75 | expected := 2 76 | if actual != expected { 77 | t.Errorf("Wanted %v, got %v", expected, actual) 78 | } 79 | 80 | actual = getLength("11112222") 81 | expected = 4 82 | if actual != expected { 83 | t.Errorf("Wanted %v, got %v", expected, actual) 84 | } 85 | 86 | actual = getLength("aa123456") 87 | expected = 4 88 | if actual != expected { 89 | t.Errorf("Wanted %v, got %v", expected, actual) 90 | } 91 | 92 | actual = getLength("876543") 93 | expected = 2 94 | if actual != expected { 95 | t.Errorf("Wanted %v, got %v", expected, actual) 96 | } 97 | 98 | actual = getLength("qwerty123456z") 99 | expected = 5 100 | if actual != expected { 101 | t.Errorf("Wanted %v, got %v", expected, actual) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-password-validator 2 | 3 | Simple password validator using raw entropy values. Hit the project with a star if you find it useful ⭐ 4 | 5 | Supported by [Boot.dev](https://boot.dev) 6 | 7 | [![](https://godoc.org/github.com/wagslane/go-password-validator?status.svg)](https://godoc.org/github.com/wagslane/go-password-validator) ![Deploy](https://github.com/wagslane/go-password-validator/workflows/Tests/badge.svg) 8 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 9 | 10 | This project can be used to front a password strength meter, or simply validate password strength on the server. Benefits: 11 | 12 | * No stupid rules (doesn't require uppercase, numbers, special characters, etc) 13 | * Everything is based on entropy (raw cryptographic strength of the password) 14 | * Doesn't load large sets of data into memory - very fast and lightweight 15 | * Doesn't contact any API's or external systems 16 | * Inspired by this [XKCD](https://xkcd.com/936/) 17 | 18 | ![XKCD Passwords](https://imgs.xkcd.com/comics/password_strength.png) 19 | 20 | ## ⚙️ Installation 21 | 22 | Outside of a Go module: 23 | 24 | ```bash 25 | go get github.com/wagslane/go-password-validator 26 | ``` 27 | 28 | ## 🚀 Quick Start 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | passwordvalidator "github.com/wagslane/go-password-validator" 35 | ) 36 | 37 | func main(){ 38 | entropy := passwordvalidator.GetEntropy("a longer password") 39 | // entropy is a float64, representing the strength in base 2 (bits) 40 | 41 | const minEntropyBits = 60 42 | err := passwordvalidator.Validate("some password", minEntropyBits) 43 | // if the password has enough entropy, err is nil 44 | // otherwise, a formatted error message is provided explaining 45 | // how to increase the strength of the password 46 | // (safe to show to the client) 47 | } 48 | ``` 49 | 50 | ## What Entropy Value Should I Use? 51 | 52 | It's up to you. That said, here is a graph that shows some common timings for different values, somewhere in the 50-70 range seems "reasonable". 53 | 54 | Keep in mind that attackers likely aren't just brute-forcing passwords, if you want protection against common passwords or [PWNed passwords](https://haveibeenpwned.com/) you'll need to do additional work. This library is lightweight, doesn't load large datasets, and doesn't contact external services. 55 | 56 | ![entropy](https://external-preview.redd.it/rhdADIZYXJM2FxqNf6UOFqU5ar0VX3fayLFpKspN8uI.png?auto=webp&s=9c142ebb37ed4c39fb6268c1e4f6dc529dcb4282) 57 | 58 | ## How It Works 59 | 60 | First, we determine the "base" number. The base is a sum of the different "character sets" found in the password. 61 | 62 | We've *arbitrarily* chosen the following character sets: 63 | 64 | * 26 lowercase letters 65 | * 26 uppercase letters 66 | * 10 digits 67 | * 5 replacement characters - `!@$&*` 68 | * 5 seperator characters - `_-., ` 69 | * 22 less common special characters - `"#%'()+/:;<=>?[\]^{|}~` 70 | 71 | 72 | Using at least one character from each set your base number will be 94: `26+26+10+5+5+22 = 94` 73 | 74 | Every unique character that doesn't match one of those sets will add `1` to the base. 75 | 76 | If you only use, for example, lowercase letters and numbers, your base will be 36: `26+10 = 36`. 77 | 78 | After we have calculated a base, the total number of brute-force-guesses is found using the following formulae: `base^length` 79 | 80 | A password using base 26 with 7 characters would require `26^7`, or `8031810176` guesses. 81 | 82 | Once we know the number of guesses it would take, we can calculate the actual entropy in bits using `log2(guesses)`. That calculation is done in log space in practice to avoid numeric overflow. 83 | 84 | ### Additional Safety 85 | 86 | We try to err on the side of reporting *less* entropy rather than *more*. 87 | 88 | #### Same Character 89 | 90 | With repeated characters like `aaaaaaaaaaaaa`, or `111222`, we modify the length of the sequence to count as no more than `2`. 91 | 92 | * `aaaa` has length 2 93 | * `111222` has length 4 94 | 95 | #### Common Sequences 96 | 97 | Common sequences of length three or greater count as length `2`. 98 | 99 | * `12345` has length 2 100 | * `765432` has length 2 101 | * `abc` has length 2 102 | * `qwerty` has length 2 103 | 104 | The sequences are checked from back->front and front->back. Here are the sequences we've implemented so far, and they're case-insensitive: 105 | 106 | * `0123456789` 107 | * `qwertyuiop` 108 | * `asdfghjkl` 109 | * `zxcvbnm` 110 | * `abcdefghijklmnopqrstuvwxyz` 111 | 112 | ## Not ZXCVBN 113 | 114 | There's another project that has a similar purpose, [zxcvbn](https://github.com/dropbox/zxcvbn), and you may want to check it out as well. Our goal is not to be zxcvbn, because it's already good at what it does. `go-password-validator` doesn't load any large datasets of real-world passwords, we write simple rules to calculate an entropy score. It's up to the user of this library to decide how to use that entropy score, and what scores constitute "secure enough" for their application. 115 | 116 | ## 💬 Contact 117 | 118 | [![Twitter Follow](https://img.shields.io/twitter/follow/wagslane.svg?label=Follow%20Wagslane&style=social)](https://twitter.com/intent/follow?screen_name=wagslane) 119 | 120 | Submit an issue (above in the issues tab) 121 | 122 | ## Transient Dependencies 123 | 124 | None! And it will stay that way, except of course for the standard library. 125 | 126 | ## 👏 Contributing 127 | 128 | I love help! Contribute by forking the repo and opening pull requests. Please ensure that your code passes the existing tests and linting, and write tests to test your changes if applicable. 129 | 130 | All pull requests should be submitted to the `main` branch. 131 | 132 | ```bash 133 | make test 134 | make fmt 135 | make vet 136 | make lint 137 | ``` 138 | --------------------------------------------------------------------------------