package cryptext import ( "crypto/rand" "io" "math/big" mathrand "math/rand" "strings" ) const ( ppStartChar = "BCDFGHJKLMNPQRSTVWXZ" ppEndChar = "ABDEFIKMNORSTUXYZ" ppVowel = "AEIOUY" ppConsonant = "BCDFGHJKLMNPQRSTVWXZ" ppSegmentLenMin = 3 ppSegmentLenMax = 7 ppMaxRepeatedVowel = 2 ppMaxRepeatedConsonant = 2 ) var ppContinuation = map[uint8]string{ 'A': "BCDFGHJKLMNPRSTVWXYZ", 'B': "ADFIKLMNORSTUY", 'C': "AEIKOUY", 'D': "AEILORSUYZ", 'E': "BCDFGHJKLMNPRSTVWXYZ", 'F': "ADEGIKLOPRTUY", 'G': "ABDEFHILMNORSTUY", 'H': "AEIOUY", 'I': "BCDFGHJKLMNPRSTVWXZ", 'J': "AEIOUY", 'K': "ADEFHILMNORSTUY", 'L': "ADEFGIJKMNOPSTUVWYZ", 'M': "ABEFIKOPSTUY", 'N': "ABEFIKOPSTUY", 'O': "BCDFGHJKLMNPRSTVWXYZ", 'P': "AEFIJLORSTUY", 'Q': "AEIOUY", 'R': "ADEFGHIJKLMNOPSTUVYZ", 'S': "ACDEIKLOPTUYZ", 'T': "AEHIJOPRSUWY", 'U': "BCDFGHJKLMNPRSTVWXZ", 'V': "AEIOUY", 'W': "AEIOUY", 'X': "AEIOUY", 'Y': "ABCDFGHKLMNPRSTVXZ", 'Z': "AEILOTUY", } var ppLog2Map = map[int]float64{ 1: 0.00000000, 2: 1.00000000, 3: 1.58496250, 4: 2.00000000, 5: 2.32192809, 6: 2.58496250, 7: 2.80735492, 8: 3.00000000, 9: 3.16992500, 10: 3.32192809, 11: 3.45943162, 12: 3.58496250, 13: 3.70043972, 14: 3.80735492, 15: 3.90689060, 16: 4.00000000, 17: 4.08746284, 18: 4.16992500, 19: 4.24792751, 20: 4.32192809, 21: 4.39231742, 22: 4.45943162, 23: 4.52356196, 24: 4.58496250, 25: 4.64385619, 26: 4.70043972, 27: 4.75488750, 28: 4.80735492, 29: 4.85798100, 30: 4.90689060, 31: 4.95419631, 32: 5.00000000, } var ( ppVowelMap = ppMakeSet(ppVowel) ppConsonantMap = ppMakeSet(ppConsonant) ppEndCharMap = ppMakeSet(ppEndChar) ) func ppMakeSet(v string) map[uint8]bool { mp := make(map[uint8]bool, len(v)) for _, chr := range v { mp[uint8(chr)] = true } return mp } func ppRandInt(rng io.Reader, max int) int { v, err := rand.Int(rng, big.NewInt(int64(max))) if err != nil { panic(err) } return int(v.Int64()) } func ppRand(rng io.Reader, chars string, entropy *float64) uint8 { chr := chars[ppRandInt(rng, len(chars))] *entropy = *entropy + ppLog2Map[len(chars)] return chr } func ppCharType(chr uint8) (bool, bool) { _, ok1 := ppVowelMap[chr] _, ok2 := ppConsonantMap[chr] return ok1, ok2 } func ppCharsetRemove(cs string, set map[uint8]bool, allowEmpty bool) string { result := "" for _, chr := range cs { if _, ok := set[uint8(chr)]; !ok { result += string(chr) } } if result == "" && !allowEmpty { return cs } return result } func ppCharsetFilter(cs string, set map[uint8]bool, allowEmpty bool) string { result := "" for _, chr := range cs { if _, ok := set[uint8(chr)]; ok { result += string(chr) } } if result == "" && !allowEmpty { return cs } return result } func PronouncablePasswordExt(rng io.Reader, pwlen int) (string, float64) { // kinda pseudo markov-chain - with a few extra rules and no weights... if pwlen <= 0 { return "", 0 } vowelCount := 0 consoCount := 0 entropy := float64(0) startChar := ppRand(rng, ppStartChar, &entropy) result := string(startChar) currentChar := startChar isVowel, isConsonant := ppCharType(currentChar) if isVowel { vowelCount = 1 } if isConsonant { consoCount = ppMaxRepeatedConsonant } segmentLen := 1 segmentLenTarget := ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin) for len(result) < pwlen { charset := ppContinuation[currentChar] if vowelCount >= ppMaxRepeatedVowel { charset = ppCharsetRemove(charset, ppVowelMap, false) } if consoCount >= ppMaxRepeatedConsonant { charset = ppCharsetRemove(charset, ppConsonantMap, false) } lastOfSegment := false newSegment := false if len(result)+1 == pwlen { // last of result charset = ppCharsetFilter(charset, ppEndCharMap, false) } else if segmentLen+1 == segmentLenTarget { // last of segment charsetNew := ppCharsetFilter(charset, ppEndCharMap, true) if charsetNew != "" { charset = charsetNew lastOfSegment = true } } else if segmentLen >= segmentLenTarget { // (perhaps) start of new segment if _, ok := ppEndCharMap[currentChar]; ok { charset = ppStartChar newSegment = true } else { // continue segment for one more char to (hopefully) find an end-char charsetNew := ppCharsetFilter(charset, ppEndCharMap, true) if charsetNew != "" { charset = charsetNew lastOfSegment = true } } } else { // normal continuation } newChar := ppRand(rng, charset, &entropy) if lastOfSegment { currentChar = newChar segmentLen++ result += strings.ToLower(string(newChar)) } else if newSegment { currentChar = newChar segmentLen = 1 result += strings.ToUpper(string(newChar)) segmentLenTarget = ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin) vowelCount = 0 consoCount = 0 } else { currentChar = newChar segmentLen++ result += strings.ToLower(string(newChar)) } isVowel, isConsonant := ppCharType(currentChar) if isVowel { vowelCount++ consoCount = 0 } if isConsonant { vowelCount = 0 if newSegment { consoCount = ppMaxRepeatedConsonant } else { consoCount++ } } } return result, entropy } func PronouncablePassword(len int) string { v, _ := PronouncablePasswordExt(rand.Reader, len) return v } func PronouncablePasswordSeeded(seed int64, len int) string { v, _ := PronouncablePasswordExt(mathrand.New(mathrand.NewSource(seed)), len) return v }