Skip to content

Commit

Permalink
Performance improvements and minor API changes
Browse files Browse the repository at this point in the history
  • Loading branch information
GGP1 committed Apr 25, 2021
1 parent ced06ba commit b584e7a
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 152 deletions.
4 changes: 3 additions & 1 deletion Makefile
@@ -1,11 +1,13 @@
.PHONY: test test-race pprof pprof-cpu-web pprof-mem-web

test:
@go test -count 1000 -timeout 30s

test-race:
@go test -race -timeout 45s

pprof:
@go test -cpuprofile cpu.pprof -memprofile mem.pprof -bench .
@go test -bench . -benchmem -cpuprofile cpu.pprof -memprofile mem.pprof

pprof-cpu-web:
@go tool pprof -http=:8080 cpu.pprof
Expand Down
32 changes: 14 additions & 18 deletions README.md
@@ -1,6 +1,5 @@
# Atoll

[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://godoc.org/github.com/GGP1/atoll)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/GGP1/atoll)](https://pkg.go.dev/github.com/GGP1/atoll)
[![Go Report Card](https://goreportcard.com/badge/github.com/GGP1/atoll)](https://goreportcard.com/report/github.com/GGP1/atoll)

Expand All @@ -13,7 +12,6 @@ Atoll is a library for generating cryptographically secure and highly random sec
- No dependencies
- Input validation
- Secret sanitization
* Common patterns cleanup and space trimming
- Include characters/words/syllables in random positions
- Exclude any undesired character/word/syllable
- **Password**:
Expand Down Expand Up @@ -42,34 +40,32 @@ import (
)

func main() {
// Generate a random password
p := &atoll.Password{
Length: 16,
Levels: []int{atoll.Lowercase, atoll.Uppercase, atoll.Digit},
Include: "á&1",
Levels: []int{atoll.Lower, atoll.Upper, atoll.Digit},
Include: "a&1",
Repeat: true,
}

password, err := atoll.NewSecret(p)
if err != nil {
log.Fatal(err)
}

fmt.Println(password)
// 1VOKM7mNA6w&oIan

// Generate a random passphrase
p2 := &atoll.Passphrase{
p1 := &atoll.Passphrase{
Length: 7,
Separator: "/",
List: atoll.NoList,
}

passphrase, err := atoll.NewSecret(p2)
passphrase, err := atoll.NewSecret(p1)
if err != nil {
log.Fatal(err)
}

fmt.Println(passphrase)
// aei/jwyjidaasres/duii/rscfiotuuckm/ydsiacf/ora/yywu
}
```

Expand All @@ -93,7 +89,7 @@ Atoll guarantees that the password will contain at least one of the characters o

Atoll offers 3 ways of generating a passphrase:

- **Without** a list (*NoList*): generate random numbers that determine the word length (between 3 and 12 letters) and if the letter is either a vowel or a constant (4/10 times a vowel is selected). Note that not using a list makes the potential attacker job harder.
- **Without** a list (*NoList*): generates random numbers that determine the word length (between 3 and 12 letters) and if the letter is either a vowel or a constant. Note that not using a list makes the potential attacker job harder.

- With a **Word** list (*WordList*): random words are taken from a 18,235 long word list.

Expand Down Expand Up @@ -129,19 +125,19 @@ In 2019 a record was set for a computer trying to generate every conceivable pas

## Benchmarks

Specifications:
Specifications:
* Operating system: windows.
* Processor: Intel(R) Core(TM) i5-9400F CPU @ 2.90GHz, 2904 Mhz, 6 Core(s), 6 Logical Processor(s).
* Installed RAM: 16GB.
* Graphics card: GeForce GTX 1060 6GB.

```
BenchmarkPassword 41131 28934 ns/op 19172 B/op 219 allocs/op
BenchmarkNewPassword 41134 28438 ns/op 18291 B/op 222 allocs/op
BenchmarkNewPassphrase 43256 27454 ns/op 6400 B/op 399 allocs/op
BenchmarkPassphrase_NoList 42313 27322 ns/op 5589 B/op 355 allocs/op
BenchmarkPassphrase_WordList 398469 3866 ns/op 752 B/op 45 allocs/op
BenchmarkPassphrase_SyllableList 361910 3714 ns/op 736 B/op 45 allocs/op
BenchmarkPassword 75937 16108 ns/op 4124 B/op 146 allocs/op
BenchmarkNewPassword 73328 16041 ns/op 3541 B/op 152 allocs/op
BenchmarkNewPassphrase 44036 26635 ns/op 6406 B/op 399 allocs/op
BenchmarkPassphrase_NoList 44032 26359 ns/op 5590 B/op 355 allocs/opp
BenchmarkPassphrase_WordList 398469 3866 ns/op 752 B/op 45 allocs/op
BenchmarkPassphrase_SyllableList 361910 3714 ns/op 736 B/op 45 allocs/op
```

Take a look at them [here](/benchmark_test.go).
Expand Down
2 changes: 1 addition & 1 deletion atoll.go
Expand Up @@ -7,7 +7,7 @@ import "math"
// 1 trillion is the number of guesses per second Edward Snowden said we should be prepared for.
const guessesPerSecond = 1000000000000

// Secret is the interface that wraps the basic method Generate.
// Secret is the interface that wraps the basic methods Generate and Entropy.
type Secret interface {
Generate() (string, error)
Entropy() float64
Expand Down
6 changes: 3 additions & 3 deletions atoll_test.go
Expand Up @@ -14,7 +14,7 @@ func TestNewSecret(t *testing.T) {
desc: "Password",
secret: &Password{
Length: 15,
Levels: []Level{Lowercase, Uppercase, Digit, Space, Special},
Levels: []Level{Lower, Upper, Digit, Space, Special},
Include: "=",
Exclude: "?",
Repeat: false,
Expand Down Expand Up @@ -50,7 +50,7 @@ func TestKeyspace(t *testing.T) {
desc: "Password",
secret: &Password{
Length: 13,
Levels: []Level{Lowercase, Uppercase, Digit},
Levels: []Level{Lower, Upper, Digit},
},
},
{
Expand Down Expand Up @@ -83,7 +83,7 @@ func TestSecondsToCrack(t *testing.T) {
desc: "Password",
secret: &Password{
Length: 26,
Levels: []Level{Lowercase, Uppercase},
Levels: []Level{Lower, Upper},
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion benchmark_test.go
Expand Up @@ -4,7 +4,7 @@ import "testing"

var password = &Password{
Length: 15,
Levels: []Level{Lowercase, Uppercase, Digit, Space, Special},
Levels: []Level{Lower, Upper, Digit, Space, Special},
Include: "bench",
Exclude: "mark1234T=%",
Repeat: false,
Expand Down
6 changes: 3 additions & 3 deletions example_test.go
Expand Up @@ -10,7 +10,7 @@ import (
func ExamplePassword() {
p := &atoll.Password{
Length: 22,
Levels: []atoll.Level{atoll.Lowercase, atoll.Uppercase, atoll.Special},
Levels: []atoll.Level{atoll.Lower, atoll.Upper, atoll.Special},
Include: "1+=g ",
Exclude: "&r/ty",
Repeat: false,
Expand All @@ -27,7 +27,7 @@ func ExamplePassword() {
}

func ExampleNewPassword() {
password, err := atoll.NewPassword(16, []atoll.Level{atoll.Digit, atoll.Lowercase})
password, err := atoll.NewPassword(16, []atoll.Level{atoll.Digit, atoll.Lower})
if err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -70,7 +70,7 @@ func ExampleNewPassphrase() {
func ExampleKeyspace() {
p := &atoll.Password{
Length: 6,
Levels: []atoll.Level{atoll.Lowercase},
Levels: []atoll.Level{atoll.Lower},
Repeat: false,
}

Expand Down
126 changes: 67 additions & 59 deletions passphrase.go
Expand Up @@ -9,8 +9,8 @@ import (
)

var (
vowels = [5]string{"a", "e", "i", "o", "u"}
constants = [21]string{"b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n",
vowels = [5]string{"a", "e", "i", "o", "u"}
consonants = [21]string{"b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n",
"p", "q", "r", "s", "t", "v", "w", "x", "y", "z"}
)

Expand Down Expand Up @@ -57,35 +57,12 @@ func (p *Passphrase) Generate() (string, error) {
}

func (p *Passphrase) generate() (string, error) {
if p.Length < 1 {
return "", errors.New("passphrase length must be equal to or higher than 1")
}

if len(p.Include) > int(p.Length) {
return "", errors.New("number of words to include exceed the password length")
}

// Look 2/3 bytes characters
if len(p.Separator) != len([]rune(p.Separator)) {
return "", fmt.Errorf("separator %q contains invalid characters", p.Separator)
}

for _, incl := range p.Include {
// Look for words contaning 2/3 bytes characters
if len(incl) != len([]rune(incl)) {
return "", fmt.Errorf("included word %q contains invalid characters", incl)
}

// Check for equality between included and excluded words
for _, excl := range p.Exclude {
if incl == excl {
return "", fmt.Errorf("word %q cannot be included and excluded", excl)
}
}
if err := p.validateParams(); err != nil {
return "", err
}

// Initialize secret slice
p.words = make([]string, int(p.Length))
p.words = make([]string, p.Length)
// Defaults
if p.Separator == "" {
p.Separator = " "
Expand All @@ -107,6 +84,37 @@ func (p *Passphrase) generate() (string, error) {
return strings.Join(p.words, p.Separator), nil
}

func (p *Passphrase) validateParams() error {
if p.Length < 1 {
return errors.New("passphrase length must be equal to or higher than 1")
}

if len(p.Include) > int(p.Length) {
return errors.New("number of words to include exceed the password length")
}

// Look for 2/3 bytes characters
if len(p.Separator) != len([]rune(p.Separator)) {
return fmt.Errorf("separator %q contains invalid characters", p.Separator)
}

for _, incl := range p.Include {
// Look for words contaning 2/3 bytes characters
if len(incl) != len([]rune(incl)) {
return fmt.Errorf("included word %q contains invalid characters", incl)
}

// Check for equality between included and excluded words
for _, excl := range p.Exclude {
if incl == excl {
return fmt.Errorf("word %q cannot be included and excluded", excl)
}
}
}

return nil
}

// includeWords randomly inserts included words in the passphrase.
func (p *Passphrase) includeWords() {
// Add included words at the end of the secret
Expand Down Expand Up @@ -144,12 +152,40 @@ func (p *Passphrase) excludeWords() {
}
}

// Entropy returns the passphrase entropy in bits.
//
// If the list used is "NoList" the secret must be already generated.
func (p *Passphrase) Entropy() float64 {
var poolLength int

switch getFuncName(p.List) {
case "NoList":
if len(p.words) == 0 {
return 0
}

words := strings.Join(p.words, "")
// Take out the separators from the secret length
secretLength := len(words) - (len(p.Separator) * int(p.Length))
return math.Log2(math.Pow(float64(len(vowels)+len(consonants)), float64(secretLength)))
case "WordList":
poolLength = len(atollWords)
case "SyllableList":
poolLength = len(atollSyllables)
}

poolLength += len(p.Include) - len(p.Exclude)

// Separators aren't included in the secret length
return math.Log2(math.Pow(float64(poolLength), float64(p.Length)))
}

// NoList generates a random passphrase without using a list, making the potential attacker work harder.
func NoList(p *Passphrase) {
var wg sync.WaitGroup
length := int(p.Length) - len(p.Include)

wg.Add(int(length))
wg.Add(length)
for i := 0; i < length; i++ {
go func(i int) {
p.words[i] = generateRandomWord()
Expand Down Expand Up @@ -177,46 +213,18 @@ func SyllableList(p *Passphrase) {
}
}

// Entropy returns the passphrase entropy in bits.
//
// If the list used is "NoList" the secret must be already generated.
func (p *Passphrase) Entropy() float64 {
var poolLength int

switch getFuncName(p.List) {
case "NoList":
if len(p.words) == 0 {
return 0
}

words := strings.Join(p.words, "")
// Take out the separators from the secret length
secretLength := len(words) - (len(p.Separator) * int(p.Length))
return math.Log2(math.Pow(float64(len(vowels)+len(constants)), float64(secretLength)))
case "WordList":
poolLength = len(atollWords)
case "SyllableList":
poolLength = len(atollSyllables)
}

poolLength += len(p.Include) - len(p.Exclude)

// Separators aren't included in the secret length
return math.Log2(math.Pow(float64(poolLength), float64(p.Length)))
}

// generateRandomWord returns a random sword without using any list or dictionary.
func generateRandomWord() string {
// Words length are randomly selected between 3 and 12 letters.
wordLength := randInt(10) + 3
wordLength := int(randInt(10)) + 3
syllables := make([]string, wordLength)

for i := 0; i < wordLength; i++ {
// Select a number from 0 to 10, 0-3 is a vowel, else a consonant
if randInt(11) <= 3 {
syllables[i] = vowels[randInt(len(vowels))]
} else {
syllables[i] = constants[randInt(len(constants))]
syllables[i] = consonants[randInt(len(consonants))]
}
}

Expand Down
2 changes: 1 addition & 1 deletion passphrase_test.go
Expand Up @@ -194,7 +194,7 @@ func TestPassphraseEntropyNoSecret(t *testing.T) {
Separator: "/",
}

var expected float64 = 0
var expected float64
got := p.Entropy()
if got != expected {
t.Errorf("Expected %f, got %f", expected, got)
Expand Down

0 comments on commit b584e7a

Please sign in to comment.