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.

189 lines
4.5 KiB

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