264 lines
5.4 KiB
Go
264 lines
5.4 KiB
Go
|
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
|
||
|
}
|