Completed
Pull Request — master (#744)
by thomas
37:49 queued 35:29
created

HierarchicalKey::toExtendedKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Key\Deterministic;
6
7
use BitWasp\Bitcoin\Address\BaseAddressCreator;
8
use BitWasp\Bitcoin\Bitcoin;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface;
11
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
12
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
13
use BitWasp\Bitcoin\Crypto\Hash;
14
use BitWasp\Bitcoin\Exceptions\InvalidDerivationException;
15
use BitWasp\Bitcoin\Key\KeyToScript\ScriptAndSignData;
16
use BitWasp\Bitcoin\Key\KeyToScript\ScriptDataFactory;
17
use BitWasp\Bitcoin\Network\NetworkInterface;
18
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\Base58ExtendedKeySerializer;
19
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\ExtendedKeySerializer;
20
use BitWasp\Bitcoin\Util\IntRange;
21
use BitWasp\Buffertools\Buffer;
22
use BitWasp\Buffertools\BufferInterface;
23
24
class HierarchicalKey
25
{
26
    /**
27
     * @var EcAdapterInterface
28
     */
29
    protected $ecAdapter;
30
31
    /**
32
     * @var int
33
     */
34
    private $depth;
35
36
    /**
37
     * @var int
38
     */
39
    private $parentFingerprint;
40
41
    /**
42
     * @var int
43
     */
44
    private $sequence;
45
46
    /**
47
     * @var BufferInterface
48
     */
49
    private $chainCode;
50
51
    /**
52
     * @var KeyInterface
53
     */
54
    private $key;
55
56
    /**
57
     * @var ScriptDataFactory
58
     */
59
    private $scriptDataFactory;
60
61
    /**
62
     * @var ScriptAndSignData|null
63
     */
64
    private $scriptAndSignData;
65
66
    /**
67
     * @param EcAdapterInterface $ecAdapter
68
     * @param ScriptDataFactory $scriptDataFactory
69
     * @param int $depth
70
     * @param int $parentFingerprint
71
     * @param int $sequence
72
     * @param BufferInterface $chainCode
73
     * @param KeyInterface $key
74
     */
75 64
    public function __construct(EcAdapterInterface $ecAdapter, ScriptDataFactory $scriptDataFactory, int $depth, int $parentFingerprint, int $sequence, BufferInterface $chainCode, KeyInterface $key)
76
    {
77 64
        if ($depth < 0 || $depth > IntRange::U8_MAX) {
78
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, must be in range [0 - 255] inclusive');
79
        }
80
81 64
        if ($parentFingerprint < 0 || $parentFingerprint > IntRange::U32_MAX) {
82
            throw new \InvalidArgumentException('Invalid fingerprint for BIP32 key, must be in range [0 - (2^31)-1] inclusive');
83
        }
84
85 64
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
86
            throw new \InvalidArgumentException('Invalid sequence for BIP32 key, must be in range [0 - (2^31)-1] inclusive');
87
        }
88
89 64
        if ($chainCode->getSize() !== 32) {
90
            throw new \RuntimeException('Chaincode should be 32 bytes');
91
        }
92
93 64
        if (!$key->isCompressed()) {
94 1
            throw new \InvalidArgumentException('A HierarchicalKey must always be compressed');
95
        }
96
97 63
        $this->ecAdapter = $ecAdapter;
98 63
        $this->depth = $depth;
99 63
        $this->sequence = $sequence;
100 63
        $this->parentFingerprint = $parentFingerprint;
101 63
        $this->chainCode = $chainCode;
102 63
        $this->key = $key;
103 63
        $this->scriptDataFactory = $scriptDataFactory;
104 63
    }
105
106
    /**
107
     * Return the depth of this key. This is limited to 256 sequential derivations.
108
     *
109
     * @return int
110
     */
111 30
    public function getDepth(): int
112
    {
113 30
        return $this->depth;
114
    }
115
116
    /**
117
     * Get the sequence number for this address. Hardened keys are
118
     * created with sequence > 0x80000000. a sequence number lower
119
     * than this can be derived with the public key.
120
     *
121
     * @return int
122
     */
123 24
    public function getSequence(): int
124
    {
125 24
        return $this->sequence;
126
    }
127
128
    /**
129
     * Get the fingerprint of the parent key. For master keys, this is 00000000.
130
     *
131
     * @return int
132
     */
133 26
    public function getFingerprint(): int
134
    {
135 26
        if ($this->getDepth() === 0) {
136 14
            return 0;
137
        }
138
139 22
        return $this->parentFingerprint;
140
    }
141
142
    /**
143
     * Return the fingerprint to be used for child keys.
144
     * @return int
145
     */
146 28
    public function getChildFingerprint(): int
147
    {
148 28
        $pubKeyHash = $this->getPublicKey()->getPubKeyHash();
149 28
        return (int) $pubKeyHash->slice(0, 4)->getInt();
150
    }
151
152
    /**
153
     * Return the chain code - a deterministic 'salt' for HMAC-SHA512
154
     * in child derivations
155
     *
156
     * @return BufferInterface
157
     */
158 24
    public function getChainCode(): BufferInterface
159
    {
160 24
        return $this->chainCode;
161
    }
162
163
    /**
164
     * @return PrivateKeyInterface
165
     */
166 35
    public function getPrivateKey(): PrivateKeyInterface
167
    {
168 35
        if ($this->key->isPrivate()) {
169
            /** @var PrivateKeyInterface $key */
170 33
            $key = $this->key;
171 33
            return $key;
172
        }
173
174 2
        throw new \RuntimeException('Unable to get private key, not known');
175
    }
176
177
    /**
178
     * Get the public key the private key or public key.
179
     *
180
     * @return PublicKeyInterface
181
     */
182 35
    public function getPublicKey(): PublicKeyInterface
183
    {
184 35
        if ($this->isPrivate()) {
185 29
            return $this->getPrivateKey()->getPublicKey();
186
        } else {
187
            /** @var PublicKeyInterface $key */
188 25
            $key = $this->key;
189 25
            return $key;
190
        }
191
    }
192
193
    /**
194
     * @return HierarchicalKey
195
     */
196 21
    public function withoutPrivateKey(): HierarchicalKey
197
    {
198 21
        $clone = clone $this;
199 21
        $clone->key = $clone->getPublicKey();
200 21
        return $clone;
201
    }
202
203
    /**
204
     * @return ScriptDataFactory
205
     */
206 26
    public function getScriptDataFactory()
207
    {
208 26
        return $this->scriptDataFactory;
209
    }
210
211
    /**
212
     * @return \BitWasp\Bitcoin\Key\KeyToScript\ScriptAndSignData
213
     */
214 22
    public function getScriptAndSignData()
215
    {
216 22
        if (null === $this->scriptAndSignData) {
217 22
            $this->scriptAndSignData = $this->scriptDataFactory->convertKey($this->key);
218
        }
219
220 22
        return $this->scriptAndSignData;
221
    }
222
223
    /**
224
     * @param BaseAddressCreator $addressCreator
225
     * @return \BitWasp\Bitcoin\Address\Address
226
     */
227 16
    public function getAddress(BaseAddressCreator $addressCreator)
228
    {
229 16
        return $this->getScriptAndSignData()->getAddress($addressCreator);
230
    }
231
232
    /**
233
     * Return whether this is a private key
234
     *
235
     * @return bool
236
     */
237 39
    public function isPrivate(): bool
238
    {
239 39
        return $this->key->isPrivate();
240
    }
241
242
    /**
243
     * Return whether the key is hardened
244
     *
245
     * @return bool
246
     */
247 2
    public function isHardened(): bool
248
    {
249 2
        return ($this->sequence >> 31) === 1;
250
    }
251
252
    /**
253
     * Create a buffer containing data to be hashed hashed to yield the child offset
254
     *
255
     * @param int $sequence
256
     * @return BufferInterface
257
     * @throws \Exception
258
     */
259 35
    public function getHmacSeed(int $sequence): BufferInterface
260
    {
261 35
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
262 6
            throw new \InvalidArgumentException("Sequence is outside valid range, must be >= 0 && <= (2^31)-1");
263
        }
264
265 29
        if (($sequence >> 31) === 1) {
266 28
            if ($this->isPrivate() === false) {
267 2
                throw new \Exception("Can't derive a hardened key without the private key");
268
            }
269
270 26
            $data = "\x00{$this->getPrivateKey()->getBinary()}";
271
        } else {
272 27
            $data = $this->getPublicKey()->getBinary();
273
        }
274
275 27
        return new Buffer($data . pack("N", $sequence));
276
    }
277
278
    /**
279
     * Derive a child key
280
     *
281
     * @param int $sequence
282
     * @return HierarchicalKey
283
     * @throws \BitWasp\Bitcoin\Exceptions\InvalidDerivationException
284
     */
285 32
    public function deriveChild(int $sequence): HierarchicalKey
286
    {
287 32
        $nextDepth = $this->depth + 1;
288 32
        if ($nextDepth > 255) {
289
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, cannot exceed 255');
290
        }
291
292 32
        $hash = Hash::hmac('sha512', $this->getHmacSeed($sequence), $this->chainCode);
293 27
        $offset = $hash->slice(0, 32);
294 27
        $chain = $hash->slice(32);
295
296 27
        if (!$this->ecAdapter->validatePrivateKey($offset)) {
297 1
            throw new InvalidDerivationException("Derived invalid key for index {$sequence}, use next index");
298
        }
299
300 26
        $key = $this->isPrivate() ? $this->getPrivateKey() : $this->getPublicKey();
301 26
        $key = $key->tweakAdd($offset->getGmp());
302
303 26
        return new HierarchicalKey(
304 26
            $this->ecAdapter,
305 26
            $this->scriptDataFactory,
306 26
            $nextDepth,
307 26
            $this->getChildFingerprint(),
308 26
            $sequence,
309 26
            $chain,
310 26
            $key
311
        );
312
    }
313
314
    /**
315
     * @param array|\stdClass|\Traversable $list
316
     * @return HierarchicalKey
317
     * @throws \Exception
318
     */
319
    public function deriveFromList($list): HierarchicalKey
320
    {
321
        if (!is_array($list) && !$list instanceof \Traversable && !$list instanceof \stdClass) {
322
            throw new \InvalidArgumentException('List must be an array or \Traversable');
323
        }
324
325
        $key = $this;
326
        foreach ($list as $sequence) {
0 ignored issues
show
Bug introduced by
The expression $list of type array|object<Traversable>|object<stdClass> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
327
            $key = $key->deriveChild((int) $sequence);
328
        }
329
330
        return $key;
331
    }
332
333
    /**
334
     * Decodes a BIP32 path into actual 32bit sequence numbers and derives the child key
335
     *
336
     * @param string $path
337
     * @return HierarchicalKey
338
     * @throws \Exception
339
     */
340 26
    public function derivePath(string $path): HierarchicalKey
341
    {
342 26
        $sequences = new HierarchicalKeySequence();
343 26
        $parts = $sequences->decodeRelative($path);
344 26
        $numParts = count($parts);
345
346 26
        $key = $this;
347 26
        for ($i = 0; $i < $numParts; $i++) {
348
            try {
349 26
                $key = $key->deriveChild((int) $parts[$i]);
350
            } catch (InvalidDerivationException $e) {
351
                if ($i === $numParts - 1) {
352
                    throw new InvalidDerivationException($e->getMessage());
353
                } else {
354
                    throw new InvalidDerivationException("Invalid derivation for non-terminal index: cannot use this path!");
355
                }
356
            }
357
        }
358
359 26
        return $key;
360
    }
361
362
    /**
363
     * Serializes the instance according to whether it wraps a private or public key.
364
     * @param NetworkInterface $network
365
     * @return string
366
     */
367 16
    public function toExtendedKey(NetworkInterface $network = null): string
368
    {
369 16
        $network = $network ?: Bitcoin::getNetwork();
370
371 16
        $extendedSerializer = new Base58ExtendedKeySerializer(new ExtendedKeySerializer($this->ecAdapter));
372 16
        $extended = $extendedSerializer->serialize($network, $this);
373 16
        return $extended;
374
    }
375
376
    /**
377
     * Explicitly serialize as a private key. Throws an exception if
378
     * the key isn't private.
379
     *
380
     * @param NetworkInterface $network
381
     * @return string
382
     */
383 16
    public function toExtendedPrivateKey(NetworkInterface $network = null): string
384
    {
385 16
        if (!$this->isPrivate()) {
386 2
            throw new \LogicException('Cannot create extended private key from public');
387
        }
388
389 14
        return $this->toExtendedKey($network);
390
    }
391
392
    /**
393
     * Explicitly serialize as a public key. This will always work.
394
     *
395
     * @param NetworkInterface $network
396
     * @return string
397
     */
398 13
    public function toExtendedPublicKey(NetworkInterface $network = null): string
399
    {
400 13
        return $this->withoutPrivateKey()->toExtendedKey($network);
401
    }
402
}
403