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 }