/
recoverycode.go
168 lines (149 loc) · 4.09 KB
/
recoverycode.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package recoverycode
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/scrypt"
"github.com/btcsuite/btcd/btcec"
)
const (
kdfKey = "muun:rc"
kdfIterations = 512
kdfBlockSize = 8
kdfParallelizationFactor = 1
kdfOutputLength = 32
)
// CurrentVersion defines the current version number for the recovery codes.
const CurrentVersion = 2
// Alphabet contains all upper-case characters except for numbers/letters that
// look alike.
const Alphabet = "ABCDEFHJKLMNPQRSTUVWXYZ2345789"
// AlphabetLegacy constains the letters that pre version 2 recovery codes can
// contain.
const AlphabetLegacy = "ABCDEFHJKMNPQRSTUVWXYZ2345789"
// Generate creates a new random recovery code using a cryptographically
// secure random number generator.
func Generate() string {
var sb strings.Builder
sb.WriteByte('L')
// we subtract 2 from the version number so that the first character of the
// alphabet correspods to version 2
sb.WriteByte(Alphabet[CurrentVersion-2])
codeLen := 30
for i := 0; i < codeLen; i++ {
sb.WriteByte(randChar(Alphabet))
j := i + 3 // we count the two bytes we wrote before the loop
if j != 0 && i != codeLen-1 && j%4 == 0 {
sb.WriteByte('-')
}
}
return sb.String()
}
// randChar returns a random character from the given string
//
// The algorithm was inspired by BSD arc4random_uniform function to avoid
// modulo bias and ensure a uniform distribution
// http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/lib/libc/crypt/arc4random_uniform.c
func randChar(chars string) byte {
clen := len(chars)
min := -clen % clen
for {
var b [1]byte
_, err := rand.Read(b[:])
if err != nil {
panic("could not read enough random bytes for recovery code")
}
r := int(b[0])
if r < min {
continue
}
return chars[r%clen]
}
}
// ConvertToKey generates a private key using the recovery code as a seed.
//
// The salt parameter is only used for version 1 codes. It will be ignored
// for version 2+ codes.
func ConvertToKey(code, salt string) (*btcec.PrivateKey, error) {
version, err := Version(code)
if err != nil {
return nil, err
}
var input []byte
switch version {
case 1:
saltBytes, err := hex.DecodeString(salt)
if err != nil {
return nil, fmt.Errorf("failed to decode salt: %w", err)
}
input, err = scrypt.Key(
[]byte(code),
saltBytes,
kdfIterations,
kdfBlockSize,
kdfParallelizationFactor,
kdfOutputLength,
)
if err != nil {
return nil, err
}
case 2:
mac := hmac.New(sha256.New, []byte(kdfKey))
mac.Write([]byte(code))
input = mac.Sum(nil)
}
// 2nd return value is the pub key which we don't need right now
priv, _ := btcec.PrivKeyFromBytes(btcec.S256(), input)
return priv, nil
}
// Validate returns an error if the recovery code is not valid or nil otherwise.
func Validate(code string) error {
_, err := Version(code)
return err
}
// Version returns the version that this recovery code corresponds to.
func Version(code string) (int, error) {
if len(code) != 39 { // code contains 32 RC chars + 7 separator chars
return 0, fmt.Errorf("invalid recovery code length %v", len(code))
}
if code[0] == 'L' { // version 2+ codes always start with L
idx := strings.IndexByte(Alphabet, code[1])
if idx == -1 {
return 0, errors.New("invalid recovery code version")
}
if !validateAlphabet(code, Alphabet) {
return 0, fmt.Errorf("invalid recovery code characters")
}
// we add 2 to the idx because the first letter corresponds to code version 2
version := idx + 2
if version > CurrentVersion {
return 0, fmt.Errorf("unrecognized recovery code version: %d", version)
}
return version, nil
}
if !validateAlphabet(code, AlphabetLegacy) {
return 0, fmt.Errorf("invalid recovery code characters")
}
return 1, nil
}
func validateAlphabet(s, alphabet string) bool {
var charsInBlock int
for _, c := range s {
if charsInBlock == 4 {
if c == '-' {
charsInBlock = 0
continue
}
return false
}
if !strings.Contains(alphabet, string(c)) {
return false
}
charsInBlock++
}
return true
}