You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

200 lines
4.9 KiB

7 years ago
7 years ago
  1. package words
  2. import (
  3. "math/big"
  4. "strings"
  5. "github.com/pkg/errors"
  6. "github.com/tendermint/go-crypto/keys/words/wordlist"
  7. )
  8. const BankSize = 2048
  9. // TODO: add error-checking codecs for invalid phrases
  10. type Codec interface {
  11. BytesToWords([]byte) ([]string, error)
  12. WordsToBytes([]string) ([]byte, error)
  13. }
  14. type WordCodec struct {
  15. words []string
  16. bytes map[string]int
  17. check ECC
  18. }
  19. var _ Codec = &WordCodec{}
  20. func NewCodec(words []string) (codec *WordCodec, err error) {
  21. if len(words) != BankSize {
  22. return codec, errors.Errorf("Bank must have %d words, found %d", BankSize, len(words))
  23. }
  24. res := &WordCodec{
  25. words: words,
  26. // TODO: configure this outside???
  27. check: NewIEEECRC32(),
  28. // check: NewIBMCRC16(),
  29. }
  30. return res, nil
  31. }
  32. // LoadCodec loads a pre-compiled language file
  33. func LoadCodec(bank string) (codec *WordCodec, err error) {
  34. words, err := loadBank(bank)
  35. if err != nil {
  36. return codec, err
  37. }
  38. return NewCodec(words)
  39. }
  40. // MustLoadCodec panics if word bank is missing, only for tests
  41. func MustLoadCodec(bank string) *WordCodec {
  42. codec, err := LoadCodec(bank)
  43. if err != nil {
  44. panic(err)
  45. }
  46. return codec
  47. }
  48. // loadBank opens a wordlist file and returns all words inside
  49. func loadBank(bank string) ([]string, error) {
  50. filename := "keys/wordlist/" + bank + ".txt"
  51. words, err := wordlist.Asset(filename)
  52. if err != nil {
  53. return nil, err
  54. }
  55. wordsAll := strings.Split(strings.TrimSpace(string(words)), "\n")
  56. return wordsAll, nil
  57. }
  58. // // TODO: read from go-bind assets
  59. // func getData(filename string) (string, error) {
  60. // f, err := os.Open(filename)
  61. // if err != nil {
  62. // return "", errors.WithStack(err)
  63. // }
  64. // defer f.Close()
  65. // data, err := ioutil.ReadAll(f)
  66. // if err != nil {
  67. // return "", errors.WithStack(err)
  68. // }
  69. // return string(data), nil
  70. // }
  71. // given this many bytes, we will produce this many words
  72. func wordlenFromBytes(numBytes int) int {
  73. // 2048 words per bank, which is 2^11.
  74. // 8 bits per byte, and we add +10 so it rounds up
  75. return (8*numBytes + 10) / 11
  76. }
  77. // given this many words, we will produce this many bytes.
  78. // sometimes there are two possibilities.
  79. // if maybeShorter is true, then represents len OR len-1 bytes
  80. func bytelenFromWords(numWords int) (length int, maybeShorter bool) {
  81. // calculate the max number of complete bytes we could store in this word
  82. length = 11 * numWords / 8
  83. // if one less byte would also generate this length, set maybeShorter
  84. if wordlenFromBytes(length-1) == numWords {
  85. maybeShorter = true
  86. }
  87. return
  88. }
  89. // TODO: add checksum
  90. func (c *WordCodec) BytesToWords(raw []byte) (words []string, err error) {
  91. // always add a checksum to the data
  92. data := c.check.AddECC(raw)
  93. numWords := wordlenFromBytes(len(data))
  94. n2048 := big.NewInt(2048)
  95. nData := big.NewInt(0).SetBytes(data)
  96. nRem := big.NewInt(0)
  97. // Alternative, use condition "nData.BitLen() > 0"
  98. // to allow for shorter words when data has leading 0's
  99. for i := 0; i < numWords; i++ {
  100. nData.DivMod(nData, n2048, nRem)
  101. rem := nRem.Int64()
  102. w := c.words[rem]
  103. // double-check bank on generation...
  104. _, err := c.GetIndex(w)
  105. if err != nil {
  106. return nil, err
  107. }
  108. words = append(words, w)
  109. }
  110. return words, nil
  111. }
  112. func (c *WordCodec) WordsToBytes(words []string) ([]byte, error) {
  113. l := len(words)
  114. if l == 0 {
  115. return nil, errors.New("Didn't provide any words")
  116. }
  117. n2048 := big.NewInt(2048)
  118. nData := big.NewInt(0)
  119. // since we output words based on the remainder, the first word has the lowest
  120. // value... we must load them in reverse order
  121. for i := 1; i <= l; i++ {
  122. rem, err := c.GetIndex(words[l-i])
  123. if err != nil {
  124. return nil, err
  125. }
  126. nRem := big.NewInt(int64(rem))
  127. nData.Mul(nData, n2048)
  128. nData.Add(nData, nRem)
  129. }
  130. // we copy into a slice of the expected size, so it is not shorter if there
  131. // are lots of leading 0s
  132. dataBytes := nData.Bytes()
  133. // copy into the container we have with the expected size
  134. outLen, flex := bytelenFromWords(len(words))
  135. toCheck := make([]byte, outLen)
  136. if len(dataBytes) > outLen {
  137. return nil, errors.New("Invalid data, could not have been generated by this codec")
  138. }
  139. copy(toCheck[outLen-len(dataBytes):], dataBytes)
  140. // validate the checksum...
  141. output, err := c.check.CheckECC(toCheck)
  142. if flex && err != nil {
  143. // if flex, try again one shorter....
  144. toCheck = toCheck[1:]
  145. output, err = c.check.CheckECC(toCheck)
  146. }
  147. return output, err
  148. }
  149. // GetIndex finds the index of the words to create bytes
  150. // Generates a map the first time it is loaded, to avoid needless
  151. // computation when list is not used.
  152. func (c *WordCodec) GetIndex(word string) (int, error) {
  153. // generate the first time
  154. if c.bytes == nil {
  155. b := map[string]int{}
  156. for i, w := range c.words {
  157. if _, ok := b[w]; ok {
  158. return -1, errors.Errorf("Duplicate word in list: %s", w)
  159. }
  160. b[w] = i
  161. }
  162. c.bytes = b
  163. }
  164. // get the index, or an error
  165. rem, ok := c.bytes[word]
  166. if !ok {
  167. return -1, errors.Errorf("Unrecognized word: %s", word)
  168. }
  169. return rem, nil
  170. }