➕ Added password validator
This commit is contained in:
parent
762d43c330
commit
82fe41b14d
1
go.mod
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/lane-c-wagner/go-password-validator
|
||||
|
||||
go 1.15
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue