diff --git a/cryptext/pronouncablePassword.go b/cryptext/pronouncablePassword.go new file mode 100644 index 0000000..232fc87 --- /dev/null +++ b/cryptext/pronouncablePassword.go @@ -0,0 +1,263 @@ +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 +} diff --git a/cryptext/pronouncablePassword_test.go b/cryptext/pronouncablePassword_test.go new file mode 100644 index 0000000..c534b06 --- /dev/null +++ b/cryptext/pronouncablePassword_test.go @@ -0,0 +1,35 @@ +package cryptext + +import ( + "fmt" + "math/rand" + "testing" +) + +func TestPronouncablePasswordExt(t *testing.T) { + for i := 0; i < 20; i++ { + pw, entropy := PronouncablePasswordExt(rand.New(rand.NewSource(int64(i))), 16) + fmt.Printf("[%.2f] => %s\n", entropy, pw) + } +} + +func TestPronouncablePasswordSeeded(t *testing.T) { + for i := 0; i < 20; i++ { + pw := PronouncablePasswordSeeded(int64(i), 8) + fmt.Printf("%s\n", pw) + } +} + +func TestPronouncablePassword(t *testing.T) { + for i := 0; i < 20; i++ { + pw := PronouncablePassword(i + 1) + fmt.Printf("%s\n", pw) + } +} + +func TestPronouncablePasswordWrongLen(t *testing.T) { + PronouncablePassword(0) + PronouncablePassword(-1) + PronouncablePassword(-2) + PronouncablePassword(-3) +} diff --git a/goextVersion.go b/goextVersion.go index 35eb8ee..a409327 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -1,5 +1,5 @@ package goext -const GoextVersion = "0.0.373" +const GoextVersion = "0.0.374" -const GoextVersionTimestamp = "2024-01-14T00:07:01+0100" +const GoextVersionTimestamp = "2024-01-14T01:37:38+0100"