Completed
Push — 0.0.34 ( 61212b...2e9020 )
by thomas
20:55
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 64
    public function __construct(EcAdapterInterface $ecAdapter, $depth, $parentFingerprint, $sequence, BufferInterface $chainCode, KeyInterface $key)
62
    {
63 64
        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 64
        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 64
        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 64
        if ($chainCode->getSize() !== 32) {
76
            throw new \RuntimeException('Chaincode should be 32 bytes');
77
        }
78
79 64
        if (!$key->isCompressed()) {
80 2
            throw new \InvalidArgumentException('A HierarchicalKey must always be compressed');
81
        }
82
83 62
        $this->ecAdapter = $ecAdapter;
84 62
        $this->depth = $depth;
85 62
        $this->sequence = $sequence;
86 62
        $this->parentFingerprint = $parentFingerprint;
87 62
        $this->chainCode = $chainCode;
88 62
        $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 62
    }
90
91
    /**
92
     * Return the depth of this key. This is limited to 256 sequential derivations.
93
     *
94
     * @return int
95
     */
96 18
    public function getDepth()
97
    {
98 18
        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 14
    public function getSequence()
109
    {
110 14
        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 14
    public function getFingerprint()
119
    {
120 14
        if ($this->getDepth() === 0) {
121 8
            return 0;
122
        }
123
124 14
        return $this->parentFingerprint;
125
    }
126
127
    /**
128
     * Return the fingerprint to be used for child keys.
129
     * @return int
130
     */
131 16
    public function getChildFingerprint()
132
    {
133 16
        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 12
    public function getChainCode()
143
    {
144 12
        return $this->chainCode;
145
    }
146
147
    /**
148
     * @return PrivateKeyInterface
149
     */
150 28
    public function getPrivateKey()
151
    {
152 28
        if ($this->key->isPrivate()) {
153 26
            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 30
    public function getPublicKey()
165
    {
166 30
        if ($this->isPrivate()) {
167 20
            return $this->getPrivateKey()->getPublicKey();
168
        } else {
169 16
            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 10
    public function toPublic()
177
    {
178 10
        if ($this->isPrivate()) {
179 8
            $this->key = $this->getPrivateKey()->getPublicKey();
180
        }
181
182 10
        return $this;
183
    }
184
185
    /**
186
     * Return whether this is a private key
187
     *
188
     * @return bool
189
     */
190 34
    public function isPrivate()
191
    {
192 34
        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 28
    public function getHmacSeed($sequence)
213
    {
214 28
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
215 12
            throw new \InvalidArgumentException("Sequence is outside valid range, must be >= 0 && <= (2^31)-1");
216
        }
217
218 16
        if (($sequence >> 31) === 1) {
219 12
            if ($this->isPrivate() === false) {
220 2
                throw new \Exception("Can't derive a hardened key without the private key");
221
            }
222
223 10
            $buffer = Buffertools::concat(new Buffer("\x00"), $this->getPrivateKey()->getBuffer());
224
        } else {
225 14
            $buffer = $this->getPublicKey()->getBuffer();
226
        }
227
228 14
        return (new Parser($buffer))
229 14
            ->writeBytes(4, Buffer::int($sequence, 4))
230 14
            ->getBuffer();
231
    }
232
233
    /**
234
     * Derive a child key
235
     *
236
     * @param int $sequence
237
     * @return HierarchicalKey
238
     * @throws \Exception
239
     */
240 22
    public function deriveChild($sequence)
241
    {
242 22
        $nextDepth = $this->depth + 1;
243 22
        if ($nextDepth > 255) {
244
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, cannot exceed 255');
245
        }
246
247 22
        $hash = Hash::hmac('sha512', $this->getHmacSeed($sequence), $this->chainCode);
248 14
        $offset = $hash->slice(0, 32);
249 14
        $chain = $hash->slice(32);
250
251 14
        if (false === $this->ecAdapter->validatePrivateKey($offset)) {
252 2
            return $this->deriveChild($sequence + 1);
253
        }
254
255 14
        $key = $this->isPrivate() ? $this->getPrivateKey() : $this->getPublicKey();
256 14
        $key = $key->tweakAdd($offset->getGmp());
257
258 14
        return new HierarchicalKey(
259 14
            $this->ecAdapter,
260 14
            $nextDepth,
261 14
            $this->getChildFingerprint(),
262 14
            $sequence,
263 14
            $chain,
264 14
            $key
265
        );
266
    }
267
268
    /**
269
     * @param array|\stdClass|\Traversable $list
270
     * @return HierarchicalKey
271
     */
272 8
    public function deriveFromList($list)
273
    {
274 8
        if (!is_array($list) && !$list instanceof \Traversable && !$list instanceof \stdClass) {
275
            throw new \InvalidArgumentException('List must be an array or \Traversable');
276
        }
277
278 8
        $key = $this;
279 8
        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 8
            $key = $key->deriveChild($sequence);
281
        }
282
283 8
        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 8
    public function derivePath($path)
294
    {
295 8
        $sequences = new HierarchicalKeySequence();
296 8
        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 12
    public function toExtendedKey(NetworkInterface $network = null)
306
    {
307 12
        $network = $network ?: Bitcoin::getNetwork();
308
309 12
        $extendedSerializer = new Base58ExtendedKeySerializer(new ExtendedKeySerializer($this->ecAdapter));
310 12
        $extended = $extendedSerializer->serialize($network, $this);
311 12
        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 12
    public function toExtendedPrivateKey(NetworkInterface $network = null)
322
    {
323 12
        if (!$this->isPrivate()) {
324 2
            throw new \LogicException('Cannot create extended private key from public');
325
        }
326
327 10
        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 10
    public function toExtendedPublicKey(NetworkInterface $network = null)
337
    {
338 10
        $clone = clone($this);
339 10
        return $clone->toPublic()->toExtendedKey($network);
340
    }
341
}
342