Reconstruct KeePassGO repository
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user