Skip to content

Commit

Permalink
Randomize indices, bug fixes and docs update
Browse files Browse the repository at this point in the history
  • Loading branch information
GGP1 committed Jan 3, 2021
1 parent 50a0c25 commit 3f86d7f
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 221 deletions.
81 changes: 43 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
[![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)

Atoll is a package for generating secure random secrets.
Atoll is a library for generating cryptographically secure and higly random secrets.

# Table of contents

- [Installation](#installation)
- [Features](#features)
- [Examples](#examples)
- [Installation](#installation)
- [Usage](#usage)
- [Documentation](#documentation)
* [Password format levels](#password-format-levels)
* [Passphrases options](#passphrases-options)
Expand All @@ -20,32 +20,30 @@ Atoll is a package for generating secure random secrets.
- [Benchmarks](#benchmarks)
- [License](#license)

## Installation

```
go get -u github.com/GGP1/atoll
```

## Features

- High level of randomness
- Well tested, coverage: 100% of statements
- Well tested, coverage: 100.0% of statements
- 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**:
* Up to 6 different [format levels](#password-format-levels)
* Up to 5 different [format levels](#password-format-levels)
* Enable/disable character repetition
- **Passphrase**:
* Choose between Word, Syllable or No list options to generate the passphrase
* Custom word/syllable separator

## Examples
## Installation

Head over [example_test.go](/example_test.go) to see more examples.
```
go get -u github.com/GGP1/atoll
```

## Usage

```
package main
Expand All @@ -58,9 +56,10 @@ import (
)
func main() {
// Generate a random password
p := &atoll.Password{
Length: 16,
Format: []int{1, 2, 3, 4, 5, 6},
Format: []int{1, 2, 3, 4, 5},
Include: "á&1",
Repeat: true,
}
Expand All @@ -73,16 +72,25 @@ func main() {
fmt.Println(password)
// A simpler way
password2, err := atoll.NewPassword(20, []int{1, 2, 3, 4})
// Generate a random passphrase
p2 := &atoll.Passphrase{
Length: 7,
Separator: "/",
List: atoll.NoList,
}
// This could be done by calling p2.Generate() aswell
passphrase, err := atoll.NewSecret(p2)
if err != nil {
log.Fatal(err)
}
fmt.Println(password2)
fmt.Println(passphrase)
}
```

Head over [example_test.go](/example_test.go) to see more examples.

## Documentation

### Password format levels
Expand All @@ -92,7 +100,6 @@ func main() {
3. Digits (1, 2, 3...)
4. Space
5. Special (!, $, %...)
6. Extended UTF-8 (¡, ¢, £, ¤, ¥...)

### Passphrases options

Expand All @@ -108,24 +115,23 @@ Atoll offers 3 ways of generating a passphrase:

> Randomness is a measure of the observer's ignorance, not an inherent quality of a process.
Atoll uses the "crypto/rand" package to generate **cryptographically secure** random numbers and using them to select characters/words/syllables from different pools.
Atoll uses the "crypto/rand" package to generate **cryptographically secure** random numbers, which "select" the characters-words-syllables from different pools and also the indices when generating a password.

### Entropy

> Entropy is a measure of the uncertainty or randomness of a system. The concept is a difficult one to grasp fully and is confusing, even to experts. Strictly speaking, any given passphrase has an entropy of zero because it is already chosen. It is the method you use to randomly select your passphrase that has entropy. Entropy tells how hard it will be to guess the passphrase itself even if an attacker knows the method you used to select your passphrase. A passphrase is more secure if it is selected using a method that has more entropy. Entropy is measured in bits. The outcome of a single coin toss -- "heads or tails" -- has one bit of entropy. - Arnold G. Reinhold
entropy := log2(poolLength ^ secretLength)
`entropy := log2(poolLength ^ secretLength)`

Pool lengths:
**Pool lengths**:

1. Password formats:
* Level 1 (lowercases): 26
* Level 2 (uppercases): 26
* Level 3 (digits): 10
* Level 4 (space): 1
* Level 5 (specials): 32
* Level 6 (extendedUTF8): 95
2. Passphrase No list (must be calculated word by word): 26 ^ number of letters in the word
2. Passphrase No list (must be calculated word by word): 26 ^ word length
3. Passphrase Word list: 18,325
4. Passphrase Syllable list: 10,129

Expand All @@ -143,28 +149,27 @@ In case you want to obtain more information about the secret security, here are
> 1,000,000,000,000,000 (1 trillion) is the number of guesses per second Edward Snowden said we should be prepared for
* Password: (((2 ^ entropy) / 1,000,000,000,000,000) / 2)
* Passphrase:

```words := strings.Split(p.Secret, p.Separator)```

NoList: 26^SumWordsLength^len(words) -> iterate over words and sum each word length

WordList and SyllableList: (((2 ^ entropy) - len(words)) / 1,000,000,000,000,000) / 2
+ *NoList*: 26^SumWordsLength^len(words) -> iterate over words and sum each word length.
+ *WordList* and *SyllableList*: (((2 ^ entropy) - len(words)) / 1,000,000,000,000,000) / 2

## Benchmarks

GOOS: windows
- GOOS: windows
- GOARCH: amd64
- GOMAXPROCS: 6

GOARCH: amd64
SPECS:
Processor Intel(R) Core(TM) i5-9400F CPU @ 2.90GHz, 2904 Mhz, 6 Core(s), 6 Logical Processor(s)

GOMAXPROCS: 6
16GB RAM

```
BenchmarkPassword 44286 27166 ns/op 18725 B/op 157 allocs/op
BenchmarkNewPassword 50005 23908 ns/op 16091 B/op 124 allocs/op
BenchmarkNewPassphrase 33240 36071 ns/op 7825 B/op 405 allocs/op
BenchmarkPassphrase_NoList 32966 36492 ns/op 7832 B/op 406 allocs/op
BenchmarkPassphrase_WordList 374997 3229 ns/op 576 B/op 27 allocs/op
BenchmarkPassphrase_SyllableList 400014 2925 ns/op 560 B/op 27 allocs/op
BenchmarkPassword 43316 27158 ns/op 17313 B/op 197 allocs/op
BenchmarkNewPassword 39214 30475 ns/op 19637 B/op 239 allocs/op
BenchmarkNewPassphrase 21698 55174 ns/op 8854 B/op 578 allocs/op
BenchmarkPassphrase_NoList 21978 55008 ns/op 8858 B/op 579 allocs/op
BenchmarkPassphrase_WordList 387115 3051 ns/op 576 B/op 27 allocs/op
BenchmarkPassphrase_SyllableList 428774 2810 ns/op 560 B/op 27 allocs/op
```

Take a look at them [here](/benchmark_test.go).
Expand Down
8 changes: 4 additions & 4 deletions atoll.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package atoll

// Secret is the interface that wraps the basic method Generate.
type Secret interface {
// Generator is the interface that wraps the basic method Generate.
type Generator interface {
Generate() (string, error)
}

// NewSecret generates a new secret.
func NewSecret(secret Secret) (string, error) {
return secret.Generate()
func NewSecret(generator Generator) (string, error) {
return generator.Generate()
}
12 changes: 6 additions & 6 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (

var password = &Password{
Length: 15,
Format: []uint8{1, 2, 3, 4, 5, 6},
Include: "bénch",
Exclude: "mÄrk",
Format: []uint8{1, 2, 3, 4, 5},
Include: "bench",
Exclude: "mark",
Repeat: true,
}

Expand All @@ -23,7 +23,7 @@ func BenchmarkPassword(b *testing.B) {

func BenchmarkNewPassword(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := NewPassword(15, []uint8{1, 2, 3, 4, 5, 6})
_, err := NewPassword(15, []uint8{1, 2, 3, 4, 5})
if err != nil {
b.Error("Failed generating password")
}
Expand All @@ -33,8 +33,8 @@ func BenchmarkNewPassword(b *testing.B) {
var passphrase = &Passphrase{
Length: 6,
Separator: "-",
Include: []string{"enjóy"},
Exclude: []string{"play¡"},
Include: []string{"enjoy"},
Exclude: []string{"play"},
}

func BenchmarkNewPassphrase(b *testing.B) {
Expand Down
9 changes: 5 additions & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
func ExamplePassword() {
p := &atoll.Password{
Length: 22,
Format: []uint8{1, 2, 3, 4, 5, 6},
Include: "1+=Á",
Exclude: "&r/Ë",
Format: []uint8{1, 2, 3, 4, 5},
Include: "1+=g",
Exclude: "&r/ty",
Repeat: false,
}

Expand All @@ -23,7 +23,7 @@ func ExamplePassword() {

fmt.Println(password)
// Example output:
// H1k+6R0tMU3=aFlh2DCy5O
// AE8f@,1^P_Ws=c!ho`T{Á+
}

func ExampleNewPassword() {
Expand All @@ -36,6 +36,7 @@ func ExampleNewPassword() {
// Example output:
// ?{{5Rt%r3OrE}7?z
}

func ExamplePassphrase() {
p := &atoll.Passphrase{
Length: 8,
Expand Down
70 changes: 29 additions & 41 deletions passphrase.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"strings"
"sync"
)

var (
Expand Down Expand Up @@ -60,22 +59,29 @@ func (p *Passphrase) Generate() (string, error) {
return "", errors.New("atoll: number of words to include exceed the password length")
}

for _, incl := range p.Include {
// Look for words contaning 2/3 bytes characters
if len(incl) != len([]rune(incl)) {
return "", fmt.Errorf("atoll: 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)
}
}
}

// Defaults
if p.Separator == "" {
p.Separator = " "
}

if p.List == nil {
p.List = NoList
}

incl := strings.Join(p.Include, p.Separator)
for _, excl := range p.Exclude {
if incl == excl {
return "", fmt.Errorf("word %q cannot be included and excluded", excl)
}
}

// Generate the secret with the list specified
// Generate the passphrase with the list specified
p.List(p)
// Include and exclude words
if len(p.Include) != 0 {
Expand All @@ -88,25 +94,12 @@ func (p *Passphrase) Generate() (string, error) {
return p.secret, nil
}

// Determine include indices and replace the existing word with an included one.
// includeWords randomly inserts included words in the passphrase, replacing already existing words.
func (p *Passphrase) includeWords() {
indices := make(map[int]struct{}, len(p.Include))

// Take a unique random index for each word
for range p.Include {
repeat:
n := randInt(int(p.Length))
if _, ok := indices[n]; !ok {
indices[n] = struct{}{}
continue
}
goto repeat
}

words := strings.Split(p.secret, p.Separator)

for i := range indices {
words[i] = p.Include[0]
for range p.Include {
words[randInt(len(words))] = p.Include[0]
p.Include = p.Include[1:]
}

Expand Down Expand Up @@ -141,30 +134,24 @@ func (p *Passphrase) excludeWords() {

// NoList generates a random passphrase without using a list, making the potential attacker work harder.
func NoList(p *Passphrase) {
var wg sync.WaitGroup
passphrase := make([]string, p.Length)

wg.Add(len(passphrase))
for i := range passphrase {
go func(i int, passphrase []string) {
defer wg.Done()
passphrase[i] = generateRandomWord()
}(i, passphrase)
passphrase[i] = generateRandomWord()
}
wg.Wait()

p.secret = strings.Join(passphrase, p.Separator)
}

// WordList generates a passphrase using a wordlist (18,325 long).
func WordList(p *Passphrase) {
words := make([]string, p.Length)
passphrase := make([]string, p.Length)

for i := range words {
words[i] = atollWords[randInt(len(atollWords))]
for i := range passphrase {
passphrase[i] = atollWords[randInt(len(atollWords))]
}

p.secret = strings.Join(words, p.Separator)
p.secret = strings.Join(passphrase, p.Separator)
}

// SyllableList generates a passphrase using a syllable list (10,129 long).
Expand All @@ -178,20 +165,21 @@ func SyllableList(p *Passphrase) {
p.secret = strings.Join(passphrase, p.Separator)
}

// generateRandomWord returns a word without using any list or dictionary.
// 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
syllables := make([]string, wordLength)

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

syllables = append(syllables, constants[randInt(len(constants))])
syllables[idx] = constants[randInt(len(constants))]
}

return strings.Join(syllables, "")
Expand Down
Loading

0 comments on commit 3f86d7f

Please sign in to comment.