1
0
mirror of https://github.com/Luzifer/password.git synced 2024-09-19 18:32:57 +00:00
password/lib/generator.go

153 lines
4.5 KiB
Go

// Package securepassword implements a password generator and check.
package securepassword // import "github.com/Luzifer/password/lib"
import (
"errors"
"fmt"
"math/rand"
"regexp"
"strings"
"time"
)
// SecurePassword provides methods for generating secure passwords and
// checking the security requirements of passwords
type SecurePassword struct {
characterTables map[string]string
insecurePattern []string
badCharacters []string
}
var (
// ErrLengthTooLow represents an error thrown if the password will
// never be able match the security considerations in this package
ErrLengthTooLow = errors.New("Passwords with a length lower than 4 will never meet the security requirements")
)
// NewSecurePassword initializes a new SecurePassword generator
func NewSecurePassword() *SecurePassword {
return &SecurePassword{
characterTables: map[string]string{
"numeric": "0123456789",
"simple": "abcdefghijklmnopqrstuvwxyz",
"special": "!#$%&()*+,-_./:;=?@[]^{}~|",
},
insecurePattern: []string{
"abcdefghijklmnopqrstuvwxyz", // Alphabet
"zyxwvutsrqponmlkjihgfedcba", // Alphabet reversed
"01234567890", // Numeric increasing
"09876543210", // Numeric decreasing
"qwertzuiopasdfghjklyxcvbnm", // German keyboard layout
"mnbvcxylkjhgfdsapoiuztrewq", // German keyboard layout reversed
"qwertyuiopasdfghjklzxcvbnm", // US keyboard layout
"mnbvcxzlkjhgfdsapoiuytrewq", // US keyboard layour reversed
"789_456_123_147_258_369_159_753", // Numpad patterns
},
badCharacters: []string{"I", "l", "0", "O", "B", "8"}, // Characters that could lead to confusion due to font
}
}
// GeneratePassword generates a new password with a given length and
// optional special characters in it. The password is automatically
// checked against CheckPasswordSecurity in order to only deliver secure
// passwords.
func (s *SecurePassword) GeneratePassword(length int, special bool) (string, error) {
// Sanity check
if length < 4 {
return "", ErrLengthTooLow
}
characterTable := strings.Join([]string{
s.characterTables["simple"],
strings.ToUpper(s.characterTables["simple"]),
s.characterTables["numeric"],
}, "")
if special {
characterTable = strings.Join([]string{characterTable, s.characterTables["special"]}, "")
}
password := ""
rand.Seed(time.Now().UnixNano())
for {
char := string(characterTable[rand.Intn(len(characterTable))])
if strings.Contains(strings.Join(s.badCharacters, ""), char) {
continue
}
password = fmt.Sprintf("%s%s",
password,
char,
)
if len(password) == length {
if s.CheckPasswordSecurity(password, special) {
break
}
password = ""
}
}
return password, nil
}
// CheckPasswordSecurity executes three checks to ensure the passwords
// meet the security considerations in this package:
//
// 1. The password may not contain pattern found on the keyboard or in alphabet
// 2. The password must have 3 or 4 different character groups in it
// 3. The password may not have repeating characters
func (s *SecurePassword) CheckPasswordSecurity(password string, needsSpecialCharacters bool) bool {
return !s.hasInsecurePattern(password) &&
s.matchesBasicSecurity(password, needsSpecialCharacters) &&
!s.hasCharacterRepetition(password)
}
func (s *SecurePassword) hasInsecurePattern(password string) bool {
for i := 0; i < len(password)-3; i++ {
slice := password[i : i+3] // Extract an 3 char slice to check
for _, pattern := range s.insecurePattern {
if strings.Contains(pattern, slice) {
return true
}
if strings.Contains(strings.ToUpper(pattern), slice) {
return true
}
}
}
return false
}
func (s *SecurePassword) matchesBasicSecurity(password string, needsSpecialCharacters bool) bool {
bytePassword := []byte(password)
// Passwords does require numeric characters
if !regexp.MustCompile(`[0-9]`).Match(bytePassword) {
return false
}
// Passwords does require lowercase alphabetical characters
if !regexp.MustCompile(`[a-z]`).Match(bytePassword) {
return false
}
// Passwords does require uppercase alphabetical characters
if !regexp.MustCompile(`[A-Z]`).Match(bytePassword) {
return false
}
// If request was to require special characters check for their existance
if needsSpecialCharacters && !regexp.MustCompile(`[^a-zA-Z0-9]`).Match(bytePassword) {
return false
}
return true
}
func (s *SecurePassword) hasCharacterRepetition(password string) bool {
for i := 1; i < len(password); i++ {
if password[i-1] == password[i] {
return true
}
}
return false
}