This repository has been archived by the owner on Jul 15, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 32
/
wordcodec.go
200 lines (170 loc) · 4.88 KB
/
wordcodec.go
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
package words
import (
"math/big"
"strings"
"github.com/pkg/errors"
"github.com/tendermint/go-crypto/keys/words/wordlist"
)
const BankSize = 2048
// TODO: add error-checking codecs for invalid phrases
type Codec interface {
BytesToWords([]byte) ([]string, error)
WordsToBytes([]string) ([]byte, error)
}
type WordCodec struct {
words []string
bytes map[string]int
check ECC
}
var _ Codec = &WordCodec{}
func NewCodec(words []string) (codec *WordCodec, err error) {
if len(words) != BankSize {
return codec, errors.Errorf("Bank must have %d words, found %d", BankSize, len(words))
}
res := &WordCodec{
words: words,
// TODO: configure this outside???
check: NewIEEECRC32(),
// check: NewIBMCRC16(),
}
return res, nil
}
// LoadCodec loads a pre-compiled language file
func LoadCodec(bank string) (codec *WordCodec, err error) {
words, err := loadBank(bank)
if err != nil {
return codec, err
}
return NewCodec(words)
}
// MustLoadCodec panics if word bank is missing, only for tests
func MustLoadCodec(bank string) *WordCodec {
codec, err := LoadCodec(bank)
if err != nil {
panic(err)
}
return codec
}
// loadBank opens a wordlist file and returns all words inside
func loadBank(bank string) ([]string, error) {
filename := "keys/words/wordlist/" + bank + ".txt"
words, err := wordlist.Asset(filename)
if err != nil {
return nil, err
}
wordsAll := strings.Split(strings.TrimSpace(string(words)), "\n")
return wordsAll, nil
}
// // TODO: read from go-bind assets
// func getData(filename string) (string, error) {
// f, err := os.Open(filename)
// if err != nil {
// return "", errors.WithStack(err)
// }
// defer f.Close()
// data, err := ioutil.ReadAll(f)
// if err != nil {
// return "", errors.WithStack(err)
// }
// return string(data), nil
// }
// given this many bytes, we will produce this many words
func wordlenFromBytes(numBytes int) int {
// 2048 words per bank, which is 2^11.
// 8 bits per byte, and we add +10 so it rounds up
return (8*numBytes + 10) / 11
}
// given this many words, we will produce this many bytes.
// sometimes there are two possibilities.
// if maybeShorter is true, then represents len OR len-1 bytes
func bytelenFromWords(numWords int) (length int, maybeShorter bool) {
// calculate the max number of complete bytes we could store in this word
length = 11 * numWords / 8
// if one less byte would also generate this length, set maybeShorter
if wordlenFromBytes(length-1) == numWords {
maybeShorter = true
}
return
}
// TODO: add checksum
func (c *WordCodec) BytesToWords(raw []byte) (words []string, err error) {
// always add a checksum to the data
data := c.check.AddECC(raw)
numWords := wordlenFromBytes(len(data))
n2048 := big.NewInt(2048)
nData := big.NewInt(0).SetBytes(data)
nRem := big.NewInt(0)
// Alternative, use condition "nData.BitLen() > 0"
// to allow for shorter words when data has leading 0's
for i := 0; i < numWords; i++ {
nData.DivMod(nData, n2048, nRem)
rem := nRem.Int64()
w := c.words[rem]
// double-check bank on generation...
_, err := c.GetIndex(w)
if err != nil {
return nil, err
}
words = append(words, w)
}
return words, nil
}
func (c *WordCodec) WordsToBytes(words []string) ([]byte, error) {
l := len(words)
if l == 0 {
return nil, errors.New("Didn't provide any words")
}
n2048 := big.NewInt(2048)
nData := big.NewInt(0)
// since we output words based on the remainder, the first word has the lowest
// value... we must load them in reverse order
for i := 1; i <= l; i++ {
rem, err := c.GetIndex(words[l-i])
if err != nil {
return nil, err
}
nRem := big.NewInt(int64(rem))
nData.Mul(nData, n2048)
nData.Add(nData, nRem)
}
// we copy into a slice of the expected size, so it is not shorter if there
// are lots of leading 0s
dataBytes := nData.Bytes()
// copy into the container we have with the expected size
outLen, flex := bytelenFromWords(len(words))
toCheck := make([]byte, outLen)
if len(dataBytes) > outLen {
return nil, errors.New("Invalid data, could not have been generated by this codec")
}
copy(toCheck[outLen-len(dataBytes):], dataBytes)
// validate the checksum...
output, err := c.check.CheckECC(toCheck)
if flex && err != nil {
// if flex, try again one shorter....
toCheck = toCheck[1:]
output, err = c.check.CheckECC(toCheck)
}
return output, err
}
// GetIndex finds the index of the words to create bytes
// Generates a map the first time it is loaded, to avoid needless
// computation when list is not used.
func (c *WordCodec) GetIndex(word string) (int, error) {
// generate the first time
if c.bytes == nil {
b := map[string]int{}
for i, w := range c.words {
if _, ok := b[w]; ok {
return -1, errors.Errorf("Duplicate word in list: %s", w)
}
b[w] = i
}
c.bytes = b
}
// get the index, or an error
rem, ok := c.bytes[word]
if !ok {
return -1, errors.Errorf("Unrecognized word: %s", word)
}
return rem, nil
}