|
|
- 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/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
- }
|