~seirdy/moac

8d9631dfc1a01f6ef0ffc50c8359f19709f83fb3 — Rohan Kumar 11 days ago 05e9bf8 master
Tests: add tests for pwgen
3 files changed, 134 insertions(+), 15 deletions(-)

M givens_test.go
M pwgen.go
A pwgen_test.go
M givens_test.go => givens_test.go +11 -9
@@ 1,15 1,17 @@
package moac
package moac_test

import (
	"math"
	"testing"

	"git.sr.ht/~seirdy/moac"
)

const margin = 0.05 // acceptable error

var tests = []struct {
var givensTests = []struct {
	name    string
	given   Givens
	given   moac.Givens
	quantum bool
	// Expected values should be within 10% error
	expectedBF float64


@@ 18,7 20,7 @@ var tests = []struct {
	{ // from README
		name:    "hitchhiker",
		quantum: true,
		given: Givens{
		given: moac.Givens{
			Mass:     5.97e24,
			Time:     1.45e17,
			Password: "v¢JÊÙúQ§4mÀÛªZûYÍé©mËiÐ× \"½J6y.ñíí'è¦ïϵ°~",


@@ 29,7 31,7 @@ var tests = []struct {
	{ // from blog post: https://seirdy.one/2021/01/12/password-strength.html
		name:    "universe",
		quantum: false,
		given: Givens{
		given: moac.Givens{
			// default mass is the mass of the observable universe
			Entropy: 510,
		},


@@ 39,9 41,9 @@ var tests = []struct {
}

func TestBruteForceability(t *testing.T) {
	for _, test := range tests {
	for _, test := range givensTests {
		t.Run(test.name, func(t *testing.T) {
			got, err := BruteForceability(&test.given, test.quantum)
			got, err := moac.BruteForceability(&test.given, test.quantum)
			if err != nil {
				t.Fatalf("BruteForceability() = %v", err)
			}


@@ 53,9 55,9 @@ func TestBruteForceability(t *testing.T) {
}

func TestMinEntropy(t *testing.T) {
	for _, test := range tests {
	for _, test := range givensTests {
		t.Run(test.name, func(t *testing.T) {
			got, err := MinEntropy(&test.given, test.quantum)
			got, err := moac.MinEntropy(&test.given, test.quantum)
			if err != nil {
				t.Fatalf("MinEntropy() = %v", err)
			}

M pwgen.go => pwgen.go +11 -6
@@ 85,11 85,7 @@ func genpwFromGivenCharsets(charsetsGiven [][]rune, entropy float64) (string, er
	return pw, nil
}

// GenPW generates a random password using characters from the charsets enumerated by charsetsWanted.
// At least one element of each charset is used.
// Available charsets include "lowercase", "uppercase", "numbers", "symbols", and "extendASCII".
// Anything else will be treated as a string containing elements of a new custom charset to use.
func GenPW(charsetsWanted []string, entropyWanted float64) (string, error) {
func buildCharsets(charsetsEnumerated *[]string) [][]rune {
	var charsetsGiven [][]rune
	charsets := map[string][]rune{
		"lowercase":     []rune(lowercase),


@@ 98,12 94,21 @@ func GenPW(charsetsWanted []string, entropyWanted float64) (string, error) {
		"symbols":       []rune(symbols),
		"extendedASCII": []rune(extendedASCII),
	}
	for _, charset := range charsetsWanted {
	for _, charset := range *charsetsEnumerated {
		if charsetRunes, found := charsets[charset]; found {
			charsetsGiven = append(charsetsGiven, charsetRunes)
		} else {
			charsetsGiven = append(charsetsGiven, []rune(charset))
		}
	}
	return charsetsGiven
}

// GenPW generates a random password using characters from the charsets enumerated by charsetsWanted.
// At least one element of each charset is used.
// Available charsets include "lowercase", "uppercase", "numbers", "symbols", and "extendASCII".
// Anything else will be treated as a string containing elements of a new custom charset to use.
func GenPW(charsetsEnumerated []string, entropyWanted float64) (string, error) {
	charsetsGiven := buildCharsets(&charsetsEnumerated)
	return genpwFromGivenCharsets(charsetsGiven, entropyWanted)
}

A pwgen_test.go => pwgen_test.go +112 -0
@@ 0,0 1,112 @@
package moac

import (
	"testing"
)

var pwgenTests = []struct {
	name           string
	charsetsWanted []string
	entropyWanted  float64
}{
	{
		name:           "everything",
		charsetsWanted: []string{"lowercase", "uppercase", "numbers", "symbols", "extendedASCII", "世界🧛"},
		entropyWanted:  256,
	},
	{
		name:           "alnum",
		charsetsWanted: []string{"lowercase", "uppercases", "numbers"},
		entropyWanted:  64,
	},
	{
		name: "tinyPassword",
		charsetsWanted: []string{
			"uppercase", "numbers", "lowercase", "numbers", "numbers", "symbols", "lowercase", "extendedASCII", "🧛",
		},
		entropyWanted: 1,
	},
	{
		name: "multipleCustomCharsets",
		charsetsWanted: []string{
			"uppercase",
			"numbers",
			"lowercase",
			"𓂸",
			"عظ؆ص",
			"ἀἁἂἃἄἅἆἇἈἉἊἋἌἍἎἏἐἑἒἓἔἕἘἙἚἛἜἝἠἡἢἣἤἥἦἧἨἩἪἫἬἭἮἯἰἱἲἳἴἵἶἷἸἹἺἻἼἽἾἿὀὁὂὃὄὅὈὉὊὋὌὍὐὑὒὓὔὕὖὗὙὛὝὟὠὡὢὣὤὥὦὧὨὩὪὫὬὭὮὯὰάὲέὴήὶίὸόὺύὼώᾀᾁᾂᾃᾄᾅᾆᾇᾈᾉᾊᾋᾌᾍᾎᾏᾐᾑᾒᾓᾔᾕᾖᾗᾘᾙᾚᾛᾜᾝᾞᾟᾠᾡᾢᾣᾤᾥᾦᾧᾨᾩᾪᾫᾬᾭᾮᾯᾰᾱᾲᾳᾴᾶᾷᾸᾹᾺΆᾼ᾽ι᾿῀῁ῂῃῄῆῇῈΈῊΉῌ῍῎῏ῐῑῒΐῖῗῘῙῚΊ῝῞῟ῠῡῢΰῤῥῦῧῨῩῪΎῬ῭΅`ῲῳῴῶῷῸΌῺΏῼ",
		},
		entropyWanted: 256,
	},
}

// second param should include at least one element of the first param.
func latterUsesFormer(former []rune, latter *[]rune) bool {
	for _, char := range former {
		for _, pwChar := range *latter {
			if pwChar == char {
				return true
			}
		}
	}
	return false
}

func pwUsesEachCharset(charsets *[][]rune, password *[]rune) (string, bool) {
	for _, charset := range *charsets {
		if !latterUsesFormer(charset, password) {
			return string(charset), false
		}
	}
	return "", true
}

func pwOnlyUsesAllowedRunes(charsets *[][]rune, password *[]rune) (rune, bool) {
	var allowedChars string
	for _, charset := range *charsets {
		allowedChars += string(charset)
	}
	allowedRunes := []rune(allowedChars)

	charSpace := len(allowedRunes)
	for _, pwChar := range *password {
		for i, char := range allowedRunes {
			if pwChar == char {
				break
			} else if i == charSpace-1 {
				return pwChar, false
			}
		}
	}
	return ' ', true
}

func TestGenPw(t *testing.T) {
	for _, test := range pwgenTests {
		charsetsWanted := &test.charsetsWanted
		entropyWanted := &test.entropyWanted
		t.Run(test.name, func(t *testing.T) {
			charsets := buildCharsets(charsetsWanted)
			// we're dealing with random passwords; try each testcase multiple times
			for i := 0; i < 10; i++ {
				password, err := GenPW(*charsetsWanted, *entropyWanted)
				if err != nil {
					t.Fatalf("GenPW() = %v", err)
				}
				pwRunes := []rune(password)
				if unusedCharset, validPW := pwUsesEachCharset(&charsets, &pwRunes); !validPW {
					t.Errorf("GenPW() = %s; didn't use each charset\nunused charset: %s", password, unusedCharset)
					i = 10
				}
				if invalidRune, validPW := pwOnlyUsesAllowedRunes(&charsets, &pwRunes); !validPW {
					t.Errorf("GenPW() = %s; used invalid character \"%v\"", password, string(invalidRune))
					i = 10
				}
				if entropy := calculateEntropy(password); entropy < *entropyWanted {
					t.Errorf("GenPW() = %s; entropy was %.3g, wanted %.3g", password, entropy, *entropyWanted)
					i = 10
				}
			}
		})
	}
}