Skip to content

Commit 9d45e86

Browse files
author
Aizen
committed
distill markov chain from EFF dynamically
1 parent a8058f7 commit 9d45e86

File tree

7 files changed

+159
-19493
lines changed

7 files changed

+159
-19493
lines changed

cmd/genpw/README.md

+26-26
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ This will install the `genpw` binary in your `$GOPATH/bin` directory.
2424

2525
Once installed, you can generate passphrases by running `genpw` with the following options:
2626

27-
```
27+
```bash
2828
Usage of genpw:
2929
-n uint
3030
number of passwords to generate (default 6)
@@ -49,15 +49,15 @@ genpw
4949

5050
This will output something like:
5151

52-
```
53-
Passphrase Log10(Guesses) EntropyLog2
54-
55-
Joilionel.scric.glicam 20.70 69.76
56-
Haneatri.unfolop.freskl 21.17 71.33
57-
Ricie.enrelerc.morsman 21.90 73.74
58-
Snuterov.possarb.sprive 22.93 77.18
59-
Hartiner.acycata.ovalnegis 23.35 78.57
60-
Grantdoti.irthed.imeatill 23.61 79.45
52+
```bash
53+
Passphrase Log10(Guesses) Log2Entropy Strength
54+
55+
Candox.swortone.harvingl 23.37 78.64 [============]
56+
Kinywaya.preterpr.reefrate 24.31 81.76 [============]
57+
Ratererl.ruse.elikewa 20.44 68.92 [==========..]
58+
Relemalo.anli.systuri 20.77 70.01 [==========..]
59+
Unpori.grog.bodityi 18.05 60.97 [=========...]
60+
Uperti.ampilyon.extedes 22.23 74.86 [===========.]
6161
```
6262

6363
1. **Generate passphrases with custom pattern**:
@@ -68,14 +68,14 @@ genpw -p "WW20dds" -n 5
6868

6969
The output might look like:
7070

71-
```go
72-
Passphrase Log10(Guesses) EntropyLog2
73-
74-
DofyonLivord2012@ 15.26 51.70
75-
DinkedMankess2026^ 16.09 54.43
76-
BlyestaHameloty2062= 18.02 60.87
77-
ShantlyzSectoo2098= 18.15 61.29
78-
AxoncingTwovernis2056* 19.12 64.52
71+
```bash
72+
Passphrase Log10(Guesses) Log2Entropy Strength
73+
74+
HandmarmOvera2053" 16.24 54.96 [========....]
75+
ResabledAnverbou2004+ 19.22 64.85 [==========..]
76+
RocraryiRegonede2072! 19.96 67.29 [==========..]
77+
SagureraHassain2045- 19.70 66.45 [==========..]
78+
WoutbDemitte2019# 15.37 52.07 [========....]
7979
```
8080
8181
or
@@ -86,14 +86,14 @@ genpw -p "w.w.w.w" -n 5
8686
8787
The output might look like:
8888
89-
```go
90-
Passphrase Log10(Guesses) EntropyLog2
91-
92-
stritters.candis.frot.unliti 27.06 90.91
93-
treralryi.jangli.stathle.resche 29.22 98.08
94-
andetap.quis.cloashea.firetorki 29.75 99.83
95-
humperes.stacessfi.splan.gidinkl 30.55 102.50
96-
unctirter.arbeday.amersdowf.ovyarvis 33.60 112.62
89+
```bash
90+
Passphrase Log10(Guesses) Log2Entropy Strength
91+
92+
cleatabit.sphongedi.zedizmobl.drooky 32.40 108.62 [============]
93+
comprewa.cedivet.refyerm.unancling 31.19 104.61 [============]
94+
eitispayi.woblyoffo.unounde.pradimisf 35.27 118.18 [============]
95+
juiselid.partu.ovenes.slampos 28.32 95.08 [============]
96+
yaholl.boort.rentlestu.hustfierm 29.38 98.61 [============]
9797
```
9898
9999
### Entropy Considerations

cryptipass.go

+127-50
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,104 @@
1-
// Package cryptipass v1.3.0 provides functionality for generating secure passphrases
2-
// composed of random words, with a focus on cryptographic security and entropy calculation.
1+
// Package cryptipass provides a passphrase generation and entropy calculation
2+
// system using wordlists and patterns. It is designed to ensure randomness
3+
// while offering flexibility in the structure of generated passwords.
34
package cryptipass
45

56
import (
67
cr "crypto/rand"
8+
_ "embed"
79
"fmt"
810
"log"
911
"math"
1012
"math/rand/v2"
1113
"strings"
1214
)
1315

14-
type Generator struct {
15-
Rng *rand.Rand
16+
// Transition represents the probability distribution of rune sequences within
17+
// words. It stores the runes, their counts, the total occurrences, and the
18+
// entropy of the sequence.
19+
type Transition struct {
20+
Runes []rune
21+
Counts []int
22+
Total int
23+
Entropy float64
1624
}
1725

18-
// NewInstance initializes the cryptographically secure random number generator (RNG).
19-
// It reads 32 bytes of entropy from crypto/rand and uses them to seed a ChaCha8-based RNG.
20-
// If the required number of bytes is not read, it logs a fatal error.
21-
func NewInstance() *Generator {
26+
func distill(tokens []string) map[string]Transition {
27+
transition_matrix := make(map[string]map[rune]int)
28+
put := func(str string, r rune) {
29+
if transition_matrix[str] == nil {
30+
transition_matrix[str] = make(map[rune]int)
31+
}
32+
transition_matrix[str][r]++
33+
}
34+
35+
for _, w := range tokens {
36+
R := []rune(w)
37+
put("lengths", rune(len(R)))
38+
put("", R[0])
39+
put(string(R[0]), R[1])
40+
for i := 0; i < len(R)-2; i++ {
41+
put(string(R[i])+string(R[i+1]), R[i+2])
42+
}
43+
}
44+
dist_trans_matrix := make(map[string]Transition)
45+
for k, rfreq := range transition_matrix {
46+
C := 0
47+
for _, freq := range rfreq {
48+
C += freq
49+
}
50+
H := 0.0
51+
tr := Transition{}
52+
tr.Counts = make([]int, 0)
53+
54+
tr.Runes = make([]rune, 0)
55+
cum := 0
56+
for ru, freq := range rfreq {
57+
p := float64(freq) / float64(C)
58+
H -= math.Log2(p) * p
59+
cum += freq
60+
tr.Counts = append(tr.Counts, cum)
61+
tr.Runes = append(tr.Runes, ru)
62+
}
63+
tr.Total = C
64+
tr.Entropy = H
65+
dist_trans_matrix[k] = tr
66+
}
67+
return dist_trans_matrix
68+
}
69+
70+
type generator struct {
71+
Rng *rand.Rand
72+
JumpTable *map[string]Transition
73+
}
74+
75+
// NewInstanceFromList generates a new passphrase generator using a custom
76+
// list of tokens (words). It uses the ChaCha8 random number generator seeded
77+
// by cryptographically secure random bytes.
78+
func NewInstanceFromList(tokens []string) *generator {
2279
seed := [32]byte{}
2380
n, err := cr.Reader.Read(seed[:])
2481
if err != nil || n != 32 {
2582
log.Fatal(n, err, seed)
2683
}
2784
rng := rand.New(rand.NewChaCha8(seed))
28-
g := new(Generator)
85+
g := new(generator)
2986
g.Rng = rng
87+
jtbl := distill(tokens)
88+
g.JumpTable = &jtbl
89+
3090
return g
3191
}
3292

33-
// NewPassphrase generates a passphrase consisting of the specified number of random words.
34-
// Each word is chosen by the GenMixWord function, and the total entropy of the passphrase
35-
// is calculated and returned along with the passphrase.
36-
//
37-
// - Parameters:
38-
//
39-
// words (uint64): The number of words to include in the passphrase.
40-
//
41-
// - Return values:
42-
//
43-
// string: The generated passphrase, with words joined by periods.
44-
//
45-
// float64: The total entropy (in bits) of the passphrase, indicating its strength.
46-
func (g *Generator) GenPassphrase(words uint64) (string, float64) {
93+
// NewInstance returns a new passphrase generator initialized with a default
94+
// wordlist. It uses the ChaCha8 random number generator for randomization.
95+
func NewInstance() *generator {
96+
return NewInstanceFromList(eff_long_word_list)
97+
}
98+
99+
// GenPassphrase generates a passphrase consisting of the specified number of
100+
// words. It returns the passphrase and its total entropy value.
101+
func (g *generator) GenPassphrase(words uint64) (string, float64) {
47102
wordvec := []string{}
48103
total_entropy := 0.0
49104
for range words {
@@ -54,31 +109,13 @@ func (g *Generator) GenPassphrase(words uint64) (string, float64) {
54109
return strings.Join(wordvec, "."), total_entropy
55110
}
56111

57-
// GenFromPattern generates a passphrase based on a specified pattern.
58-
// The pattern dictates the structure of the generated passphrase, with the following
59-
// format options:
60-
//
61-
// - `w`: Lowercase word generated via GenMixWord.
62-
//
63-
// - `W`: Capitalized word generated via GenMixWord (first letter uppercased).
64-
//
65-
// - `d`: Random digit (0-9).
66-
//
67-
// - `s`: Random symbol from a predefined set (@#!$%&=?^+-*").
68-
//
69-
// Any other characters in the pattern are appended as-is.
70-
//
71-
// - Parameters:
72-
//
73-
// pattern (string): A string pattern that specifies the structure of the passphrase.
74-
//
75-
// - Return values:
76-
//
77-
// string: The generated passphrase based on the given pattern.
78-
//
79-
// float64: The total entropy (in bits) of the generated passphrase.
80-
81-
func (g *Generator) GenFromPattern(pattern string) (string, float64) {
112+
// GenFromPattern generates a passphrase following the specified pattern.
113+
// Supported pattern symbols are:
114+
// - 'w', 'W': words (lowercase or capitalized)
115+
// - 'd': digits
116+
// - 's': symbols
117+
// - 'c', 'C': characters (lowercase or uppercase)
118+
func (g *generator) GenFromPattern(pattern string) (string, float64) {
82119
passphrase := ""
83120
entropy := 0.0
84121
pushnext := false
@@ -122,7 +159,9 @@ func (g *Generator) GenFromPattern(pattern string) (string, float64) {
122159
return passphrase, entropy
123160
}
124161

125-
func (g *Generator) GenWord(c rune) (string, float64) {
162+
// GenWord generates a word based on the character pattern (e.g., 'w', 'W').
163+
// It returns the generated word and its entropy.
164+
func (g *generator) GenWord(c rune) (string, float64) {
126165
head, h_head := g.PickNext("")
127166
leng, h_leng := g.PickLength()
128167
if c == 'W' {
@@ -143,12 +182,14 @@ type CertifyResult struct {
143182
StdDev float64
144183
}
145184

185+
// Certify verifies the randomness and entropy of the generated passphrases.
186+
// It runs multiple iterations to certify that the empirical entropy matches the nominal entropy.
146187
func Certify(Gen func() (string, float64)) CertifyResult {
147188
nominal_H := 0.0
148189
nominal_H2 := 0.0
149190
cnt_nom_H := 0.0
150191
for range 1000 {
151-
//pilot run to estimate budjet
192+
152193
_, nh := Gen()
153194
nominal_H += nh
154195
nominal_H2 += nh * nh
@@ -185,3 +226,39 @@ func Certify(Gen func() (string, float64)) CertifyResult {
185226
}
186227

187228
}
229+
230+
// PickNext selects the next rune in the sequence based on the previous seed.
231+
// It uses the stored transition matrix to ensure a smooth distribution of
232+
// rune occurrences, retrying until a valid match is found.
233+
func (g *generator) PickNext(seed string) (string, float64) {
234+
L := min(len(seed), 2)
235+
tok := strings.ToLower(seed[len(seed)-L:])
236+
retry:
237+
if tr, ok := (*g.JumpTable)[tok]; ok {
238+
N := g.Rng.IntN(tr.Total)
239+
for i, v := range tr.Counts {
240+
if N < v {
241+
return string(tr.Runes[i]), tr.Entropy
242+
}
243+
}
244+
panic("unexpected")
245+
}
246+
tok = tok[1:]
247+
goto retry
248+
}
249+
250+
// PickLength selects the length of the next generated word based on the
251+
// transition matrix of word lengths. It ensures the generated word has an
252+
// appropriate length and entropy.
253+
func (g *generator) PickLength() (int, float64) {
254+
tr, ok := (*g.JumpTable)["lengths"]
255+
if ok {
256+
N := g.Rng.IntN(tr.Total)
257+
for i, v := range tr.Counts {
258+
if N < v {
259+
return int(tr.Runes[i]), tr.Entropy
260+
}
261+
}
262+
}
263+
panic("unexpected rand num")
264+
}

cryptipass_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestBasic(t *testing.T) {
2424
}
2525

2626
func TestCert(t *testing.T) {
27-
g := cryptipass.Generator{}
27+
g := cryptipass.NewInstance()
2828
g.Rng = rand.New(rand.NewPCG(37512033, 27996124))
2929

3030
type FN func() (string, float64)

0 commit comments

Comments
 (0)