Modularize, add ability to fixup special requirements, etc.

This commit is contained in:
Aaron Lindsay 2013-07-27 09:52:46 -04:00
parent 7fcc002e02
commit ef82efb92d
3 changed files with 257 additions and 150 deletions

175
password.go Normal file
View File

@ -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.")
}
}

74
sites.go Normal file
View File

@ -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
}},
}

View File

@ -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)
}