package recoverycode import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "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: 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 { 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 }