Added password validator

This commit is contained in:
Andreas Schneider 2020-11-01 11:33:13 +01:00
parent 762d43c330
commit 82fe41b14d
10 changed files with 328 additions and 0 deletions

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.15
require (
github.com/alecthomas/kong v0.2.11
github.com/go-chi/chi v4.1.2+incompatible
github.com/lane-c-wagner/go-password-validator v0.1.0
github.com/satori/go.uuid v1.2.0
github.com/sethvargo/go-password v0.2.0
github.com/tidwall/buntdb v1.1.2

2
go.sum
View File

@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/go-chi/chi v1.0.0 h1:s/kv1cTXfivYjdKJdyUzNGyAWZ/2t7duW1gKn5ivu+c=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/lane-c-wagner/go-password-validator v0.1.0 h1:URcgm0OLHOl22PLHla9tUgrvtzWJvNZQ0fUkiTNMDV0=
github.com/lane-c-wagner/go-password-validator v0.1.0/go.mod h1:+ffeYvV/jNEIJDYTTujyJ/sfdarIIsv01xD+JA4hkDI=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Lane Wagner
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.

View File

@ -0,0 +1,102 @@
# go-password-validator
Validate the Strength of a Password in Go
[![](https://godoc.org/github.com/lane-c-wagner/go-password-validator?status.svg)](https://godoc.org/github.com/lane-c-wagner/go-password-validator)
This project can be used to front a password strength meter, or simplay validate password strength on the server. Benefits:
* No stupid rules (use uppercase, use special characters, etc)
* Everything is based on entropy (raw cryptographic strength of the password)
* Inspired by this [XKCD](https://xkcd.com/936/)
![XKCD Passwords](https://imgs.xkcd.com/comics/password_strength.png)
## ⚙️ Installation
Outside of a Go module:
```bash
go get github.com/lane-c-wagner/go-password-validator
```
## 🚀 Quick Start
```go
package main
import (
passwordvalidator "github.com/lane-c-wagner/go-password-validator"
)
func main(){
entropy := passwordvalidator.GetEntropy("a longer password")
// entropy is a float64, representing the strength in base 2 (bits)
const minEntropyBits = 60
err := passwordvalidator.Validate("some password", minEntropyBits)
// if the password has enough entropy, err is nil
// otherwise, a formatted error message is provided explaining
// how to increase the strength of the password
// (safe to show to the client)
}
```
## What Entropy Value Should I Use?
It's up to you. That said, here is a pretty good graph that shows some timings for differnt values:
![entropy](https://external-preview.redd.it/rhdADIZYXJM2FxqNf6UOFqU5ar0VX3fayLFpKspN8uI.png?auto=webp&s=9c142ebb37ed4c39fb6268c1e4f6dc529dcb4282)
Somewhere in the 50-70 range seems "average"
## How It Works
First we determines the "base" number. The base is a sum of the different "character sets" found in the password.
The current character sets include:
* 26 lowercase letters
* 26 uppercase
* 10 digits
* 32 special characters - ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~`
Using at least one character from each set your base number will be 94: `26+26+10+32 = 94`
Every unique character that doesn't match one of those sets will add `1` to the base.
If you only use, for example, lowercase letters and numbers, your base will be 36: `26+10 = 36`.
After we have calculated a base, the total number of brute-force-guesses is found using the following formulae: `base^length`
A password using base 26 with 7 characters would require `26^7`, or `8031810176` guesses.
### Additional Safety
To add further safety to dumb passwords like aaaaaaaaaaaaa, or 123123123, We modify the length of the password to count any more than two of the same character as 0.
* `aaaa` has length 2
* `12121234` has length 6
## 💬 Contact
[![Twitter Follow](https://img.shields.io/twitter/follow/wagslane.svg?label=Follow%20Wagslane&style=social)](https://twitter.com/intent/follow?screen_name=wagslane)
Submit an issue (above in the issues tab)
## Transient Dependencies
None! And it will stay that way, except of course for the standard library.
## 👏 Contributing
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.
All pull requests should be submitted to the "master" branch.
```bash
go test
```
```bash
go fmt
```

View File

@ -0,0 +1,64 @@
package passwordvalidator
const (
specialChars = ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~`
lowerChars = `abcdefghijklmnopqrstuvwxyz`
upperChars = `ABCDEFGHIJKLMNOPQRSTUVWXYZ`
digitsChars = `0123456789`
)
func getBase(password string) int {
chars := map[rune]struct{}{}
for _, c := range password {
chars[c] = struct{}{}
}
hasSpecial := false
hasLower := false
hasUpper := false
hasDigits := false
base := 0
for c := range chars {
if containsRune(specialChars, c) {
hasSpecial = true
continue
}
if containsRune(lowerChars, c) {
hasLower = true
continue
}
if containsRune(upperChars, c) {
hasUpper = true
continue
}
if containsRune(digitsChars, c) {
hasDigits = true
continue
}
base++
}
if hasSpecial {
base += len(specialChars)
}
if hasLower {
base += len(lowerChars)
}
if hasUpper {
base += len(upperChars)
}
if hasDigits {
base += len(digitsChars)
}
return base
}
func containsRune(s string, r rune) bool {
for _, c := range s {
if c == r {
return true
}
}
return false
}

View File

@ -0,0 +1,47 @@
package passwordvalidator
import (
"math"
)
// GetEntropy returns the entropy in bits for the given password
// See the ReadMe for more information
func GetEntropy(password string) float64 {
return getEntropy(password)
}
func getEntropy(password string) float64 {
base := getBase(password)
length := getLength(password)
// calculate log2(base^length)
return logPow(float64(base), length, 2)
}
func logX(base, n float64) float64 {
if base == 0 {
return 0
}
// change of base formulae
return math.Log2(n) / math.Log2(base)
}
func logBaseTo2(base, n float64) float64 {
if base == 0 {
return 0
}
// change of base formulae
return math.Log2(n) / math.Log2(base)
}
// logPow calculates log_base(x^y)
// without leaving logspace for each multiplication step
// this makes it take less space in memory
func logPow(expBase float64, pow int, logBase float64) float64 {
// logb (MN) = logb M + logb N
total := 0.0
for i := 0; i < pow; i++ {
total += logX(logBase, expBase)
}
return total
}

View File

@ -0,0 +1,3 @@
module github.com/lane-c-wagner/go-password-validator
go 1.15

View File

@ -0,0 +1,20 @@
package passwordvalidator
func getLength(password string) int {
const maxNumSameChar = 2
chars := map[rune]int{}
for _, c := range password {
if _, ok := chars[c]; !ok {
chars[c] = 0
}
if chars[c] >= maxNumSameChar {
continue
}
chars[c]++
}
length := 0
for _, count := range chars {
length += count
}
return length
}

View File

@ -0,0 +1,65 @@
package passwordvalidator
import (
"errors"
"fmt"
"strings"
)
// Validate returns nil if the password has greater than or
// equal to the minimum entropy. If not, an error is returned
// that explains how the password can be strengthened. This error
// is safe to show the client
func Validate(password string, minEntropy float64) error {
entropy := getEntropy(password)
if entropy >= minEntropy {
return nil
}
hasSpecial := false
hasLower := false
hasUpper := false
hasDigits := false
for _, c := range password {
if containsRune(specialChars, c) {
hasSpecial = true
continue
}
if containsRune(lowerChars, c) {
hasLower = true
continue
}
if containsRune(upperChars, c) {
hasUpper = true
continue
}
if containsRune(digitsChars, c) {
hasDigits = true
continue
}
}
allMessages := []string{}
if !hasSpecial {
allMessages = append(allMessages, "including special characters")
}
if !hasLower {
allMessages = append(allMessages, "using lowercase letters")
}
if !hasUpper {
allMessages = append(allMessages, "using uppercase letters")
}
if !hasDigits {
allMessages = append(allMessages, "using numbers")
}
if len(allMessages) > 0 {
return fmt.Errorf(
"Insecure password. Try %v or using a longer password",
strings.Join(allMessages, ", "),
)
}
return errors.New("Insecure password. Try using a longer password")
}

3
vendor/modules.txt vendored
View File

@ -4,6 +4,9 @@ github.com/alecthomas/kong
# github.com/go-chi/chi v4.1.2+incompatible
## explicit
github.com/go-chi/chi
# github.com/lane-c-wagner/go-password-validator v0.1.0
## explicit
github.com/lane-c-wagner/go-password-validator
# github.com/pkg/errors v0.8.1
github.com/pkg/errors
# github.com/satori/go.uuid v1.2.0