decred.org/dcrdex@v1.0.3/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 }