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 }