1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
|
// Package mnemonics is a package that converts []byte's into human-friendly
// phrases, using common words pulled from a dictionary. The dictionary size is
// 1626, and multiple languages are supported. Each dictionary supports
// modified phrases. Only the first few characters of each word are important.
// These characters form a unique prefix. For example, in the English
// dictionary, the unique prefix len (EnglishUniquePrefixLen) is 3, which means
// the word 'abbey' could be replaced with the word 'abbot', and the program
// would still run as expected.
//
// The primary purpose of this library is creating human-friendly
// cryptographically secure passwords. A cryptographically secure password
// needs to contain between 128 and 256 bits of entropy. Humans are typically
// incapable of generating sufficiently secure passwords without a random
// number generator, and 256-bit random numbers tend to difficult to memorize
// and even to write down (a single mistake in the writing, or even a single
// somewhat sloppy character can render the backup useless).
//
// By using a small set of common words instead of random numbers, copying
// errors are more easily spotted and memorization is also easier, without
// sacrificing password strength.
//
// The mnemonics package does not have any functions for actually generating
// entropy, it just converts existing entropy into human-friendly phrases.
package mnemonics
import (
"errors"
"math/big"
"strings"
"unicode/utf8"
"golang.org/x/text/unicode/norm"
)
const (
// DictionarySize specifies the size of the dictionaries that are used by
// the mnemonics package. All dictionaries are the same length so that the
// same []byte can be encoded into multiple languages and all results will
// resemble eachother.
DictionarySize = 1626
)
var (
errEmptyInput = errors.New("input has len 0 - not valid for conversion")
errUnknownDictionary = errors.New("language not recognized")
errUnknownWord = errors.New("word not found in dictionary for given language")
)
type (
// DictionaryID is a type-safe identifier that indicates which dictionary
// should be used.
DictionaryID string
// Dictionary is a DictionarySize list of words which can be used to create
// human-friendly entropy.
Dictionary [DictionarySize]string
// Phrase is the human readable version of a random []byte. Most typically,
// a phrase is displayed to the user using the String method.
Phrase []string
)
// The conversion functions can be seen as changing the base of a number. A
// []byte can actually be viewed as a slice of base-256 numbers, and a []dict
// can be viewed as a slice of base-1626 numbers. The conversions are a little
// strange because leading 0's need to be preserved.
//
// For example, in base 256:
//
// {0} -> 0
// {255} -> 255
// {0, 0} -> 256
// {1, 0} -> 257
// {0, 1} -> 512
//
// Every possible []byte has a unique big.Int which represents it, and every
// big.Int represents a unique []byte.
// bytesToInt converts a byte slice to a big.Int in a way that preserves
// leading 0s, and ensures there is a perfect 1:1 mapping between Int's and
// []byte's.
func bytesToInt(bs []byte) *big.Int {
base := big.NewInt(256)
exp := big.NewInt(1)
result := big.NewInt(-1)
for i := 0; i < len(bs); i++ {
tmp := big.NewInt(int64(bs[i]))
tmp.Add(tmp, big.NewInt(1))
tmp.Mul(tmp, exp)
exp.Mul(exp, base)
result.Add(result, tmp)
}
return result
}
// intToBytes conversts a big.Int to a []byte, following the conventions
// documented at bytesToInt.
func intToBytes(bi *big.Int) (bs []byte) {
base := big.NewInt(256)
for bi.Cmp(base) >= 0 {
i := new(big.Int).Mod(bi, base).Int64()
bs = append(bs, byte(i))
bi.Sub(bi, base)
bi.Div(bi, base)
}
bs = append(bs, byte(bi.Int64()))
return bs
}
// phraseToInt coverts a phrase into a big.Int, using logic similar to
// bytesToInt.
func phraseToInt(p Phrase, did DictionaryID) (*big.Int, error) {
// Determine which dictionary to use based on the input language.
var dict Dictionary
var prefixLen int
switch {
case did == English:
dict = englishDictionary
prefixLen = EnglishUniquePrefixLen
case did == German:
dict = germanDictionary
prefixLen = GermanUniquePrefixLen
case did == Japanese:
dict = japaneseDictionary
prefixLen = JapaneseUniquePrefixLen
default:
return nil, errUnknownDictionary
}
base := big.NewInt(1626)
exp := big.NewInt(1)
result := big.NewInt(-1)
for _, word := range p {
// Normalize the input.
word = norm.NFC.String(word)
// Get the first prefixLen runes from the string.
var prefix []byte
var runeCount int
for _, r := range word {
encR := make([]byte, utf8.RuneLen(r))
utf8.EncodeRune(encR, r)
prefix = append(prefix, encR...)
runeCount++
if runeCount == prefixLen {
break
}
}
// Find the index associated with the phrase.
var tmp *big.Int
found := false
for j, word := range dict {
if strings.HasPrefix(word, string(prefix)) {
tmp = big.NewInt(int64(j))
found = true
break
}
}
if !found {
return nil, errUnknownWord
}
// Add the index to the int.
tmp.Add(tmp, big.NewInt(1))
tmp.Mul(tmp, exp)
exp.Mul(exp, base)
result.Add(result, tmp)
}
return result, nil
}
// intToPhrase converts a phrase into a big.Int, working in a fashion similar
// to bytesToInt.
func intToPhrase(bi *big.Int, did DictionaryID) (p Phrase, err error) {
// Determine which dictionary to use based on the input language.
var dict Dictionary
switch {
case did == English:
dict = englishDictionary
case did == German:
dict = germanDictionary
case did == Japanese:
dict = japaneseDictionary
default:
return nil, errUnknownDictionary
}
base := big.NewInt(DictionarySize)
for bi.Cmp(base) >= 0 {
i := new(big.Int).Mod(bi, base).Int64()
p = append(p, dict[i])
bi.Sub(bi, base)
bi.Div(bi, base)
}
p = append(p, dict[bi.Int64()])
return p, nil
}
// ToPhrase converts an input []byte to a human-friendly phrase. The conversion
// is reversible.
func ToPhrase(entropy []byte, did DictionaryID) (Phrase, error) {
if len(entropy) == 0 {
return nil, errEmptyInput
}
intEntropy := bytesToInt(entropy)
return intToPhrase(intEntropy, did)
}
// FromPhrase converts an input phrase back to the original []byte.
func FromPhrase(p Phrase, did DictionaryID) ([]byte, error) {
if len(p) == 0 {
return nil, errEmptyInput
}
intEntropy, err := phraseToInt(p, did)
if err != nil {
return nil, err
}
return intToBytes(intEntropy), nil
}
// FromString converts an input string into a phrase, and then calls
// 'FromPhrase'.
func FromString(str string, did DictionaryID) ([]byte, error) {
phrase := Phrase(strings.Split(str, " "))
return FromPhrase(phrase, did)
}
// String combines a phrase into a single string by concatenating the
// individual words with space separation.
func (p Phrase) String() string {
return strings.Join(p, " ")
}
|