Completed
Pull Request — master (#431)
by thomas
145:42 queued 75:41
created

HierarchicalKey::__construct()   D

Complexity

Conditions 9
Paths 6

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 9.0368

Importance

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