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