mirror of
https://github.com/muun/recovery.git
synced 2025-02-23 11:32:33 -05:00
163 lines
3.9 KiB
Go
163 lines
3.9 KiB
Go
package recoverycode
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
fmt "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:
|
|
input, err = scrypt.Key(
|
|
[]byte(code),
|
|
[]byte(salt),
|
|
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 {
|
|
return 0, errors.New("invalid recovery code length")
|
|
}
|
|
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
|
|
}
|