cloudflare / gokey

A simple vaultless password manager in Go

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Generated passwords look very similar

SebastianMpl opened this issue · comments

Guys,
Just downloaded Linux x64 binary from the release page and while playing with he app I've noticed strange thing:

seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 10
sPYL`0_9rj
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 11
D5`0Nm*nuY;
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 12
z/Beul!Q1,.V
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 13
/Beul!Q1,.V>]
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 14
Beul!Q1,.V>]<@
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 15
w+m6j-vJH{"bz/B
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 16
w+m6j-vJH{"bz/Be
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 17
w+m6j-vJH{"bz/Beu
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 18
w+m6j-vJH{"bz/Beul
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 19
w+m6j-vJH{"bz/Beul!
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 20
w+m6j-vJH{"bz/Beul!Q
[seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 50
w+m6j-vJH{"bz/Beul!Q1,.V>]<@0psPYL`0_9rjP[b,d~^`:%
seba@sebapc Pobrane]$ ./gokey-v0.1.2-linux-amd64 -p example-p@ss -r github.com -l 51
w+m6j-vJH{"bz/Beul!Q1,.V>]<@0psPYL`0_9rjP[b,d~^`:%3

As you may see the result password with additional character is very similar to the previous password.
The same is for different Master Password and different websites.

Is it the expected behaviour or not?
Thanks,

Hi,

It should be expected: the combination of the master password + realm string should create a unique deterministic pseudo-random generator. Notice, that the requested password length is not contributing here yet.

Later, a filtering algorithm just takes the input from this DPRNG and tries to convert it to a string, which passes the requested password spec (including length). The result is than in most cases the string would be similar, just truncated to the requested length unless the bigger string stops conforming to the password spec (and in which case it would be just discarded and the next string from the DPRNG taken until it conforms).

see the relevant code here:

gokey/keygen.go

Lines 67 to 156 in 2d48d3b

func (spec *PasswordSpec) Compliant(password string) bool {
var upper, lower, digits, special int
for _, c := range password {
if unicode.IsUpper(c) {
upper++
}
if unicode.IsLower(c) {
lower++
}
if unicode.IsDigit(c) {
digits++
}
if unicode.IsSymbol(c) || unicode.IsPunct(c) {
if spec.AllowedSpecial == "" {
special++
} else {
if strings.ContainsRune(spec.AllowedSpecial, c) {
special++
} else {
return false
}
}
}
}
if !allowed(upper, spec.Upper) || !allowed(lower, spec.Lower) || !allowed(digits, spec.Digits) || !allowed(special, spec.Special) {
return false
}
return true
}
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?"
func randRange(rng io.Reader, max byte) (byte, error) {
var base [1]byte
for {
_, err := io.ReadFull(rng, base[:])
if err != nil {
return 0, err
}
if 255 == base[0] {
continue
}
rem := 255 % max
buck := 255 / max
if base[0] < 255-rem {
return base[0] / buck, nil
}
}
}
func (keygen *KeyGen) genRandStr(length int) (string, error) {
bytes := make([]byte, length)
for i := 0; i < length; i++ {
pos, err := randRange(keygen.rng, byte(len(chars)))
if err != nil {
return "", err
}
bytes[i] = chars[pos]
}
return string(bytes), nil
}
func (keygen *KeyGen) GeneratePassword(spec *PasswordSpec) (string, error) {
if !spec.Valid() {
return "", errors.New("invalid password specification")
}
for {
password, err := keygen.genRandStr(spec.Length)
if err != nil {
return "", err
}
if spec.Compliant(password) {
return password, nil
}
}
}