From 8896698ac2ad65d3fbf713c293019301d3ee8fd0 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sat, 2 May 2015 15:03:27 +0200 Subject: [PATCH] Initial running version --- .gitignore | 1 + lib/generator.go | 121 +++++++++++++++++++++++++++++++++++++ lib/generator_test.go | 136 ++++++++++++++++++++++++++++++++++++++++++ main.go | 86 ++++++++++++++++++++++++++ 4 files changed, 344 insertions(+) create mode 100644 .gitignore create mode 100644 lib/generator.go create mode 100644 lib/generator_test.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3097ab --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +password diff --git a/lib/generator.go b/lib/generator.go new file mode 100644 index 0000000..b616e4a --- /dev/null +++ b/lib/generator.go @@ -0,0 +1,121 @@ +package securepassword // import "github.com/Luzifer/password/lib" + +import ( + "fmt" + "math/rand" + "regexp" + "strings" + "time" +) + +type SecurePassword struct { + characterTables map[string]string + insecurePattern []string + badCharacters []string +} + +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 + } +} + +func (s *SecurePassword) GeneratePassword(length int, special bool) string { + 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 { + password = fmt.Sprintf("%s%s", + password, + string(characterTable[rand.Intn(len(characterTable))]), + ) + if len(password) == length { + if s.CheckPasswordSecurity(password, special) { + break + } + password = "" + } + } + return password +} + +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 +} diff --git a/lib/generator_test.go b/lib/generator_test.go new file mode 100644 index 0000000..852b90a --- /dev/null +++ b/lib/generator_test.go @@ -0,0 +1,136 @@ +package securepassword + +import "testing" + +func TestInsecurePasswords(t *testing.T) { + passwords := map[string]string{ + "8452028337962356": "Password with only numeric characters was accepted.", + "adfgjadrgdagasdf": "Password with only lowercase characters was accepted.", + "ASEFSTDHQAEGFADF": "Password with only uppercase characters was accepted.", + "135fach74nc94bd6": "Password without uppercase characters was accepted.", + "235JGOA0YTVKS46S": "Password without lowercase characters was accepted.", + + "cKTn5mQXfasdS6qy": "Password with pattern asd was accepted.", + "cKTn5mQXfdsaS6qy": "Password with pattern dsa was accepted.", + "cKTn5mQXf345S6qy": "Password with pattern 345 was accepted.", + "cKTn5mQXf987S6qy": "Password with pattern 987 was accepted.", + "cKTn5mQXfabcS6qy": "Password with pattern abc was accepted.", + "cKTn5mQXfcbaS6qy": "Password with pattern cba was accepted.", + "cKTn5mQXfABCS6qy": "Password with pattern ABC was accepted.", + "cKTn5mQXfONMS6qy": "Password with pattern ONM was accepted.", + + "Gncj5zzK29Dvx92h": "Password with character repetition was accepted", + "Gncj5%%K29Dvx92h": "Password with character repetition was accepted", + "Gncj55%K29Dvx92h": "Password with character repetition was accepted", + } + + for password, errorMessage := range passwords { + if NewSecurePassword().CheckPasswordSecurity(password, false) { + t.Error(errorMessage) + } + } +} + +func TestSecurePasswords(t *testing.T) { + passwords := []string{ + "6e1GZ6V2empWAky5Z13a", + "DLHZA2zfWor1XUoJYvFR", + "sMf3uNf2E1pxPFMymah5", + "prb4tX1vtyVL7R316dKU", + "7bWc9C1ciL62h5u26Z9g", + } + + for _, password := range passwords { + if !NewSecurePassword().CheckPasswordSecurity(password, false) { + t.Errorf("Password was rejected: %s", password) + } + } +} + +func TestPasswordWithoutSpecialCharaterFail(t *testing.T) { + passwords := []string{ + "6e1GZ6V2empWAky5Z13a", + "DLHZA2zfWor1XUoJYvFR", + "sMf3uNf2E1pxPFMymah5", + "prb4tX1vtyVL7R316dKU", + "7bWc9C1ciL62h5u26Z9g", + } + + for _, password := range passwords { + if NewSecurePassword().CheckPasswordSecurity(password, true) { + t.Errorf("Password was accepted: %s", password) + } + } +} + +func TestSecurePasswordWithSpecialCharacter(t *testing.T) { + passwords := []string{ + `a*5S(+zQ9=3%/1MiNr`, + `/K}.]C4-a/{,39r$"(D+`, + `9dk#:@xjPd_m$:F"}>Cj`, + } + + for _, password := range passwords { + if !NewSecurePassword().CheckPasswordSecurity(password, true) { + t.Errorf("Password was rejected: %s", password) + } + } +} + +func BenchmarkGeneratePasswords8Char(b *testing.B) { + pwd := NewSecurePassword() + for i := 0; i < b.N; i++ { + pwd.GeneratePassword(8, false) + } +} + +func BenchmarkGeneratePasswords8CharSpecial(b *testing.B) { + pwd := NewSecurePassword() + for i := 0; i < b.N; i++ { + pwd.GeneratePassword(8, true) + } +} + +func BenchmarkGeneratePasswords16Char(b *testing.B) { + pwd := NewSecurePassword() + for i := 0; i < b.N; i++ { + pwd.GeneratePassword(16, false) + } +} + +func BenchmarkGeneratePasswords16CharSpecial(b *testing.B) { + pwd := NewSecurePassword() + for i := 0; i < b.N; i++ { + pwd.GeneratePassword(16, true) + } +} + +func BenchmarkGeneratePasswords32Char(b *testing.B) { + pwd := NewSecurePassword() + for i := 0; i < b.N; i++ { + pwd.GeneratePassword(32, false) + } +} + +func BenchmarkGeneratePasswords32CharSpecial(b *testing.B) { + pwd := NewSecurePassword() + for i := 0; i < b.N; i++ { + pwd.GeneratePassword(32, true) + } +} + +func BenchmarkGeneratePasswords128Char(b *testing.B) { + pwd := NewSecurePassword() + for i := 0; i < b.N; i++ { + pwd.GeneratePassword(128, false) + } +} + +func BenchmarkGeneratePasswords128CharSpecial(b *testing.B) { + pwd := NewSecurePassword() + for i := 0; i < b.N; i++ { + pwd.GeneratePassword(128, true) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f013a2b --- /dev/null +++ b/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "strconv" + + "github.com/Luzifer/password/lib" + "github.com/codegangsta/cli" + "github.com/gorilla/mux" +) + +var pwd *securepassword.SecurePassword + +func init() { + pwd = securepassword.NewSecurePassword() +} + +func main() { + app := cli.NewApp() + app.Usage = "generates secure random passwords" + app.Version = "1.0.0" + + app.Commands = []cli.Command{ + { + Name: "serve", + Usage: "start an API server to request passwords", + Action: startAPIServer, + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "port", + Value: 3000, + Usage: "port to listen on", + }, + }, + }, + { + Name: "get", + Usage: "generate and return a secure random password", + Action: printPassword, + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "length, l", + Value: 20, + Usage: "length of the generated password", + }, + cli.BoolFlag{ + Name: "special, s", + Usage: "use special characters in your password", + }, + }, + }, + } + + app.Run(os.Args) +} + +func startAPIServer(c *cli.Context) { + r := mux.NewRouter() + r.HandleFunc("/v1/getPassword", handleAPIGetPasswordv1).Methods("GET") + + http.Handle("/", r) + http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), nil) +} + +func printPassword(c *cli.Context) { + fmt.Println(pwd.GeneratePassword(c.Int("length"), c.Bool("special"))) +} + +func handleAPIGetPasswordv1(res http.ResponseWriter, r *http.Request) { + length, err := strconv.Atoi(r.URL.Query().Get("length")) + if err != nil { + length = 20 + } + special := r.URL.Query().Get("special") == "true" + + if length > 128 { + http.Error(res, "Please do not use length with more than 128 characters!", http.StatusNotAcceptable) + return + } + + res.Header().Add("Content-Type", "text/plain") + res.Header().Add("Cache-Control", "no-cache") + res.Write([]byte(pwd.GeneratePassword(length, special))) +}