Reconstruct KeePassGO repository

This commit is contained in:
Joe Julian
2026-03-29 11:04:38 -07:00
commit a2a8fcbd14
34 changed files with 14041 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
package passwords
import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"strings"
)
const (
lowercaseChars = "abcdefghijkmnopqrstuvwxyz"
uppercaseChars = "ABCDEFGHJKLMNPQRSTUVWXYZ"
digitChars = "23456789"
symbolChars = "!@#$%^&*()-_=+[]{}<>?/."
)
var ErrImpossibleProfile = errors.New("impossible password profile")
type Profile struct {
Name string
Length int
Lowercase bool
Uppercase bool
Digits bool
Symbols bool
MinLowercase int
MinUppercase int
MinDigits int
MinSymbols int
ExcludeSimilar bool
}
func DefaultProfiles() map[string]Profile {
return map[string]Profile{
"strong": {
Name: "strong",
Length: 24,
Lowercase: true,
Uppercase: true,
Digits: true,
Symbols: true,
MinLowercase: 2,
MinUppercase: 2,
MinDigits: 2,
MinSymbols: 2,
ExcludeSimilar: true,
},
"memorable": {
Name: "memorable",
Length: 20,
Lowercase: true,
Uppercase: true,
Digits: true,
Symbols: false,
MinLowercase: 4,
MinUppercase: 2,
MinDigits: 2,
ExcludeSimilar: true,
},
}
}
func Generate(profile Profile) (string, error) {
if err := validateProfile(profile); err != nil {
return "", err
}
var chars []byte
var pool strings.Builder
if profile.Lowercase {
pool.WriteString(lowercaseChars)
chars = append(chars, mustRandomChars(lowercaseChars, profile.MinLowercase)...)
}
if profile.Uppercase {
pool.WriteString(uppercaseChars)
chars = append(chars, mustRandomChars(uppercaseChars, profile.MinUppercase)...)
}
if profile.Digits {
pool.WriteString(digitChars)
chars = append(chars, mustRandomChars(digitChars, profile.MinDigits)...)
}
if profile.Symbols {
pool.WriteString(symbolChars)
chars = append(chars, mustRandomChars(symbolChars, profile.MinSymbols)...)
}
allChars := pool.String()
for len(chars) < profile.Length {
ch, err := randomChar(allChars)
if err != nil {
return "", err
}
chars = append(chars, ch)
}
if err := shuffle(chars); err != nil {
return "", err
}
return string(chars), nil
}
func validateProfile(profile Profile) error {
if profile.Length <= 0 {
return fmt.Errorf("%w: length must be positive", ErrImpossibleProfile)
}
required := profile.MinLowercase + profile.MinUppercase + profile.MinDigits + profile.MinSymbols
if required > profile.Length {
return fmt.Errorf("%w: minimum character counts exceed length", ErrImpossibleProfile)
}
if profile.MinLowercase > 0 && !profile.Lowercase {
return fmt.Errorf("%w: lowercase disabled with lowercase minimum", ErrImpossibleProfile)
}
if profile.MinUppercase > 0 && !profile.Uppercase {
return fmt.Errorf("%w: uppercase disabled with uppercase minimum", ErrImpossibleProfile)
}
if profile.MinDigits > 0 && !profile.Digits {
return fmt.Errorf("%w: digits disabled with digit minimum", ErrImpossibleProfile)
}
if profile.MinSymbols > 0 && !profile.Symbols {
return fmt.Errorf("%w: symbols disabled with symbol minimum", ErrImpossibleProfile)
}
if !profile.Lowercase && !profile.Uppercase && !profile.Digits && !profile.Symbols {
return fmt.Errorf("%w: no character sets enabled", ErrImpossibleProfile)
}
return nil
}
func mustRandomChars(chars string, count int) []byte {
if count <= 0 {
return nil
}
out := make([]byte, 0, count)
for i := 0; i < count; i++ {
ch, err := randomChar(chars)
if err != nil {
panic(err)
}
out = append(out, ch)
}
return out
}
func randomChar(chars string) (byte, error) {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
if err != nil {
return 0, fmt.Errorf("random index: %w", err)
}
return chars[n.Int64()], nil
}
func shuffle(chars []byte) error {
for i := len(chars) - 1; i > 0; i-- {
n, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
if err != nil {
return fmt.Errorf("shuffle password: %w", err)
}
j := int(n.Int64())
chars[i], chars[j] = chars[j], chars[i]
}
return nil
}
+97
View File
@@ -0,0 +1,97 @@
package passwords
import (
"strings"
"testing"
)
func TestGenerateRespectsProfileRequirements(t *testing.T) {
t.Parallel()
profile := Profile{
Name: "strong",
Length: 24,
Lowercase: true,
Uppercase: true,
Digits: true,
Symbols: true,
MinLowercase: 2,
MinUppercase: 2,
MinDigits: 2,
MinSymbols: 2,
ExcludeSimilar: true,
}
password, err := Generate(profile)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if len(password) != 24 {
t.Fatalf("len(password) = %d, want 24", len(password))
}
if countFromSet(password, lowercaseChars) < 2 {
t.Fatalf("lowercase count in %q is too small", password)
}
if countFromSet(password, uppercaseChars) < 2 {
t.Fatalf("uppercase count in %q is too small", password)
}
if countFromSet(password, digitChars) < 2 {
t.Fatalf("digit count in %q is too small", password)
}
if countFromSet(password, symbolChars) < 2 {
t.Fatalf("symbol count in %q is too small", password)
}
if strings.ContainsAny(password, "O0Il1") {
t.Fatalf("password %q contains excluded similar characters", password)
}
}
func TestGenerateRejectsImpossibleProfiles(t *testing.T) {
t.Parallel()
_, err := Generate(Profile{
Name: "bad",
Length: 6,
Lowercase: true,
Uppercase: true,
Digits: true,
Symbols: true,
MinLowercase: 2,
MinUppercase: 2,
MinDigits: 2,
MinSymbols: 2,
})
if err == nil {
t.Fatal("Generate() error = nil, want impossible profile error")
}
}
func TestProfileSetReturnsNamedProfiles(t *testing.T) {
t.Parallel()
set := DefaultProfiles()
profile, ok := set["strong"]
if !ok {
t.Fatalf("DefaultProfiles()[\"strong\"] missing")
}
if profile.Length < 20 || !profile.Symbols {
t.Fatalf("strong profile = %#v, want a strong reusable profile", profile)
}
}
func countFromSet(password, chars string) int {
count := 0
for _, r := range password {
if strings.ContainsRune(chars, r) {
count++
}
}
return count
}