github.com/Finschia/finschia-sdk@v0.48.1/crypto/hd/hdpath.go (about)

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