diff --git a/password.go b/password.go new file mode 100644 index 0000000..4db31ce --- /dev/null +++ b/password.go @@ -0,0 +1,175 @@ +package main + +import ( + "crypto/sha512" + "io" + "strings" +) + +const LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz" +const UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +const NUMBERS = "0123456789" + +type Password struct { + restrictions *PasswordRestrictions + maxValue int + curAlphaLower int + curAlphaUpper int + curNumber int + curSpecial int + password string + completedOps int /*Simplistic infinite loop detection*/ + + hashChan chan int +} + +func NewPassword(restrictions *PasswordRestrictions, initialPassword string) *Password { + pw := new(Password) + pw.restrictions = restrictions + + //setup the goroutine to generate the bytes we need + pw.hashChan = make(chan int) + go func() { + h := sha512.New() + io.WriteString(h, initialPassword+strings.ToLower(restrictions.name)) + hashBytes := h.Sum(nil) + + for { + for _, b := range hashBytes { + pw.hashChan <- int(b) + } + h.Write(hashBytes) + hashBytes = h.Sum(nil) + } + }() + + pw.curAlphaLower = 0 + pw.curAlphaUpper = 0 + pw.curNumber = 0 + pw.curSpecial = 0 + pw.password = "" + + pw.maxValue = 62 /*alphanumeric*/ + len(restrictions.allowedSpecial) + + return pw +} + +func (pw *Password) generateCharacter(fillMinFirst bool) (string) { + //if we're trying to fill the minimum requirements, do so, but only if they're not already met + for (fillMinFirst && (pw.curAlphaLower < pw.restrictions.alphaLowerMin || pw.curAlphaUpper < pw.restrictions.alphaUpperMin || pw.curNumber < pw.restrictions.numberMin || pw.curSpecial < pw.restrictions.specialMin)) { + val := <-pw.hashChan % pw.maxValue + switch { + case val >= 0 && val < 26: + if pw.curAlphaLower < pw.restrictions.alphaLowerMin { + pw.curAlphaLower++ + return LOWERCASE_LETTERS[val : val+1] + } + case val >= 26 && val < 52: + if pw.curAlphaUpper < pw.restrictions.alphaUpperMin { + pw.curAlphaUpper++ + return UPPERCASE_LETTERS[val-26 : val-25] + } + case val >= 52 && val < 62: + if pw.curNumber < pw.restrictions.numberMin { + pw.curNumber++ + return NUMBERS[val-52 : val-51] + } + default: + if pw.curSpecial < pw.restrictions.specialMin { + pw.curSpecial++ + return pw.restrictions.allowedSpecial[val-62 : val-61] + } + } + } + + //if the minimum requirements are met, generate any character + val := <-pw.hashChan % pw.maxValue + switch { + case val >= 0 && val < 26: + if pw.curAlphaLower+1 <= pw.restrictions.alphaLowerMax { + pw.curAlphaLower++ + return LOWERCASE_LETTERS[val : val+1] + } + case val >= 26 && val < 52: + if pw.curAlphaUpper+1 <= pw.restrictions.alphaUpperMax { + pw.curAlphaUpper++ + return UPPERCASE_LETTERS[val-26 : val-25] + } + case val >= 52 && val < 62: + if pw.curNumber+1 <= pw.restrictions.numberMax { + pw.curNumber++ + return NUMBERS[val-52 : val-51] + } + default: + if pw.curSpecial+1 <= pw.restrictions.specialMax { + pw.curSpecial++ + return pw.restrictions.allowedSpecial[val-62 : val-61] + } + } + + panic("Generated invalid character code") + return "" +} + +func (pw *Password) removeCharacterAt(i int) { + removedChar := pw.password[:i] + pw.password = pw.password[:i] + pw.password[i+1:] + + switch { + case strings.Contains(LOWERCASE_LETTERS, removedChar): + pw.curAlphaLower-- + case strings.Contains(UPPERCASE_LETTERS, removedChar): + pw.curAlphaUpper-- + case strings.Contains(NUMBERS, removedChar): + pw.curNumber-- + case strings.Contains(pw.restrictions.allowedSpecial, removedChar): + pw.curSpecial-- + default: + panic("Unknown character made its way into the password") + } +} + +func (pw *Password) InitialRandom() { + pw.doOp() + //build up the initial string, making sure we don't go over the max allowed for each type + for length := 0; length < pw.restrictions.maxLength; length++ { + pw.password += pw.generateCharacter(false) + } +} + +func (pw *Password) FixupMinimumRequirements() { + //now, make sure we meet the minimum requirements. If we don't, start replacing items in the password + for (pw.curAlphaLower < pw.restrictions.alphaLowerMin || pw.curAlphaUpper < pw.restrictions.alphaUpperMin || pw.curNumber < pw.restrictions.numberMin || pw.curSpecial < pw.restrictions.specialMin){ + pw.doOp() + pw.removeCharacterAt(0) + pw.password += pw.generateCharacter(true) + } +} + +func (pw *Password) RandomizeOrder() { + pw.doOp() + //finally, (pseudo-)randomize the password ordering, one element at a time + for i := 0; i < len(pw.password); i++ { + position := (<-pw.hashChan % (len(pw.password) - i)) + i + pw.password = pw.password[position:position+1] + pw.password[0:position] + pw.password[position+1:] + } +} + +func (pw *Password) FixupAdditionalRequirements() { + if pw.restrictions.additionalRequirements == nil { + return + } + + for badChar, i := pw.restrictions.additionalRequirements(pw.password); !badChar; badChar, i = pw.restrictions.additionalRequirements(pw.password) { + pw.doOp() + pw.removeCharacterAt(i) + pw.password = pw.password[:i] + pw.generateCharacter(true) + pw.password[i:] + } +} + +func (pw *Password) doOp() { + pw.completedOps++ + if pw.completedOps > 1000 { + panic("Error: Attempting to generate your password has taken 1000 'operations'. It is likely it is stuck in an infinite loop. Please contact the authors of this package if you feel this is in error.") + } +} diff --git a/sites.go b/sites.go new file mode 100644 index 0000000..2b13da8 --- /dev/null +++ b/sites.go @@ -0,0 +1,74 @@ +package main + +type PasswordRestrictions struct { + name string + longName string + minLength int + maxLength int + alphaLowerMin int + alphaLowerMax int + alphaUpperMin int + alphaUpperMax int + numberMin int + numberMax int + specialMin int + specialMax int + allowedSpecial string + additionalRequirements func(string) (bool, int) //returns the index of the problem character +} + +var sites []PasswordRestrictions = []PasswordRestrictions{ + PasswordRestrictions{ + name: "etrade", + longName: "E-trade: etrade.com", + minLength: 6, + maxLength: 32, + alphaLowerMin: 0, + alphaLowerMax: 32, + alphaUpperMin: 0, + alphaUpperMax: 32, + numberMin: 1, + numberMax: 32, + specialMin: 0, + specialMax: 0}, + PasswordRestrictions{ + name: "fidelity", + longName: "Fidelity Netbenefits", + minLength: 6, + maxLength: 12, + alphaLowerMin: 0, + alphaLowerMax: 12, + alphaUpperMin: 0, + alphaUpperMax: 12, + numberMin: 0, + numberMax: 12, + specialMin: 0, + specialMax: 0}, + PasswordRestrictions{ + name: "bbt", + longName: "BB&T Banking", + minLength: 8, + maxLength: 12, + alphaLowerMin: 0, + alphaLowerMax: 12, + alphaUpperMin: 0, + alphaUpperMax: 12, + numberMin: 1, + //There is no max requirement on numbers, but we must have at least one + //letter (either uppercase or lowercase) so putting an upper bound on + //numbers is easier than an either-or lower bound on letters + numberMax: 11, + specialMin: 0, + specialMax: 0, + additionalRequirements: func(pass string) (bool, int) { + if len(pass) < 1 { + return true, 0 + } + for i := 1; i < len(pass); i++ { + if pass[i-1:i] == pass[i:i+1] { + return false, i + } + } + return true, 0 + }}, +} diff --git a/specialpass.go b/specialpass.go index 1df9eb8..dbb99fc 100644 --- a/specialpass.go +++ b/specialpass.go @@ -4,57 +4,13 @@ import ( "code.google.com/p/gopass" "flag" "fmt" - "io" "strings" - "crypto/sha512" ) -type PasswordRestrictions struct { - name string - minLength int - maxLength int - alphaLowerMin int - alphaLowerMax int - alphaUpperMin int - alphaUpperMax int - numberMin int - numberMax int - specialMin int - specialMax int - allowedSpecial string -} - var siteName string const DEFAULT_SITE = "etrade" -var sites []PasswordRestrictions = []PasswordRestrictions{ - PasswordRestrictions{ - name: "etrade", - minLength: 6, - maxLength: 32, - alphaLowerMin: 0, - alphaLowerMax: 32, - alphaUpperMin: 0, - alphaUpperMax: 32, - numberMin: 1, - numberMax: 32, - specialMin: 0, - specialMax: 0}, - PasswordRestrictions{ - name: "fidelity", - minLength: 6, - maxLength: 12, - alphaLowerMin: 0, - alphaLowerMax: 12, - alphaUpperMin: 0, - alphaUpperMax: 12, - numberMin: 0, - numberMax: 12, - specialMin: 0, - specialMax: 0}, -} - func init() { const site_usage = "Name of site for which to generate password" flag.StringVar(&siteName, "siteName", DEFAULT_SITE, site_usage) @@ -77,6 +33,7 @@ func main() { for _, v := range sites { fmt.Println("\t" + v.name) } + return } oldPassword, err := gopass.GetPass("Enter Password: ") @@ -84,111 +41,12 @@ func main() { panic(err) } - c := make(chan int) - go generateBytes(c, oldPassword, restrictions.name) + pw := NewPassword(restrictions, oldPassword) + pw.InitialRandom() + pw.FixupMinimumRequirements() + pw.FixupAdditionalRequirements() + pw.RandomizeOrder() + pw.FixupAdditionalRequirements() //run this again, since randomizing the order can potentially introduce things which don't meet the additional requirements if they rely on not having duplicate characters next to each other, etc. - maxValue := 62 /*alphanumeric*/ + len(restrictions.allowedSpecial) - - curAlphaLower := 0 - curAlphaUpper := 0 - curNumber := 0 - curSpecial := 0 - password := "" - - lower := "abcdefghijklmnopqrstuvwxyz" - upper := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - numbers := "0123456789" - - //build up the initial string, making sure we don't go over the max allowed for each type - for length := 0; length < restrictions.maxLength; length++ { - val := <- c % maxValue - switch { - case val >= 0 && val < 26: - if curAlphaLower + 1 <= restrictions.alphaLowerMax { - password += lower[val:val+1] - curAlphaLower++ - } - case val >= 26 && val < 52: - if curAlphaUpper + 1 <= restrictions.alphaUpperMax { - password += upper[val-26:val-25] - curAlphaUpper++ - } - case val >= 52 && val < 62: - if curNumber + 1 <= restrictions.numberMax { - password += numbers[val-52:val-51] - curNumber++ - } - default: - if curSpecial + 1 <= restrictions.specialMax { - password += restrictions.allowedSpecial[val-62:val-61] - curSpecial++ - } - } - } - - //now, make sure we meet the minimum requirements. If we don't, start replacing items in the password - for { - removed := true - switch { - case curAlphaLower < restrictions.alphaLowerMin: - val := <- c % 26 - password += lower[val:val+1] - curAlphaLower++ - case curAlphaUpper < restrictions.alphaUpperMin: - val := <- c % 26 - password += upper[val:val+1] - curAlphaUpper++ - case curNumber < restrictions.numberMin: - val := <- c % 10 - password += numbers[val:val+1] - curNumber++ - case curSpecial < restrictions.specialMin: - val := <- c % len(restrictions.allowedSpecial) - password += restrictions.allowedSpecial[val:val+1] - curSpecial++ - default: - removed = false - } - if !removed { - break - } - - removedChar := password[:1] - password = password[1:] - - switch{ - case strings.Contains(lower, removedChar): - curAlphaLower-- - case strings.Contains(upper, removedChar): - curAlphaUpper-- - case strings.Contains(numbers, removedChar): - curNumber-- - case strings.Contains(restrictions.allowedSpecial, removedChar): - curSpecial-- - default: - panic("Unknown character made its way into the password") - } - } - - //finally, (pseudo-)randomize the password ordering, one element at a time - for i := 0; i < restrictions.maxLength; i++ { - position := (<- c % (restrictions.maxLength - i)) + i - password = password[position:position+1] + password[0:position] + password[position+1:restrictions.maxLength] - } - - fmt.Println("Generated password for " + restrictions.name + ": " + password) -} - -func generateBytes(c chan int, password string, siteName string) { - h := sha512.New() - io.WriteString(h, password + strings.ToLower(siteName)) - hashBytes := h.Sum(nil) - - for { - for _, b := range hashBytes { - c <- int(b) - } - h.Write(hashBytes) - hashBytes = h.Sum(nil) - } + fmt.Println("Generated password for " + restrictions.name + ": " + pw.password) }