Completed
Pull Request — 0.0.35 (#660)
by thomas
22:22
created

HierarchicalKey::isHardened()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace BitWasp\Bitcoin\Key\Deterministic;
4
5
use BitWasp\Bitcoin\Address\BaseAddressCreator;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
11
use BitWasp\Bitcoin\Crypto\Hash;
12
use BitWasp\Bitcoin\Key\KeyToScript\ScriptAndSignData;
13
use BitWasp\Bitcoin\Key\KeyToScript\ScriptDataFactory;
14
use BitWasp\Bitcoin\Network\NetworkInterface;
15
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\Base58ExtendedKeySerializer;
16
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\ExtendedKeySerializer;
17
use BitWasp\Bitcoin\Util\IntRange;
18
use BitWasp\Buffertools\Buffer;
19
use BitWasp\Buffertools\BufferInterface;
20
use BitWasp\Buffertools\Buffertools;
21
use BitWasp\Buffertools\Parser;
22
23
class HierarchicalKey
24
{
25
    /**
26
     * @var EcAdapterInterface
27
     */
28
    protected $ecAdapter;
29
30
    /**
31
     * @var int
32
     */
33
    private $depth;
34
35
    /**
36
     * @var int
37
     */
38
    private $parentFingerprint;
39
40
    /**
41
     * @var int
42
     */
43
    private $sequence;
44
45
    /**
46
     * @var BufferInterface
47
     */
48
    private $chainCode;
49
50
    /**
51
     * @var PublicKeyInterface|PrivateKeyInterface
52
     */
53
    private $key;
54
55
    /**
56
     * @var ScriptDataFactory
57
     */
58
    private $scriptDataFactory;
59
60
    /**
61
     * @var ScriptAndSignData|null
62
     */
63
    private $scriptAndSignData;
64
65
    /**
66
     * @param EcAdapterInterface $ecAdapter
67
     * @param ScriptDataFactory $scriptDataFactory
68
     * @param int $depth
69
     * @param int $parentFingerprint
70
     * @param int $sequence
71
     * @param BufferInterface $chainCode
72
     * @param KeyInterface $key
73
     */
74 69
    public function __construct(EcAdapterInterface $ecAdapter, ScriptDataFactory $scriptDataFactory, $depth, $parentFingerprint, $sequence, BufferInterface $chainCode, KeyInterface $key)
75
    {
76 69
        if ($depth < 0 || $depth > IntRange::U8_MAX) {
77
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, must be in range [0 - 255] inclusive');
78
        }
79
80 69
        if ($parentFingerprint < 0 || $parentFingerprint > IntRange::U32_MAX) {
81
            throw new \InvalidArgumentException('Invalid fingerprint for BIP32 key, must be in range [0 - (2^31)-1] inclusive');
82
        }
83
84 69
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
85
            throw new \InvalidArgumentException('Invalid sequence for BIP32 key, must be in range [0 - (2^31)-1] inclusive');
86
        }
87
88 69
        if ($chainCode->getSize() !== 32) {
89
            throw new \RuntimeException('Chaincode should be 32 bytes');
90
        }
91
92 69
        if (!$key->isCompressed()) {
93 1
            throw new \InvalidArgumentException('A HierarchicalKey must always be compressed');
94
        }
95
96 68
        $this->ecAdapter = $ecAdapter;
97 68
        $this->depth = $depth;
98 68
        $this->sequence = $sequence;
99 68
        $this->parentFingerprint = $parentFingerprint;
100 68
        $this->chainCode = $chainCode;
101 68
        $this->key = $key;
0 ignored issues
show
Documentation Bug introduced by
It seems like $key of type object<BitWasp\Bitcoin\C...apter\Key\KeyInterface> is incompatible with the declared type object<BitWasp\Bitcoin\C...ey\PrivateKeyInterface> of property $key.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
102 68
        $this->scriptDataFactory = $scriptDataFactory;
103 68
    }
104
105
    /**
106
     * Return the depth of this key. This is limited to 256 sequential derivations.
107
     *
108
     * @return int
109
     */
110 34
    public function getDepth()
111
    {
112 34
        return $this->depth;
113
    }
114
115
    /**
116
     * Get the sequence number for this address. Hardened keys are
117
     * created with sequence > 0x80000000. a sequence number lower
118
     * than this can be derived with the public key.
119
     *
120
     * @return int
121
     */
122 29
    public function getSequence()
123
    {
124 29
        return $this->sequence;
125
    }
126
127
    /**
128
     * Get the fingerprint of the parent key. For master keys, this is 00000000.
129
     *
130
     * @return string
131
     */
132 30
    public function getFingerprint()
133
    {
134 30
        if ($this->getDepth() === 0) {
135 14
            return 0;
136
        }
137
138 30
        return $this->parentFingerprint;
139
    }
140
141
    /**
142
     * Return the fingerprint to be used for child keys.
143
     * @return int
144
     */
145 31
    public function getChildFingerprint()
146
    {
147 31
        return $this->getPublicKey()->getPubKeyHash()->slice(0, 4)->getInt();
148
    }
149
150
    /**
151
     * Return the chain code - a deterministic 'salt' for HMAC-SHA512
152
     * in child derivations
153
     *
154
     * @return BufferInterface
155
     */
156 28
    public function getChainCode()
157
    {
158 28
        return $this->chainCode;
159
    }
160
161
    /**
162
     * @return PrivateKeyInterface
163
     */
164 40
    public function getPrivateKey()
165
    {
166 40
        if ($this->key->isPrivate()) {
167 38
            return $this->key;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->key; of type BitWasp\Bitcoin\Crypto\E...Key\PrivateKeyInterface adds the type BitWasp\Bitcoin\Crypto\E...\Key\PublicKeyInterface to the return on line 167 which is incompatible with the return type documented by BitWasp\Bitcoin\Key\Dete...hicalKey::getPrivateKey of type BitWasp\Bitcoin\Crypto\E...Key\PrivateKeyInterface.
Loading history...
168
        }
169
170 2
        throw new \RuntimeException('Unable to get private key, not known');
171
    }
172
173
    /**
174
     * Get the public key the private key or public key.
175
     *
176
     * @return PublicKeyInterface
177
     */
178 42
    public function getPublicKey()
179
    {
180 42
        if ($this->isPrivate()) {
181 34
            return $this->getPrivateKey()->getPublicKey();
182
        } else {
183 31
            return $this->key;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->key; of type BitWasp\Bitcoin\Crypto\E...Key\PrivateKeyInterface adds the type BitWasp\Bitcoin\Crypto\E...Key\PrivateKeyInterface to the return on line 183 which is incompatible with the return type documented by BitWasp\Bitcoin\Key\Dete...chicalKey::getPublicKey of type BitWasp\Bitcoin\Crypto\E...\Key\PublicKeyInterface.
Loading history...
184
        }
185
    }
186
187
    /**
188
     * @return HierarchicalKey
189
     */
190 25
    public function withoutPrivateKey()
191
    {
192 25
        $clone = clone $this;
193 25
        $clone->key = $clone->getPublicKey();
194 25
        return $clone;
195
    }
196
197
    /**
198
     * @param ScriptDataFactory $factory
199
     * @return HierarchicalKey
200
     */
201 1
    public function withScriptFactory(ScriptDataFactory $factory)
202
    {
203 1
        $clone = clone $this;
204 1
        $clone->scriptDataFactory = $factory;
205 1
        return $clone;
206
    }
207
208
    /**
209
     * @return ScriptDataFactory
210
     */
211 28
    public function getScriptDataFactory()
212
    {
213 28
        return $this->scriptDataFactory;
214
    }
215
216
    /**
217
     * @return \BitWasp\Bitcoin\Key\KeyToScript\ScriptAndSignData
218
     */
219 17
    public function getScriptAndSignData()
220
    {
221 17
        if (null === $this->scriptAndSignData) {
222 17
            $this->scriptAndSignData = $this->scriptDataFactory->convertKey($this->key);
223
        }
224
225 17
        return $this->scriptAndSignData;
226
    }
227
228
    /**
229
     * @param BaseAddressCreator $addressCreator
230
     * @return \BitWasp\Bitcoin\Address\Address
231
     */
232 17
    public function getAddress(BaseAddressCreator $addressCreator)
233
    {
234 17
        $scriptAndSignData = $this->getScriptAndSignData();
235 17
        return $addressCreator->fromOutputScript($scriptAndSignData->getScriptPubKey());
236
    }
237
238
    /**
239
     * Return whether this is a private key
240
     *
241
     * @return bool
242
     */
243 46
    public function isPrivate()
244
    {
245 46
        return $this->key->isPrivate();
246
    }
247
248
    /**
249
     * Return whether the key is hardened
250
     *
251
     * @return bool
252
     */
253 2
    public function isHardened()
254
    {
255 2
        return ($this->sequence >> 31) === 1;
256
    }
257
258
    /**
259
     * Create a buffer containing data to be hashed hashed to yield the child offset
260
     *
261
     * @param int $sequence
262
     * @return BufferInterface
263
     * @throws \Exception
264
     */
265 37
    public function getHmacSeed($sequence)
266
    {
267 37
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
268 6
            throw new \InvalidArgumentException("Sequence is outside valid range, must be >= 0 && <= (2^31)-1");
269
        }
270
271 31
        if (($sequence >> 31) === 1) {
272 28
            if ($this->isPrivate() === false) {
273 2
                throw new \Exception("Can't derive a hardened key without the private key");
274
            }
275
276 26
            $buffer = Buffertools::concat(new Buffer("\x00"), $this->getPrivateKey()->getBuffer());
277
        } else {
278 29
            $buffer = $this->getPublicKey()->getBuffer();
279
        }
280
281 29
        return (new Parser($buffer))
282 29
            ->writeBytes(4, Buffer::int($sequence, 4))
283 29
            ->getBuffer();
284
    }
285
286
    /**
287
     * @param $nextDepth
288
     * @param $sequence
289
     * @param BufferInterface $chainCode
290
     * @param KeyInterface $key
291
     * @return HierarchicalKey
292
     * @throws \Exception
293
     */
294 29
    protected function childKey($nextDepth, $sequence, BufferInterface $chainCode, KeyInterface $key)
295
    {
296 29
        return new HierarchicalKey(
297 29
            $this->ecAdapter,
298 29
            $this->scriptDataFactory,
299 29
            $nextDepth,
300 29
            $this->getChildFingerprint(),
301 29
            $sequence,
302 29
            $chainCode,
303 29
            $key
304
        );
305
    }
306
307
    /**
308
     * Derive a child key
309
     *
310
     * @param int $sequence
311
     * @return HierarchicalKey
312
     * @throws \Exception
313
     */
314 34
    public function deriveChild($sequence)
315
    {
316 34
        $nextDepth = $this->depth + 1;
317 34
        if ($nextDepth > 255) {
318
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, cannot exceed 255');
319
        }
320
321 34
        $hash = Hash::hmac('sha512', $this->getHmacSeed($sequence), $this->chainCode);
322 29
        $offset = $hash->slice(0, 32);
323 29
        $chain = $hash->slice(32);
324
325 29
        if (false === $this->ecAdapter->validatePrivateKey($offset)) {
326 1
            return $this->deriveChild($sequence + 1);
327
        }
328
329 29
        $key = $this->isPrivate() ? $this->getPrivateKey() : $this->getPublicKey();
330 29
        $key = $key->tweakAdd($offset->getGmp());
331
332 29
        return $this->childKey($nextDepth, $sequence, $chain, $key);
333
    }
334
335
    /**
336
     * @param array|\stdClass|\Traversable $list
337
     * @return HierarchicalKey
338
     */
339 24
    public function deriveFromList($list)
340
    {
341 24
        if (!is_array($list) && !$list instanceof \Traversable && !$list instanceof \stdClass) {
342
            throw new \InvalidArgumentException('List must be an array or \Traversable');
343
        }
344
345 24
        $key = $this;
346 24
        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...
347 24
            $key = $key->deriveChild($sequence);
348
        }
349
350 24
        return $key;
351
    }
352
353
    /**
354
     * Decodes a BIP32 path into actual 32bit sequence numbers and derives the child key
355
     *
356
     * @param string $path
357
     * @return HierarchicalKey
358
     * @throws \Exception
359
     */
360 24
    public function derivePath($path)
361
    {
362 24
        $sequences = new HierarchicalKeySequence();
363 24
        return $this->deriveFromList($sequences->decodePath($path));
364
    }
365
366
    /**
367
     * Serializes the instance according to whether it wraps a private or public key.
368
     * @param NetworkInterface $network
369
     * @return string
370
     */
371 16
    public function toExtendedKey(NetworkInterface $network = null)
372
    {
373 16
        $network = $network ?: Bitcoin::getNetwork();
374
375 16
        $extendedSerializer = new Base58ExtendedKeySerializer(new ExtendedKeySerializer($this->ecAdapter));
376 16
        $extended = $extendedSerializer->serialize($network, $this);
377 16
        return $extended;
378
    }
379
380
    /**
381
     * Explicitly serialize as a private key. Throws an exception if
382
     * the key isn't private.
383
     *
384
     * @param NetworkInterface $network
385
     * @return string
386
     */
387 16
    public function toExtendedPrivateKey(NetworkInterface $network = null)
388
    {
389 16
        if (!$this->isPrivate()) {
390 2
            throw new \LogicException('Cannot create extended private key from public');
391
        }
392
393 14
        return $this->toExtendedKey($network);
394
    }
395
396
    /**
397
     * Explicitly serialize as a public key. This will always work.
398
     *
399
     * @param NetworkInterface $network
400
     * @return string
401
     */
402 13
    public function toExtendedPublicKey(NetworkInterface $network = null)
403
    {
404 13
        return $this->withoutPrivateKey()->toExtendedKey($network);
405
    }
406
}
407