decred.org/dcrdex@v1.0.5/client/mnemonic/seed.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package mnemonic
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"encoding/binary"
     9  	"errors"
    10  	"fmt"
    11  	"sort"
    12  	"strings"
    13  	"time"
    14  
    15  	"decred.org/dcrdex/dex/encode"
    16  )
    17  
    18  const (
    19  	entropyBytes  = 18 // 144 bits
    20  	timeBytes     = 2
    21  	seedWords     = 15
    22  	secondsPerDay = 86_400
    23  )
    24  
    25  // New generates new random entropy and a mnemonic seed that encodes the
    26  // entropy, the current time, and a 5-bit checksum.
    27  func New() ([]byte, string) {
    28  	entropy := encode.RandomBytes(entropyBytes)
    29  	stamp := time.Now()
    30  	return entropy, generateMnemonic(entropy, stamp)
    31  }
    32  
    33  // DecodeMnemonic decodes the entropy, time, and checksum from the mnemonic
    34  // seed and validates the checksum.
    35  func DecodeMnemonic(mnemonic string) ([]byte, time.Time, error) {
    36  	words := strings.Fields(mnemonic)
    37  	if len(words) != 15 {
    38  		return nil, time.Time{}, fmt.Errorf("expected 15 words, got %d", len(words))
    39  	}
    40  	buf := make([]byte, entropyBytes+timeBytes+1) // extra byte for checksum bits
    41  	var cursor int
    42  	for i := range words {
    43  		v, err := wordIndex(words[i])
    44  		if err != nil {
    45  			return nil, time.Time{}, err
    46  		}
    47  		bs := make([]byte, 2)
    48  		binary.BigEndian.PutUint16(bs, v)
    49  		b0, b1 := bs[0], bs[1]
    50  		byteIdx := cursor / 8
    51  		avail := 8 - (cursor % 8)
    52  		// Take the last three bits from the first byte, b0.
    53  		if avail < 3 {
    54  			buf[byteIdx] |= b0 >> (3 - avail)
    55  			cursor += avail
    56  			byteIdx++
    57  			n := 3 - avail
    58  			buf[byteIdx] = b0 << (8 - n)
    59  			cursor += n
    60  		} else {
    61  			buf[byteIdx] |= (b0 << (avail - 3))
    62  			cursor += 3
    63  		}
    64  		// Append the entire second byte.
    65  		byteIdx = cursor / 8
    66  		avail = 8 - (cursor % 8)
    67  		buf[byteIdx] |= b1 >> (8 - avail)
    68  		cursor += avail
    69  		if avail < 8 {
    70  			byteIdx++
    71  			n := 8 - avail
    72  			buf[byteIdx] |= b1 << (8 - n)
    73  			cursor += n
    74  		}
    75  	}
    76  	// The first 5 bits of the last byte are the checksum.
    77  	acquiredChecksum := buf[entropyBytes+timeBytes] >> 3
    78  	h := sha256.Sum256(buf[:entropyBytes+timeBytes])
    79  	expectedChecksum := h[0] >> 3
    80  	if acquiredChecksum != expectedChecksum {
    81  		return nil, time.Time{}, errors.New("checksum mismatch")
    82  	}
    83  	entropy := buf[:entropyBytes]
    84  	daysB := buf[entropyBytes : entropyBytes+timeBytes]
    85  	days := binary.BigEndian.Uint16(daysB)
    86  	stamp := time.Unix(int64(days)*secondsPerDay, 0)
    87  	return entropy, stamp, nil
    88  }
    89  
    90  // GenerateMnemonic generates a mnemonic seed from the entropy and time.
    91  // Note that the time encoded in the mnemonic seed is truncated to midnight, so
    92  // the time returned when decoding will not be the same as the time passed in
    93  // here.
    94  func GenerateMnemonic(entropy []byte, stamp time.Time) (string, error) {
    95  	if len(entropy) != entropyBytes {
    96  		return "", fmt.Errorf("entropy must be %d bytes", entropyBytes)
    97  	}
    98  	return generateMnemonic(entropy, stamp), nil
    99  }
   100  
   101  // The entropy length is assumed to be correct.
   102  func generateMnemonic(entropy []byte, stamp time.Time) string {
   103  	days := uint16(stamp.Unix() / secondsPerDay)
   104  	timeB := make([]byte, 2)
   105  	binary.BigEndian.PutUint16(timeB, days)
   106  	buf := make([]byte, entropyBytes+timeBytes+1) // extra byte for checksum bits
   107  	copy(buf[:entropyBytes], entropy)
   108  	copy(buf[entropyBytes:entropyBytes+timeBytes], timeB)
   109  	// checksum
   110  	h := sha256.Sum256(buf[:entropyBytes+timeBytes])
   111  	buf[entropyBytes+timeBytes] = h[0] & 248 // 11111000
   112  	var cursor int
   113  	words := make([]string, seedWords)
   114  	for i := 0; i < seedWords; i++ {
   115  		idxB := make([]byte, 2)
   116  		byteIdx := cursor / 8
   117  		remain := 8 - (cursor % 8)
   118  		// We only write three bits to the first byte of the uint16.
   119  		if remain < 3 {
   120  			clearN := 8 - remain
   121  			masked := (buf[byteIdx] << clearN) >> clearN
   122  			idxB[0] = masked << (3 - remain)
   123  			cursor += remain
   124  			byteIdx++
   125  			n := 3 - remain
   126  			idxB[0] |= buf[byteIdx] >> (8 - n)
   127  			cursor += n
   128  		} else {
   129  			// Bits we want are from index (8 - remain) to (11 - remain).
   130  			idxB[0] = (buf[byteIdx] << (8 - remain)) >> 5
   131  			cursor += 3
   132  		}
   133  		// Write all 8 bits of the second byte of the uint16.
   134  		byteIdx = cursor / 8
   135  		remain = 8 - (cursor % 8)
   136  		idxB[1] = buf[byteIdx] << (8 - remain)
   137  		cursor += remain
   138  		if remain < 8 {
   139  			n := 8 - remain
   140  			byteIdx++
   141  			idxB[1] |= buf[byteIdx] >> (8 - n)
   142  			cursor += n
   143  		}
   144  		idx := binary.BigEndian.Uint16(idxB)
   145  		words[i] = wordList[idx]
   146  	}
   147  	return strings.Join(words, " ")
   148  }
   149  
   150  func wordIndex(word string) (uint16, error) {
   151  	i := sort.Search(len(wordList), func(i int) bool {
   152  		return strings.Compare(wordList[i], word) >= 0
   153  	})
   154  	if i == len(wordList) {
   155  		return 0, fmt.Errorf("word %q exceeded range", word)
   156  	}
   157  	if wordList[i] != word {
   158  		return 0, fmt.Errorf("word %q not known. closest match lexicographically is %q", word, wordList[i])
   159  	}
   160  	return uint16(i), nil
   161  }