github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/libs/cosmos-sdk/crypto/keys/hd/hdpath.go (about)

     1  // Package hd provides basic functionality Hierarchical Deterministic Wallets.
     2  //
     3  // The user must understand the overall concept of the BIP 32 and the BIP 44 specs:
     4  //  https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
     5  //  https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
     6  //
     7  // In combination with the bip39 package in go-crypto this package provides the functionality for deriving keys using a
     8  // BIP 44 HD path, or, more general, by passing a BIP 32 path.
     9  //
    10  // In particular, this package (together with bip39) provides all necessary functionality to derive keys from
    11  // mnemonics generated during the cosmos fundraiser.
    12  package hd
    13  
    14  import (
    15  	"crypto/hmac"
    16  	"crypto/sha512"
    17  
    18  	"encoding/binary"
    19  	"fmt"
    20  	"math/big"
    21  	"strconv"
    22  	"strings"
    23  
    24  	"github.com/btcsuite/btcd/btcec"
    25  )
    26  
    27  // BIP44Params wraps BIP 44 params (5 level BIP 32 path).
    28  // To receive a canonical string representation ala
    29  // m / purpose' / coinType' / account' / change / addressIndex
    30  // call String() on a BIP44Params instance.
    31  type BIP44Params struct {
    32  	Purpose      uint32 `json:"purpose"`
    33  	CoinType     uint32 `json:"coinType"`
    34  	Account      uint32 `json:"account"`
    35  	Change       bool   `json:"change"`
    36  	AddressIndex uint32 `json:"addressIndex"`
    37  }
    38  
    39  // NewParams creates a BIP 44 parameter object from the params:
    40  // m / purpose' / coinType' / account' / change / addressIndex
    41  func NewParams(purpose, coinType, account uint32, change bool, addressIdx uint32) *BIP44Params {
    42  	return &BIP44Params{
    43  		Purpose:      purpose,
    44  		CoinType:     coinType,
    45  		Account:      account,
    46  		Change:       change,
    47  		AddressIndex: addressIdx,
    48  	}
    49  }
    50  
    51  // NewParamsFromPath parses the BIP44 path and unmarshals it into a Bip44Params. It supports both
    52  // absolute and relative paths.
    53  func NewParamsFromPath(path string) (*BIP44Params, error) {
    54  	spl := strings.Split(path, "/")
    55  
    56  	// Handle absolute or relative paths
    57  	switch {
    58  	case spl[0] == path:
    59  		return nil, fmt.Errorf("path %s doesn't contain '/' separators", path)
    60  
    61  	case strings.TrimSpace(spl[0]) == "":
    62  		return nil, fmt.Errorf("ambiguous path %s: use 'm/' prefix for absolute paths, or no leading '/' for relative ones", path)
    63  
    64  	case strings.TrimSpace(spl[0]) == "m":
    65  		spl = spl[1:]
    66  	}
    67  
    68  	if len(spl) != 5 {
    69  		return nil, fmt.Errorf("invalid path length %s", path)
    70  	}
    71  
    72  	// Check items can be parsed
    73  	purpose, err := hardenedInt(spl[0])
    74  	if err != nil {
    75  		return nil, fmt.Errorf("invalid HD path purpose %s: %w", spl[0], err)
    76  	}
    77  	coinType, err := hardenedInt(spl[1])
    78  	if err != nil {
    79  		return nil, fmt.Errorf("invalid HD path coin type %s: %w", spl[1], err)
    80  	}
    81  	account, err := hardenedInt(spl[2])
    82  	if err != nil {
    83  		return nil, fmt.Errorf("invalid HD path account %s: %w", spl[2], err)
    84  	}
    85  	change, err := hardenedInt(spl[3])
    86  	if err != nil {
    87  		return nil, fmt.Errorf("invalid HD path change %s: %w", spl[3], err)
    88  	}
    89  
    90  	addressIdx, err := hardenedInt(spl[4])
    91  	if err != nil {
    92  		return nil, fmt.Errorf("invalid HD path address index %s: %w", spl[4], err)
    93  	}
    94  
    95  	// Confirm valid values
    96  	if spl[0] != "44'" {
    97  		return nil, fmt.Errorf("first field in path must be 44', got %s", spl[0])
    98  	}
    99  
   100  	if !isHardened(spl[1]) || !isHardened(spl[2]) {
   101  		return nil,
   102  			fmt.Errorf("second and third field in path must be hardened (ie. contain the suffix ', got %s and %s", spl[1], spl[2])
   103  	}
   104  	if isHardened(spl[3]) || isHardened(spl[4]) {
   105  		return nil,
   106  			fmt.Errorf("fourth and fifth field in path must not be hardened (ie. not contain the suffix ', got %s and %s", spl[3], spl[4])
   107  	}
   108  
   109  	if !(change == 0 || change == 1) {
   110  		return nil, fmt.Errorf("change field can only be 0 or 1")
   111  	}
   112  
   113  	return &BIP44Params{
   114  		Purpose:      purpose,
   115  		CoinType:     coinType,
   116  		Account:      account,
   117  		Change:       change > 0,
   118  		AddressIndex: addressIdx,
   119  	}, nil
   120  }
   121  
   122  func hardenedInt(field string) (uint32, error) {
   123  	field = strings.TrimSuffix(field, "'")
   124  	i, err := strconv.Atoi(field)
   125  	if err != nil {
   126  		return 0, err
   127  	}
   128  	if i < 0 {
   129  		return 0, fmt.Errorf("fields must not be negative. got %d", i)
   130  	}
   131  	return uint32(i), nil
   132  }
   133  
   134  func isHardened(field string) bool {
   135  	return strings.HasSuffix(field, "'")
   136  }
   137  
   138  // NewFundraiserParams creates a BIP 44 parameter object from the params:
   139  // m / 44' / coinType' / account' / 0 / address_index
   140  // The fixed parameters (purpose', coin_type', and change) are determined by what was used in the fundraiser.
   141  func NewFundraiserParams(account, coinType, addressIdx uint32) *BIP44Params {
   142  	return NewParams(44, coinType, account, false, addressIdx)
   143  }
   144  
   145  // DerivationPath returns the BIP44 fields as an array.
   146  func (p BIP44Params) DerivationPath() []uint32 {
   147  	change := uint32(0)
   148  	if p.Change {
   149  		change = 1
   150  	}
   151  	return []uint32{
   152  		p.Purpose,
   153  		p.CoinType,
   154  		p.Account,
   155  		change,
   156  		p.AddressIndex,
   157  	}
   158  }
   159  
   160  // String returns the full absolute HD path of the BIP44 (https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) params:
   161  // m / purpose' / coin_type' / account' / change / address_index
   162  func (p BIP44Params) String() string {
   163  	var changeStr string
   164  	if p.Change {
   165  		changeStr = "1"
   166  	} else {
   167  		changeStr = "0"
   168  	}
   169  	return fmt.Sprintf("m/%d'/%d'/%d'/%s/%d",
   170  		p.Purpose,
   171  		p.CoinType,
   172  		p.Account,
   173  		changeStr,
   174  		p.AddressIndex)
   175  }
   176  
   177  // ComputeMastersFromSeed returns the master public key, master secret, and chain code in hex.
   178  func ComputeMastersFromSeed(seed []byte) (secret [32]byte, chainCode [32]byte) {
   179  	masterSecret := []byte("Bitcoin seed")
   180  	secret, chainCode = i64(masterSecret, seed)
   181  
   182  	return
   183  }
   184  
   185  // DerivePrivateKeyForPath derives the private key by following the BIP 32/44 path from privKeyBytes,
   186  // using the given chainCode.
   187  func DerivePrivateKeyForPath(privKeyBytes [32]byte, chainCode [32]byte, path string) ([32]byte, error) {
   188  	data := privKeyBytes
   189  	parts := strings.Split(path, "/")
   190  
   191  	switch {
   192  	case parts[0] == path:
   193  		return [32]byte{}, fmt.Errorf("path '%s' doesn't contain '/' separators", path)
   194  	case strings.TrimSpace(parts[0]) == "m":
   195  		parts = parts[1:]
   196  	}
   197  
   198  	for _, part := range parts {
   199  		// do we have an apostrophe?
   200  		harden := part[len(part)-1:] == "'"
   201  		// harden == private derivation, else public derivation:
   202  		if harden {
   203  			part = part[:len(part)-1]
   204  		}
   205  
   206  		// As per the extended keys specification in
   207  		// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys
   208  		// index values are in the range [0, 1<<31-1] aka [0, max(int32)]
   209  		idx, err := strconv.ParseUint(part, 10, 31)
   210  		if err != nil {
   211  			return [32]byte{}, fmt.Errorf("invalid BIP 32 path %s: %w", path, err)
   212  		}
   213  
   214  		data, chainCode = derivePrivateKey(data, chainCode, uint32(idx), harden)
   215  	}
   216  	var derivedKey [32]byte
   217  	n := copy(derivedKey[:], data[:])
   218  	if n != 32 || len(data) != 32 {
   219  		return [32]byte{}, fmt.Errorf("expected a key of length 32, got length: %d", len(data))
   220  	}
   221  
   222  	return derivedKey, nil
   223  }
   224  
   225  // derivePrivateKey derives the private key with index and chainCode.
   226  // If harden is true, the derivation is 'hardened'.
   227  // It returns the new private key and new chain code.
   228  // For more information on hardened keys see:
   229  //  - https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
   230  func derivePrivateKey(privKeyBytes [32]byte, chainCode [32]byte, index uint32, harden bool) ([32]byte, [32]byte) {
   231  	var data []byte
   232  	if harden {
   233  		index |= 0x80000000
   234  		data = append([]byte{byte(0)}, privKeyBytes[:]...)
   235  	} else {
   236  		// this can't return an error:
   237  		_, ecPub := btcec.PrivKeyFromBytes(btcec.S256(), privKeyBytes[:])
   238  		pubkeyBytes := ecPub.SerializeCompressed()
   239  		data = pubkeyBytes
   240  
   241  		/* By using btcec, we can remove the dependency on tendermint/crypto/secp256k1
   242  		pubkey := secp256k1.PrivKeySecp256k1(privKeyBytes).PubKey()
   243  		public := pubkey.(secp256k1.PubKeySecp256k1)
   244  		data = public[:]
   245  		*/
   246  	}
   247  	data = append(data, uint32ToBytes(index)...)
   248  	data2, chainCode2 := i64(chainCode[:], data)
   249  	x := addScalars(privKeyBytes[:], data2[:])
   250  	return x, chainCode2
   251  }
   252  
   253  // modular big endian addition
   254  func addScalars(a []byte, b []byte) [32]byte {
   255  	aInt := new(big.Int).SetBytes(a)
   256  	bInt := new(big.Int).SetBytes(b)
   257  	sInt := new(big.Int).Add(aInt, bInt)
   258  	x := sInt.Mod(sInt, btcec.S256().N).Bytes()
   259  	x2 := [32]byte{}
   260  	copy(x2[32-len(x):], x)
   261  	return x2
   262  }
   263  
   264  func uint32ToBytes(i uint32) []byte {
   265  	b := [4]byte{}
   266  	binary.BigEndian.PutUint32(b[:], i)
   267  	return b[:]
   268  }
   269  
   270  // i64 returns the two halfs of the SHA512 HMAC of key and data.
   271  func i64(key []byte, data []byte) (il [32]byte, ir [32]byte) {
   272  	mac := hmac.New(sha512.New, key)
   273  	// sha512 does not err
   274  	_, _ = mac.Write(data)
   275  
   276  	I := mac.Sum(nil)
   277  	copy(il[:], I[:32])
   278  	copy(ir[:], I[32:])
   279  
   280  	return
   281  }