Grand test fixup (#138)

* start fixing up tests

* fix up tests + automate with drone

* fiddle with linting

* messing about with drone.yml

* some more fiddling

* hmmm

* add cache

* add vendor directory

* verbose

* ci updates

* update some little things

* update sig
This commit is contained in:
Tobi Smethurst
2021-08-12 21:03:24 +02:00
committed by GitHub
parent 329a5e8144
commit 98263a7de6
2677 changed files with 1090869 additions and 219 deletions

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,21 @@
test:
go test ./...
fmt:
go fmt ./...
vet:
go vet ./...
install-lint:
GO111MODULE=off go get -u golang.org/x/lint/golint
GO111MODULE=off go list -f {{.Target}} golang.org/x/lint/golint
lint:
go list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status
install-staticcheck:
GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck
staticcheck:
staticcheck -f stylish ./...

View File

@ -0,0 +1,137 @@
# go-password-validator
Simple password validator using raw entropy values. Hit the project with a star if you find it useful ⭐
Supported by [Qvault](https://qvault.io)
[![](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)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
This project can be used to front a password strength meter, or simply validate password strength on the server. Benefits:
* No stupid rules (doesn't require uppercase, numbers, special characters, etc)
* Everything is based on entropy (raw cryptographic strength of the password)
* Doesn't load large sets of data into memory - very fast and lightweight
* Doesn't contact any API's or external systems
* 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/wagslane/go-password-validator
```
## 🚀 Quick Start
```go
package main
import (
passwordvalidator "github.com/wagslane/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 graph that shows some common timings for different values, somewhere in the 50-70 range seems "reasonable".
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.
![entropy](https://external-preview.redd.it/rhdADIZYXJM2FxqNf6UOFqU5ar0VX3fayLFpKspN8uI.png?auto=webp&s=9c142ebb37ed4c39fb6268c1e4f6dc529dcb4282)
## How It Works
First, we determine the "base" number. The base is a sum of the different "character sets" found in the password.
We've *arbitrarily* chosen the following character sets:
* 26 lowercase letters
* 26 uppercase letters
* 10 digits
* 5 replacement characters - `!@$&*`
* 5 seperator characters - `_-., `
* 22 less common special characters - `"#%'()+/:;<=>?[\]^{|}~`
Using at least one character from each set your base number will be 94: `26+26+10+5+5+22 = 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.
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.
### Additional Safety
We try to err on the side of reporting *less* entropy rather than *more*.
#### Same Character
With repeated characters like `aaaaaaaaaaaaa`, or `111222`, we modify the length of the sequence to count as no more than `2`.
* `aaaa` has length 2
* `111222` has length 4
#### Common Sequences
Common sequences of length three or greater count as length `2`.
* `12345` has length 2
* `765432` has length 2
* `abc` has length 2
* `qwerty` has length 2
The sequences are checked from back->front and front->back. Here are the sequences we've implemented so far, and they're case-insensitive:
* `0123456789`
* `qwertyuiop`
* `asdfghjkl`
* `zxcvbnm`
* `abcdefghijklmnopqrstuvwxyz`
## Not ZXCVBN
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.
## 💬 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 `main` branch.
```bash
make test
make fmt
make vet
make lint
```

View File

@ -0,0 +1,75 @@
package passwordvalidator
import "strings"
const (
replaceChars = `!@$&*`
sepChars = `_-., `
otherSpecialChars = `"#%'()+/:;<=>?[\]^{|}~`
lowerChars = `abcdefghijklmnopqrstuvwxyz`
upperChars = `ABCDEFGHIJKLMNOPQRSTUVWXYZ`
digitsChars = `0123456789`
)
func getBase(password string) int {
chars := map[rune]struct{}{}
for _, c := range password {
chars[c] = struct{}{}
}
hasReplace := false
hasSep := false
hasOtherSpecial := false
hasLower := false
hasUpper := false
hasDigits := false
base := 0
for c := range chars {
if strings.ContainsRune(replaceChars, c) {
hasReplace = true
continue
}
if strings.ContainsRune(sepChars, c) {
hasSep = true
continue
}
if strings.ContainsRune(otherSpecialChars, c) {
hasOtherSpecial = true
continue
}
if strings.ContainsRune(lowerChars, c) {
hasLower = true
continue
}
if strings.ContainsRune(upperChars, c) {
hasUpper = true
continue
}
if strings.ContainsRune(digitsChars, c) {
hasDigits = true
continue
}
base++
}
if hasReplace {
base += len(replaceChars)
}
if hasSep {
base += len(sepChars)
}
if hasOtherSpecial {
base += len(otherSpecialChars)
}
if hasLower {
base += len(lowerChars)
}
if hasUpper {
base += len(upperChars)
}
if hasDigits {
base += len(digitsChars)
}
return base
}

View File

@ -0,0 +1,39 @@
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)
}
// 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/wagslane/go-password-validator
go 1.16

View File

@ -0,0 +1,94 @@
package passwordvalidator
const (
seqNums = "0123456789"
seqKeyboard0 = "qwertyuiop"
seqKeyboard1 = "asdfghjkl"
seqKeyboard2 = "zxcvbnm"
seqAlphabet = "abcdefghijklmnopqrstuvwxyz"
)
func removeMoreThanTwoFromSequence(s, seq string) string {
seqRunes := []rune(seq)
runes := []rune(s)
matches := 0
for i := 0; i < len(runes); i++ {
for j := 0; j < len(seqRunes); j++ {
if i >= len(runes) {
break
}
r := runes[i]
r2 := seqRunes[j]
if r != r2 {
matches = 0
continue
}
// found a match, advance the counter
matches++
if matches > 2 {
runes = deleteRuneAt(runes, i)
} else {
i++
}
}
}
return string(runes)
}
func deleteRuneAt(runes []rune, i int) []rune {
if i >= len(runes) ||
i < 0 {
return runes
}
copy(runes[i:], runes[i+1:])
runes[len(runes)-1] = 0
runes = runes[:len(runes)-1]
return runes
}
func getReversedString(s string) string {
n := 0
rune := make([]rune, len(s))
for _, r := range s {
rune[n] = r
n++
}
rune = rune[0:n]
// Reverse
for i := 0; i < n/2; i++ {
rune[i], rune[n-1-i] = rune[n-1-i], rune[i]
}
// Convert back to UTF-8.
return string(rune)
}
func removeMoreThanTwoRepeatingChars(s string) string {
var prevPrev rune
var prev rune
runes := []rune(s)
for i := 0; i < len(runes); i++ {
r := runes[i]
if r == prev && r == prevPrev {
runes = deleteRuneAt(runes, i)
i--
}
prevPrev = prev
prev = r
}
return string(runes)
}
func getLength(password string) int {
password = removeMoreThanTwoRepeatingChars(password)
password = removeMoreThanTwoFromSequence(password, seqNums)
password = removeMoreThanTwoFromSequence(password, seqKeyboard0)
password = removeMoreThanTwoFromSequence(password, seqKeyboard1)
password = removeMoreThanTwoFromSequence(password, seqKeyboard2)
password = removeMoreThanTwoFromSequence(password, seqAlphabet)
password = removeMoreThanTwoFromSequence(password, getReversedString(seqNums))
password = removeMoreThanTwoFromSequence(password, getReversedString(seqKeyboard0))
password = removeMoreThanTwoFromSequence(password, getReversedString(seqKeyboard1))
password = removeMoreThanTwoFromSequence(password, getReversedString(seqKeyboard2))
password = removeMoreThanTwoFromSequence(password, getReversedString(seqAlphabet))
return len(password)
}

View File

@ -0,0 +1,75 @@
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
}
hasReplace := false
hasSep := false
hasOtherSpecial := false
hasLower := false
hasUpper := false
hasDigits := false
for _, c := range password {
if strings.ContainsRune(replaceChars, c) {
hasReplace = true
continue
}
if strings.ContainsRune(sepChars, c) {
hasSep = true
continue
}
if strings.ContainsRune(otherSpecialChars, c) {
hasOtherSpecial = true
continue
}
if strings.ContainsRune(lowerChars, c) {
hasLower = true
continue
}
if strings.ContainsRune(upperChars, c) {
hasUpper = true
continue
}
if strings.ContainsRune(digitsChars, c) {
hasDigits = true
continue
}
}
allMessages := []string{}
if !hasOtherSpecial || !hasSep || !hasReplace {
allMessages = append(allMessages, "including more 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")
}