Completed
Push — 0.0.35 ( b2fc74...d8d049 )
by thomas
28:12 queued 09:10
created

HierarchicalKey::deriveChild()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4.0027

Importance

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