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 }