From 6893ae54890933fbc089a923b8f2581ba1f803dd Mon Sep 17 00:00:00 2001 From: tyler Date: Wed, 24 May 2023 16:44:14 -0400 Subject: [PATCH] Added password generator library --- password/password.go | 65 +++++++++++++++ password/password_test.go | 170 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 password/password.go create mode 100644 password/password_test.go diff --git a/password/password.go b/password/password.go new file mode 100644 index 0000000..7b66038 --- /dev/null +++ b/password/password.go @@ -0,0 +1,65 @@ +package password + +import ( + "crypto/rand" + "fmt" + "math/big" +) + +var upperRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") +var lowerRunes = []rune("abcdefghijklmnopqrstuvwxyz") +var numberRunes = []rune("0123456789") +var symbolRunes = []rune("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") + +// Generate generates a random string with the specified size containing runes from the designated sets. +func Generate(upper, lower, number, symbol bool, size int) (string, error) { + if size <= 0 { + return "", fmt.Errorf("password: Password size must be greater than 0") + } + + runeSet := []rune{} + + if upper { + runeSet = append(runeSet, upperRunes...) + } + if lower { + runeSet = append(runeSet, lowerRunes...) + } + if number { + runeSet = append(runeSet, numberRunes...) + } + if symbol { + runeSet = append(runeSet, symbolRunes...) + } + + if len(runeSet) == 0 { + return "", fmt.Errorf("password: No runes selected for password") + } + + password := make([]rune, size) + for i := 0; i < size; i++ { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(runeSet)))) + if err != nil { + return "", fmt.Errorf("password: Error while generating random number: %v", err) + } + password[i] = runeSet[n.Int64()] + } + + return string(password), nil +} + +// Random creates an array of random bytes with specified size. +// If used to generate password salt, then size of 16 bytes (128 bits) is recommended. +func Random(size int) ([]byte, error) { + if size < 0 { + return nil, fmt.Errorf("password: Size cannot be less than zero") + } + random := make([]byte, size) + + _, err := rand.Read(random) + if err != nil { + return nil, fmt.Errorf("password: Error while reading random bytes: %v", err) + } + + return random, nil +} diff --git a/password/password_test.go b/password/password_test.go new file mode 100644 index 0000000..fa414a3 --- /dev/null +++ b/password/password_test.go @@ -0,0 +1,170 @@ +package password + +import ( + "strings" + "testing" +) + +func TestRuneCount(t *testing.T) { + tests := []struct { + name string + arg []rune + want int + }{ + {"upperRunes", upperRunes, 26}, + {"lowerRunes", lowerRunes, 26}, + {"numberRunes", numberRunes, 10}, + {"symbolRunes", symbolRunes, 32}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := len(tt.arg) + if got != tt.want { + t.Fatalf("len(%s) = %d, want %d", tt.name, got, tt.want) + } + }) + } +} + +func TestGenerateReturnsPasswordWithCorrectSize(t *testing.T) { + tests := []struct { + upper bool + lower bool + number bool + symbol bool + size int + }{ + {true, true, true, true, 3}, + {true, true, true, true, 45}, + {true, false, false, false, 20}, + {false, true, true, false, 100}, + } + + for _, tt := range tests { + password, err := Generate(tt.upper, tt.lower, tt.number, tt.symbol, tt.size) + if err != nil { + t.Fatalf("Want Generate() err = nil, got %v", err) + } + + len := len(password) + if len != tt.size { + t.Fatalf("len(password) = %d want %d", len, tt.size) + } + } +} + +func TestGenerateReturnsErrorWithBadInput(t *testing.T) { + tests := []struct { + upper bool + lower bool + number bool + symbol bool + size int + }{ + {false, false, false, false, 3}, + {true, true, true, true, 0}, + {true, false, false, false, -20}, + } + + for _, tt := range tests { + _, err := Generate(tt.upper, tt.lower, tt.number, tt.symbol, tt.size) + if err == nil { + t.Fatalf("Generate() err = nil, want not nil") + } + } +} + +func TestGenerateReturnsPasswordWithCorrectRunes(t *testing.T) { + tests := []struct { + upper bool + lower bool + number bool + symbol bool + size int + }{ + {true, false, false, false, 100}, + {false, true, false, false, 100}, + {false, false, true, false, 100}, + {false, false, false, true, 100}, + {true, true, false, false, 100}, + {true, false, true, false, 100}, + {true, false, false, true, 100}, + {false, true, true, false, 100}, + {false, true, false, true, 100}, + {false, false, true, true, 100}, + {true, true, true, false, 100}, + {true, true, false, true, 100}, + {true, false, true, true, 100}, + {false, true, true, true, 100}, + {true, true, true, true, 100}, + } + + for _, tt := range tests { + password, err := Generate(tt.upper, tt.lower, tt.number, tt.symbol, tt.size) + if err != nil { + t.Fatalf("Want Generate() err = nil, got %v", err) + } + + for _, r := range password { + if !tt.upper && strings.ContainsRune(string(upperRunes), r) { + t.Fatalf("Got password with upper rune, want no upper runes") + } + + if !tt.lower && strings.ContainsRune(string(lowerRunes), r) { + t.Fatalf("Got password with lower rune, want no lower runes") + } + + if !tt.number && strings.ContainsRune(string(numberRunes), r) { + t.Fatalf("Got password with number rune, want no number runes") + } + + if !tt.symbol && strings.ContainsRune(string(symbolRunes), r) { + t.Fatalf("Got password with symbol rune, want no symbol runes") + } + } + } +} + +func TestRandomReturnsCorrectNumberOfBytes(t *testing.T) { + tests := []struct { + size int + }{ + {0}, + {3}, + {45}, + {20}, + {100}, + } + + for _, tt := range tests { + randomBytes, err := Random(tt.size) + if err != nil { + t.Fatalf("Want Random() err = nil, got %v", err) + } + + len := len(randomBytes) + if len != tt.size { + t.Fatalf("len(randomBytes) = %d, want %d", len, tt.size) + } + } +} + +func TestRandomReturnsErrorWithNegativeSize(t *testing.T) { + tests := []struct { + size int + }{ + {-1}, + {-3}, + {-45}, + {-20}, + {-100}, + } + + for _, tt := range tests { + _, err := Random(tt.size) + if err == nil { + t.Fatalf("Random() err = nil, want not nil") + } + } +}