mirror of
https://codeberg.org/scip/valpass.git
synced 2025-12-16 12:11:00 +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