mirror of
https://codeberg.org/scip/valpass.git
synced 2025-12-16 04:01:01 +01:00
initial commit
This commit is contained in:
39
Makefile
Normal file
39
Makefile
Normal file
@@ -0,0 +1,39 @@
|
||||
# Copyright © 2023 Thomas von Dein
|
||||
|
||||
# This module is published under the terms of the BSD 3-Clause
|
||||
# License. Please read the file LICENSE for details.
|
||||
|
||||
#
|
||||
# no need to modify anything below
|
||||
|
||||
VERSION = $(shell grep VERSION handler.go | head -1 | cut -d '"' -f2)
|
||||
|
||||
all: buildlocal
|
||||
|
||||
buildlocal:
|
||||
go build -o example/example example/example.go
|
||||
|
||||
clean:
|
||||
rm -rf $(tool) coverage.out testdata t/out example/example
|
||||
|
||||
test: clean
|
||||
go test $(ARGS)
|
||||
|
||||
singletest:
|
||||
@echo "Call like this: make singletest TEST=TestName ARGS=-v"
|
||||
go test -run $(TEST) $(ARGS)
|
||||
|
||||
cover-report:
|
||||
go test -cover -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
goupdate:
|
||||
go get -t -u=patch ./...
|
||||
|
||||
lint:
|
||||
golangci-lint run -p bugs -p unused
|
||||
|
||||
release: buildlocal test
|
||||
gh release create v$(VERSION) --generate-notes
|
||||
|
||||
|
||||
68
example/test.go
Normal file
68
example/test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/tlinden/valpass"
|
||||
)
|
||||
|
||||
const template string = `
|
||||
Metric Random Threshhold Result
|
||||
------------------------------------------------------------------
|
||||
Compression rate 0%% min %d%% %d%%
|
||||
Character distribution 100%% min %0.2f%% %0.2f%%
|
||||
Character entropy 8.0 bits/char min %0.2f %0.2f bits/char
|
||||
Character redundancy 0.0%% max %0.2f%% %0.2f%%
|
||||
Dictionary match false false %t
|
||||
------------------------------------------------------------------
|
||||
Validation response %t
|
||||
`
|
||||
|
||||
func main() {
|
||||
opts := valpass.Options{
|
||||
Compress: valpass.MIN_COMPRESS,
|
||||
CharDistribution: valpass.MIN_DIST,
|
||||
Entropy: valpass.MIN_ENTROPY,
|
||||
Dictionary: &valpass.Dictionary{Words: ReadDict("t/american-english")},
|
||||
UTF8: false,
|
||||
}
|
||||
|
||||
res, err := valpass.Validate(os.Args[1], opts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
template,
|
||||
opts.Compress,
|
||||
res.Compress,
|
||||
opts.CharDistribution,
|
||||
res.CharDistribution,
|
||||
opts.Entropy,
|
||||
res.Entropy,
|
||||
100-opts.CharDistribution,
|
||||
100-res.CharDistribution,
|
||||
res.DictionaryMatch,
|
||||
res.Ok,
|
||||
)
|
||||
}
|
||||
|
||||
func ReadDict(path string) []string {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/tlinden/valpass
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/alecthomas/repr v0.4.0 // indirect
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
285
lib.bak
Normal file
285
lib.bak
Normal file
@@ -0,0 +1,285 @@
|
||||
package valpass
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
* Contains the raw dictionary data and some flags. Must be provided
|
||||
* by the user
|
||||
*/
|
||||
type Dictionary struct {
|
||||
Words []string // the actual dictionary
|
||||
Submatch bool // if true 'foo' would match 'foobar'
|
||||
}
|
||||
|
||||
/*
|
||||
* Options define how to operate the validation
|
||||
*/
|
||||
type Options struct {
|
||||
Compress int // minimum compression rate in percent
|
||||
CharDistribution float64 // minimum char distribution in percent
|
||||
Entropy float64 // minimum entropy value in bits/char
|
||||
Dictionary []string // if set, lookup given dictionary, the caller provides it
|
||||
UTF8 bool // if true work on unicode utf-8 space, not just bytes
|
||||
}
|
||||
|
||||
/*
|
||||
* Default validation config, a compromise of comfort and security, as always.
|
||||
*/
|
||||
const (
|
||||
MIN_ENTROPY float64 = 3.0
|
||||
MIN_COMPRESS int = 10
|
||||
MIN_DICT bool = false
|
||||
MIN_DIST float64 = 10.0
|
||||
MAX_UTF8 int = 2164864 // max characters encodable with utf8
|
||||
MAX_CHARS int = 95 // maximum printable US ASCII chars
|
||||
MIN_DICT_LEN int = 5000
|
||||
|
||||
// we start our ascii arrays at char(32), so to have max 95
|
||||
// elements in the slice, we subtract 32 from each ascii code
|
||||
MIN_ASCII int = 32
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Ok bool
|
||||
Options
|
||||
}
|
||||
|
||||
func Validate(passphrase string, opts ...Options) (Result, error) {
|
||||
result := Result{Ok: true}
|
||||
options := Options{
|
||||
MIN_COMPRESS,
|
||||
MIN_DIST,
|
||||
MIN_ENTROPY,
|
||||
nil,
|
||||
false,
|
||||
}
|
||||
|
||||
if len(opts) == 1 {
|
||||
options = opts[0]
|
||||
}
|
||||
|
||||
if options.Entropy > 0 {
|
||||
var entropy float64
|
||||
var err error
|
||||
|
||||
switch options.UTF8 {
|
||||
case true:
|
||||
entropy, err = GetEntropyUTF8(passphrase)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
default:
|
||||
entropy, err = GetEntropyAscii(passphrase)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
if entropy <= options.Entropy {
|
||||
result.Ok = false
|
||||
}
|
||||
|
||||
result.Entropy = entropy
|
||||
}
|
||||
|
||||
if options.Compress > 0 {
|
||||
compression, err := GetCompression([]byte(passphrase))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if compression >= options.Compress {
|
||||
result.Ok = false
|
||||
}
|
||||
|
||||
result.Compress = compression
|
||||
}
|
||||
|
||||
if options.CharDistribution > 0 {
|
||||
var dist float64
|
||||
|
||||
switch options.UTF8 {
|
||||
case true:
|
||||
dist = GetDistributionUTF8(passphrase)
|
||||
default:
|
||||
dist = GetDistributionAscii(passphrase)
|
||||
}
|
||||
if dist <= options.CharDistribution {
|
||||
result.Ok = false
|
||||
}
|
||||
|
||||
result.CharDistribution = dist
|
||||
}
|
||||
|
||||
if len(options.Dictionary) > 0 {
|
||||
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* we compress with Flate level 9 (max) and see if the result is
|
||||
* smaller than the password, in which case it could be compressed and
|
||||
* contains repeating characters; OR it is larger than the password,
|
||||
* in which case it could NOT be compressed, which is what we want.
|
||||
*/
|
||||
func GetCompression(passphrase []byte) (int, error) {
|
||||
var b bytes.Buffer
|
||||
flater, _ := flate.NewWriter(&b, 9)
|
||||
|
||||
if _, err := flater.Write(passphrase); err != nil {
|
||||
return 0, fmt.Errorf("failed to write to flate writer: %w", err)
|
||||
}
|
||||
|
||||
if err := flater.Flush(); err != nil {
|
||||
return 0, fmt.Errorf("failed to flush flate writer: %w", err)
|
||||
}
|
||||
|
||||
if err := flater.Close(); err != nil {
|
||||
return 0, fmt.Errorf("failed to close flate writer: %w", err)
|
||||
}
|
||||
|
||||
// use floats to avoid division by zero panic
|
||||
length := float32(len(passphrase))
|
||||
compressed := float32(len(b.Bytes()))
|
||||
|
||||
if compressed >= length {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
percent := 100 - (compressed / (length / 100))
|
||||
|
||||
return int(percent), nil
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the entropy as bits/rune, where rune is a unicode char in
|
||||
* utf8 space.
|
||||
*/
|
||||
func GetEntropyUTF8(passphrase string) (float64, error) {
|
||||
var entropy float64
|
||||
length := len(passphrase)
|
||||
|
||||
wherechar := make([]int, MAX_UTF8)
|
||||
hist := make([]int, length)
|
||||
var histlen int
|
||||
|
||||
for i := 0; i < MAX_UTF8; i++ {
|
||||
wherechar[i] = -1
|
||||
}
|
||||
|
||||
for _, char := range passphrase {
|
||||
if wherechar[char] == -1 {
|
||||
wherechar[char] = histlen
|
||||
histlen++
|
||||
}
|
||||
|
||||
hist[wherechar[char]]++
|
||||
}
|
||||
|
||||
for i := 0; i < histlen; i++ {
|
||||
diff := float64(hist[i]) / float64(length)
|
||||
entropy -= diff * math.Log2(diff)
|
||||
}
|
||||
|
||||
return entropy, nil
|
||||
}
|
||||
|
||||
/* same thing for us ascii */
|
||||
func GetEntropyAscii(passphrase string) (float64, error) {
|
||||
var entropy float64
|
||||
length := len(passphrase)
|
||||
|
||||
wherechar := make([]int, MAX_CHARS)
|
||||
hist := make([]int, length)
|
||||
var histlen int
|
||||
|
||||
for i := 0; i < MAX_CHARS; i++ {
|
||||
wherechar[i] = -1
|
||||
}
|
||||
|
||||
for _, char := range []byte(passphrase) {
|
||||
if char < MIN_ASCII || char > 126 {
|
||||
return 0, fmt.Errorf("non-printable ASCII character encountered: %c", char)
|
||||
}
|
||||
if wherechar[char-MIN_ASCII] == -1 {
|
||||
wherechar[char-MIN_ASCII] = histlen
|
||||
histlen++
|
||||
}
|
||||
|
||||
hist[wherechar[char-MIN_ASCII]]++
|
||||
}
|
||||
|
||||
for i := 0; i < histlen; i++ {
|
||||
diff := float64(hist[i]) / float64(length)
|
||||
entropy -= diff * math.Log2(diff)
|
||||
}
|
||||
|
||||
return entropy, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* Return character distribution
|
||||
*/
|
||||
func GetDistributionUTF8(passphrase string) float64 {
|
||||
hash := make([]int, MAX_UTF8)
|
||||
var chars float64
|
||||
|
||||
for _, char := range passphrase {
|
||||
hash[char]++
|
||||
}
|
||||
|
||||
for i := 0; i < MAX_UTF8; i++ {
|
||||
if hash[i] > 0 {
|
||||
chars++
|
||||
}
|
||||
}
|
||||
return chars / (float64(MAX_UTF8) / 100)
|
||||
}
|
||||
|
||||
func GetDistributionAscii(passphrase string) float64 {
|
||||
hash := make([]int, MAX_CHARS)
|
||||
var chars float64
|
||||
|
||||
for _, char := range []byte(passphrase) {
|
||||
hash[int(char)-MIN_ASCII]++
|
||||
}
|
||||
|
||||
for i := 0; i < MAX_CHARS; i++ {
|
||||
if hash[i] > 0 {
|
||||
chars++
|
||||
}
|
||||
}
|
||||
return chars / (float64(MAX_CHARS) / 100)
|
||||
}
|
||||
|
||||
func GetDictMatch(passphrase string, dict *Dictionary) (bool, error) {
|
||||
if len(dict.Words) < MIN_DICT_LEN {
|
||||
return false, fmt.Errorf("provided dictionary is too small")
|
||||
}
|
||||
|
||||
lcpass := strings.ToLower(passphrase)
|
||||
|
||||
if dict.Submatch {
|
||||
for _, word := range dict.Words {
|
||||
if strings.Contains(strings.ToLower(word), lcpass) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, word := range dict.Words {
|
||||
if lcpass == strings.ToLower(word) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
318
lib.go
Normal file
318
lib.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package valpass
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
* Contains the raw dictionary data and some flags. Must be provided
|
||||
* by the user
|
||||
*/
|
||||
type Dictionary struct {
|
||||
Words []string // the actual dictionary
|
||||
Submatch bool // if true 'foo' would match 'foobar'
|
||||
}
|
||||
|
||||
/*
|
||||
* Options define how to operate the validation
|
||||
*/
|
||||
type Options struct {
|
||||
Compress int // minimum compression rate in percent
|
||||
CharDistribution float64 // minimum char distribution in percent
|
||||
Entropy float64 // minimum entropy value in bits/char
|
||||
Dictionary *Dictionary // if set, lookup given dictionary, the caller provides it
|
||||
UTF8 bool // if true work on unicode utf-8 space, not just bytes
|
||||
}
|
||||
|
||||
/*
|
||||
* Default validation config, a compromise of comfort and security, as always.
|
||||
*/
|
||||
const (
|
||||
MIN_ENTROPY float64 = 3.0
|
||||
MIN_COMPRESS int = 10
|
||||
MIN_DICT bool = false
|
||||
MIN_DIST float64 = 10.0
|
||||
MAX_UTF8 int = 2164864 // max characters encodable with utf8
|
||||
MAX_CHARS int = 95 // maximum printable US ASCII chars
|
||||
MIN_DICT_LEN int = 5000
|
||||
|
||||
// we start our ascii arrays at char(32), so to have max 95
|
||||
// elements in the slice, we subtract 32 from each ascii code
|
||||
MIN_ASCII byte = 32
|
||||
)
|
||||
|
||||
/*
|
||||
Stores the results of all validations.
|
||||
*/
|
||||
type Result struct {
|
||||
Ok bool // overall result
|
||||
DictionaryMatch bool // true if the password matched a dictionary entry
|
||||
Compress int // actual compression rate in percent
|
||||
CharDistribution float64 // actual character distribution in percent
|
||||
Entropy float64 // actual entropy value in bits/chars
|
||||
}
|
||||
|
||||
/*
|
||||
* Generic validation function. You should only call this function and
|
||||
* tune it using the Options struct. However, options are optional,
|
||||
* there are sensible defaults builtin
|
||||
*/
|
||||
func Validate(passphrase string, opts ...Options) (Result, error) {
|
||||
result := Result{Ok: true}
|
||||
|
||||
// defaults, see above
|
||||
options := Options{
|
||||
MIN_COMPRESS,
|
||||
MIN_DIST,
|
||||
MIN_ENTROPY,
|
||||
nil,
|
||||
false,
|
||||
}
|
||||
|
||||
if len(opts) == 1 {
|
||||
options = opts[0]
|
||||
}
|
||||
|
||||
// execute the actual validation checks
|
||||
|
||||
if options.Entropy > 0 {
|
||||
var entropy float64
|
||||
var err error
|
||||
|
||||
switch options.UTF8 {
|
||||
case true:
|
||||
entropy, err = GetEntropyUTF8(passphrase)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
default:
|
||||
entropy, err = GetEntropyAscii(passphrase)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
if entropy <= options.Entropy {
|
||||
result.Ok = false
|
||||
}
|
||||
|
||||
result.Entropy = entropy
|
||||
}
|
||||
|
||||
if options.Compress > 0 {
|
||||
compression, err := GetCompression([]byte(passphrase))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if compression >= options.Compress {
|
||||
result.Ok = false
|
||||
}
|
||||
|
||||
result.Compress = compression
|
||||
}
|
||||
|
||||
if options.CharDistribution > 0 {
|
||||
var dist float64
|
||||
|
||||
switch options.UTF8 {
|
||||
case true:
|
||||
dist = GetDistributionUTF8(passphrase)
|
||||
default:
|
||||
dist = GetDistributionAscii(passphrase)
|
||||
}
|
||||
if dist <= options.CharDistribution {
|
||||
result.Ok = false
|
||||
}
|
||||
|
||||
result.CharDistribution = dist
|
||||
}
|
||||
|
||||
if options.Dictionary != nil {
|
||||
match, err := GetDictMatch(passphrase, options.Dictionary)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if match {
|
||||
result.Ok = false
|
||||
result.DictionaryMatch = true
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* we compress with Flate level 9 (max) and see if the result is
|
||||
* smaller than the password, in which case it could be compressed and
|
||||
* contains repeating characters; OR it is larger than the password,
|
||||
* in which case it could NOT be compressed, which is what we want.
|
||||
*/
|
||||
func GetCompression(passphrase []byte) (int, error) {
|
||||
var b bytes.Buffer
|
||||
flater, _ := flate.NewWriter(&b, 9)
|
||||
|
||||
if _, err := flater.Write(passphrase); err != nil {
|
||||
return 0, fmt.Errorf("failed to write to flate writer: %w", err)
|
||||
}
|
||||
|
||||
if err := flater.Flush(); err != nil {
|
||||
return 0, fmt.Errorf("failed to flush flate writer: %w", err)
|
||||
}
|
||||
|
||||
if err := flater.Close(); err != nil {
|
||||
return 0, fmt.Errorf("failed to close flate writer: %w", err)
|
||||
}
|
||||
|
||||
// use floats to avoid division by zero panic
|
||||
length := float32(len(passphrase))
|
||||
compressed := float32(len(b.Bytes()))
|
||||
|
||||
if compressed >= length {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
percent := 100 - (compressed / (length / 100))
|
||||
|
||||
return int(percent), nil
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the entropy as bits/rune, where rune is a unicode char in
|
||||
* utf8 space.
|
||||
*/
|
||||
func GetEntropyUTF8(passphrase string) (float64, error) {
|
||||
var entropy float64
|
||||
length := len(passphrase)
|
||||
|
||||
wherechar := make([]int, MAX_UTF8)
|
||||
hist := make([]int, length)
|
||||
var histlen int
|
||||
|
||||
for i := 0; i < MAX_UTF8; i++ {
|
||||
wherechar[i] = -1
|
||||
}
|
||||
|
||||
for _, char := range passphrase {
|
||||
if wherechar[char] == -1 {
|
||||
wherechar[char] = histlen
|
||||
histlen++
|
||||
}
|
||||
|
||||
hist[wherechar[char]]++
|
||||
}
|
||||
|
||||
for i := 0; i < histlen; i++ {
|
||||
diff := float64(hist[i]) / float64(length)
|
||||
entropy -= diff * math.Log2(diff)
|
||||
}
|
||||
|
||||
return entropy, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Return the entropy as bits/char, where char is a printable char in
|
||||
US-ASCII space. Returns error if a char is non-printable.
|
||||
*/
|
||||
func GetEntropyAscii(passphrase string) (float64, error) {
|
||||
var entropy float64
|
||||
length := len(passphrase)
|
||||
|
||||
wherechar := make([]int, MAX_CHARS)
|
||||
hist := make([]int, length)
|
||||
var histlen int
|
||||
|
||||
for i := 0; i < MAX_CHARS; i++ {
|
||||
wherechar[i] = -1
|
||||
}
|
||||
|
||||
for _, char := range []byte(passphrase) {
|
||||
if char < MIN_ASCII || char > 126 {
|
||||
return 0, fmt.Errorf("non-printable ASCII character encountered: %c", char)
|
||||
}
|
||||
if wherechar[char-MIN_ASCII] == -1 {
|
||||
wherechar[char-MIN_ASCII] = histlen
|
||||
histlen++
|
||||
}
|
||||
|
||||
hist[wherechar[char-MIN_ASCII]]++
|
||||
}
|
||||
|
||||
for i := 0; i < histlen; i++ {
|
||||
diff := float64(hist[i]) / float64(length)
|
||||
entropy -= diff * math.Log2(diff)
|
||||
}
|
||||
|
||||
return entropy, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* Return character distribution in utf8 space
|
||||
*/
|
||||
func GetDistributionUTF8(passphrase string) float64 {
|
||||
hash := make([]int, MAX_UTF8)
|
||||
var chars float64
|
||||
|
||||
for _, char := range passphrase {
|
||||
hash[char]++
|
||||
}
|
||||
|
||||
for i := 0; i < MAX_UTF8; i++ {
|
||||
if hash[i] > 0 {
|
||||
chars++
|
||||
}
|
||||
}
|
||||
return chars / (float64(MAX_UTF8) / 100)
|
||||
}
|
||||
|
||||
/*
|
||||
* Return character distribution in US-ASCII space
|
||||
*/
|
||||
func GetDistributionAscii(passphrase string) float64 {
|
||||
hash := make([]int, MAX_CHARS)
|
||||
var chars float64
|
||||
|
||||
for _, char := range []byte(passphrase) {
|
||||
hash[char-MIN_ASCII]++
|
||||
}
|
||||
|
||||
for i := 0; i < MAX_CHARS; i++ {
|
||||
if hash[i] > 0 {
|
||||
chars++
|
||||
}
|
||||
}
|
||||
return chars / (float64(MAX_CHARS) / 100)
|
||||
}
|
||||
|
||||
/*
|
||||
* Return true if password can be found in given dictionary. This has
|
||||
* to be supplied by the user, we do NOT ship with a dictionary!
|
||||
*/
|
||||
func GetDictMatch(passphrase string, dict *Dictionary) (bool, error) {
|
||||
if len(dict.Words) < MIN_DICT_LEN {
|
||||
return false, fmt.Errorf("provided dictionary is too small")
|
||||
}
|
||||
|
||||
lcpass := strings.ToLower(passphrase)
|
||||
|
||||
if dict.Submatch {
|
||||
for _, word := range dict.Words {
|
||||
if strings.Contains(strings.ToLower(word), lcpass) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, word := range dict.Words {
|
||||
if lcpass == strings.ToLower(word) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
268
lib_test.go
Normal file
268
lib_test.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package valpass_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tlinden/valpass"
|
||||
)
|
||||
|
||||
type Tests struct {
|
||||
name string
|
||||
want bool
|
||||
negate bool
|
||||
opts valpass.Options
|
||||
}
|
||||
|
||||
var pass_random_good = []string{
|
||||
`5W@'"5b5=S)b]):xwBuEEu=,x}A46<aS`,
|
||||
`QAfwWn;]6ECn-(wZ-z7MxZL)zRA!TO%t`,
|
||||
`_5>}+RMm=FRj1a>r/!gG*3tQ>s<&Uh{I`,
|
||||
`~Dc6RHW?Yj"nDj)WaWAg#F<IsA[4j?G{`,
|
||||
`B;S0|lq:Ns#!{r1UaE0QG7R}tA'K'TNW`,
|
||||
`/~]-bT':EeA:dK&[+752EKvS@C1\U70d`,
|
||||
`3>cNh2_1(gB(DsA]m$4f/[hHf>{}E*\Q`,
|
||||
`Gr5#qF/!:ih?n7p|c?pN50IWc]5$+Q(]`,
|
||||
`S#(|irk.%U}[RBFZ2L;}XdDrmOU;SP<\`,
|
||||
`+L:T#&@ce[yqWZ0mTfm[D'#a=Ke[j7w'`,
|
||||
`:N8vqQ{Vb]@.y?\P2d8,)yHHE?>l|Gi_`,
|
||||
`^+s5,#2h<,?_s_Qsd2l;|D42TV3h{7M^`,
|
||||
`.^e#(l5$3}1l/-/Uk0,;t^Z[$X0,'h)O`,
|
||||
`]-xAyz-"P$98_Z[77@bmo9ZF)I#"Fa,6`,
|
||||
`HLkM\]n70U2qU)%Mp{gK@CHt,twiPzH%`,
|
||||
`wU2?2&4yx/7HuR@k:~]%/77,DyaNW|"Q`,
|
||||
`nb\ZmKT[J)%@=\nF9E2!%N-(+S}Lq95B`,
|
||||
`=+0b2[#FMcT~re:PifIWh$IL+>4uyBg1`,
|
||||
`xEm]AS#<]cgayw)>O/c<i,)BO[MC0qF,`,
|
||||
`EScP'NqM|7/>7e2'orRcS%x6v[sgX(!p`,
|
||||
`[.L|hvRRd.@)y?dH?Z46EcEa%/#!m39j`,
|
||||
`,$88R.N+C>+adUcw!D"11$H">:SKOiKp`,
|
||||
`8#uY]ByJ]iCNp?6-#;&m\pO[G>*!27ge`,
|
||||
`@UNu)/qMT{ekO(}qhh4!HI9\QRdrdh^'`,
|
||||
`FfoO3pLr_aoGC]lpvo"?RT3E@2f8-764`,
|
||||
`Us.dn65ZmF]M}e0Z!$!r0ex-/Z5nwx?J`,
|
||||
`e6p{,373[@c@/:CcQ"+(u^U"}^CzxRY.`,
|
||||
`kwpHHIqcsuWOio@jlIA2UbO63dkhh'|D`,
|
||||
`Yeq@?/Fq.}}"i2dXT=vR2C56hY9R)!_w`,
|
||||
`49ZFp54$@\kJ:D;[ZV(VcY|!&sI\O8;&`,
|
||||
`SK(ILi(q#FD-*uBbX4,;;1MM2</Md57(`,
|
||||
`TA"s$ix&5tlHqk^)182870PpW4X8jH_]`,
|
||||
`i"0&lJa?FA>]sD#:AVI)O7|L2x$$WI>(`,
|
||||
`_ao{jJ4Z0#njg}GCV{UpQsQubgb!F$-?`,
|
||||
`KtAkA~]c}0gj)H7.C6is*>50eIT$OW?*`,
|
||||
`Cv[o<mOux790E|[kNrh<n;S\1qU42kNN`,
|
||||
`Xj3:.j%kN?k_qYkNMUcQJe@[<K6v.4R~`,
|
||||
`aNRU-vO~LX~AwFbUe9t}[WK*3r;PGc/b`,
|
||||
`|E|Jl]YjM<4gNh0b1%)^SP:_;%#A\b4b`,
|
||||
`Q4#U1/2'5V[_CzYdm7OSZJJE-cSf9^cG`,
|
||||
`!jK6zb4)pGrAL/|w|#$a}O||C(0:>:.6`,
|
||||
`7t&/B36m8IeM*^e}.)-/X+M8r7'\q:cu`,
|
||||
`0iw8o:,bQJ=;d&<CK6?UcaqggQ&r!~%E`,
|
||||
`^/FPWoYDwij"B//t}|3aV6vaLI$\3E4%`,
|
||||
`^sJ~J.>r?$u'0J,2VD6$Fou,[D~q_vzO`,
|
||||
`rVV\wI.L@AAI?+;lU@gnmxKFiob>?s!8`,
|
||||
`o]K;x.6$u|^M7kL:lM"13a@rQiD1IJoh`,
|
||||
`xM;!)\?;=!lH]|j^jzGG}?6v*O:s~*o=`,
|
||||
`f"7#AnRu*b9_=sk^^mMX?+K^ElemvJ(<`,
|
||||
`L4WSx8ocC1$74A4#zF!*h8Bq_Eq/1s7s`,
|
||||
}
|
||||
|
||||
var pass_diceware_good = []string{
|
||||
`abutting Eucharist dramatized unlearns`,
|
||||
`Terrence decorates dwarfed saucing`,
|
||||
`swamping nauseated tapioca ascribe`,
|
||||
`insatiably ensconcing royally Clarice`,
|
||||
`inshore watchdog blunderers methods`,
|
||||
`Plasticine brotherly prances dryness`,
|
||||
`rustproof flipper commodity nudging`,
|
||||
`unburdened frostings adapter vivider`,
|
||||
`facile Niamey begrudge menage`,
|
||||
`nightcaps miniseries Hannibal strongly`,
|
||||
`foresails produces sufficing cannibal`,
|
||||
`berths allowing Lewiston sounds`,
|
||||
`hazier Hockney snobbier redefines`,
|
||||
`Monroe castaways narwhals roadbed`,
|
||||
`schuss Trieste assist kebobs`,
|
||||
`anteater pianos damping attaining`,
|
||||
`desisting colossus refused Madagascan`,
|
||||
`misguiding urinalyses moonscapes Taiping`,
|
||||
`fracases Indies dishwasher crimsons`,
|
||||
`doorman Kleenexes hostessed stooped`,
|
||||
`telephoto boozing monoxide Asiago`,
|
||||
`completed dogfish rawboned curvacious`,
|
||||
`physics virtually rocketing relevant`,
|
||||
`infantile sharpest buckler gazillions`,
|
||||
`forbids midlands accosts furniture`,
|
||||
`concocts Alcestis nitpicker Hindustan`,
|
||||
`heirlooms wending Borodin billows`,
|
||||
`commotion absinthe chilis drainer`,
|
||||
`prerecord brokerages colonel implied`,
|
||||
`spoons abates swathed Pocono`,
|
||||
`speedy poultices Smollett tracing`,
|
||||
`viragoes unwind gasped earache`,
|
||||
`rulings Mencken damasking matched`,
|
||||
`Sarajevo footbridge stables furloughed`,
|
||||
`proclaimed baffling carefully Anatolia`,
|
||||
`Cecily Nicaraguan excrete lobbed`,
|
||||
`enfold cranny tearjerker blazon`,
|
||||
`bucketed Corneille eclectic Maurine`,
|
||||
`Berwick gasohol slices bonkers`,
|
||||
`swearers iodized Ohioans warden`,
|
||||
`Cortez insular several phloem`,
|
||||
`assented insolvent beguile aquaplane`,
|
||||
`commend trails Amazon clambering`,
|
||||
`excretory greatness plackets creeks`,
|
||||
`transistor exclusion inboxes sidling`,
|
||||
`cherries elongating Lollard piques`,
|
||||
`heartening orbiting zombie revile`,
|
||||
`reconcile completes roughs innocence`,
|
||||
`quickness Cheever Thimbu scours`,
|
||||
`hobble piteously precepts sorest`,
|
||||
`braving shirted backstage Taiping`,
|
||||
}
|
||||
|
||||
var pass_worst_bad = []string{
|
||||
`123456`, `charlie`, `summer`, `sophie`, `merlin`,
|
||||
`password`, `aa123456`, `George`, `Ferrari`, `cookie`,
|
||||
`123456789`, `donald`, `Harley`, `Cheese`, `ashley`,
|
||||
`12345678`, `password1`, `222222`, `Computer`, `bandit`,
|
||||
`12345`, `qwerty123`, `Jessica`, `jesus`, `killer`,
|
||||
`111111`, `letmein`, `ginger`, `Corvette`, `aaaaaa`,
|
||||
`1234567`, `zxcvbnm`, `abcdef`, `Mercedes`, `1q2w3e`,
|
||||
`sunshine`, `login`, `Jordan`, `flower`, `zaq1zaq1`,
|
||||
`qwerty`, `starwars`, `55555`, `Blahblah`, `mustang`,
|
||||
`iloveyou`, `121212`, `Tigger`, `Maverick`, `test`,
|
||||
`princess`, `bailey`, `Joshua`, `Hello`, `hockey`,
|
||||
`admin`, `freedom`, `Pepper`, `loveme`, `dallas`,
|
||||
`welcome`, `shadow`, `Robert`, `nicole`, `whatever`,
|
||||
`666666`, `passw0rd`, `Matthew`, `hunter`, `admin123`,
|
||||
`abc123`, `master`, `12341234`, `amanda`, `michael`,
|
||||
`football`, `baseball`, `Andrew`, `jennifer`, `liverpool`,
|
||||
`123123`, `buster`, `lakers`, `banana`, `querty`,
|
||||
`monkey`, `Daniel`, `andrea`, `chelsea`, `william`,
|
||||
`654321`, `Hannah`, `1qaz2wsx`, `ranger`, `soccer`,
|
||||
`!@#$%^&*`, `Thomas`, `starwars`, `trustno1`, `london`,
|
||||
}
|
||||
|
||||
var pass_dict_bad = []string{
|
||||
`clued`, `lads`, `stifle`,
|
||||
`receptivity`, `apprehends`, `accounts`,
|
||||
`putts`, `spurt`, `sideswipe`,
|
||||
`dabbed`, `goatskin`, `nooks`,
|
||||
`sulkiness`, `worships`, `coevals`,
|
||||
`entwining`, `sportscasters`, `pew`,
|
||||
`horse`, `daybeds`, `booklet`,
|
||||
`Suzette`, `abbreviate`, `stubborn`,
|
||||
`govern`, `ageism`, `refereeing`,
|
||||
`dents`, `Wyeth`, `concentric`,
|
||||
`Kamehameha`, `grosser`, `belie`,
|
||||
`wherefore`, `president`, `pipit`,
|
||||
`pinholes`, `mummifying`, `quartermasters`,
|
||||
`fruitlessness`, `seafarer`, `Einsteins`,
|
||||
`stomping`, `glided`, `retried`,
|
||||
`effected`, `ministry`,
|
||||
}
|
||||
|
||||
var opts_std = valpass.Options{
|
||||
Compress: valpass.MIN_COMPRESS,
|
||||
CharDistribution: valpass.MIN_DIST,
|
||||
Entropy: valpass.MIN_ENTROPY,
|
||||
Dictionary: nil,
|
||||
UTF8: false,
|
||||
}
|
||||
|
||||
var opts_dict = valpass.Options{
|
||||
Compress: valpass.MIN_COMPRESS,
|
||||
CharDistribution: valpass.MIN_DIST,
|
||||
Entropy: valpass.MIN_ENTROPY,
|
||||
Dictionary: &valpass.Dictionary{Words: ReadDict("t/american-english")},
|
||||
UTF8: false,
|
||||
}
|
||||
|
||||
var goodtests = []Tests{
|
||||
{
|
||||
name: "checkgood",
|
||||
want: true,
|
||||
opts: opts_std,
|
||||
},
|
||||
{
|
||||
name: "checkgood-dict",
|
||||
want: true,
|
||||
opts: opts_dict,
|
||||
},
|
||||
}
|
||||
|
||||
var badtests = []Tests{
|
||||
{
|
||||
name: "checkbad",
|
||||
want: false,
|
||||
opts: opts_std,
|
||||
},
|
||||
{
|
||||
name: "checkbad-dict",
|
||||
want: false,
|
||||
opts: opts_dict,
|
||||
},
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range goodtests {
|
||||
for _, pass := range pass_random_good {
|
||||
CheckPassword(t, pass, tt.name, tt.want, tt.opts)
|
||||
}
|
||||
|
||||
for _, pass := range pass_diceware_good {
|
||||
CheckPassword(t, pass, tt.name, tt.want, tt.opts)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tt := range badtests {
|
||||
for _, pass := range pass_worst_bad {
|
||||
CheckPassword(t, pass, tt.name, tt.want, tt.opts)
|
||||
}
|
||||
|
||||
for _, pass := range pass_dict_bad {
|
||||
CheckPassword(t, pass, tt.name, tt.want, tt.opts)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func CheckPassword(t *testing.T, password string,
|
||||
name string, want bool, opts valpass.Options) {
|
||||
|
||||
result, err := valpass.Validate(password, opts)
|
||||
if err != nil {
|
||||
t.Errorf("test %s failed with error: %s\n", name, err)
|
||||
}
|
||||
|
||||
if want && !result.Ok {
|
||||
t.Errorf("test %s failed. pass: %s, want: %t, got: %t, dict: %t\nresult: %v\n",
|
||||
name, password, want, result.Ok, result.DictionaryMatch, result)
|
||||
}
|
||||
|
||||
if !want && result.Ok {
|
||||
t.Errorf("test %s failed. pass: %s, want: %t, got: %t, dict: %t\nresult: %v\n",
|
||||
name, password, want, result.Ok, result.DictionaryMatch, result)
|
||||
}
|
||||
}
|
||||
|
||||
func ReadDict(path string) []string {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
104332
t/american-english
Normal file
104332
t/american-english
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user