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