Completed
Pull Request — master (#600)
by thomas
56:08 queued 50:25
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 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 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\Bitcoin;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
11
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
12
use BitWasp\Bitcoin\Crypto\Hash;
13
use BitWasp\Bitcoin\Network\NetworkInterface;
14
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\Base58ExtendedKeySerializer;
15
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\ExtendedKeySerializer;
16
use BitWasp\Bitcoin\Util\IntRange;
17
use BitWasp\Buffertools\Buffer;
18
use BitWasp\Buffertools\BufferInterface;
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, int $depth, int $parentFingerprint, int $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(): int
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(): int
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 int
117
     */
118 13
    public function getFingerprint(): int
119
    {
120 13
        if ($this->getDepth() === 0) {
121 11
            return 0;
122
        }
123
124 9
        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(): int
132
    {
133 14
        $pubKeyHash = $this->getPublicKey()->getPubKeyHash();
134 14
        return (int) $pubKeyHash->slice(0, 4)->getInt();
135
    }
136
137
    /**
138
     * Return the chain code - a deterministic 'salt' for HMAC-SHA512
139
     * in child derivations
140
     *
141
     * @return BufferInterface
142
     */
143 11
    public function getChainCode(): BufferInterface
144
    {
145 11
        return $this->chainCode;
146
    }
147
148
    /**
149
     * @return PrivateKeyInterface
150
     */
151 23
    public function getPrivateKey(): PrivateKeyInterface
152
    {
153 23
        if ($this->key->isPrivate()) {
154 21
            return $this->key;
155
        }
156
157 2
        throw new \RuntimeException('Unable to get private key, not known');
158
    }
159
160
    /**
161
     * Get the public key the private key or public key.
162
     *
163
     * @return PublicKeyInterface
164
     */
165 25
    public function getPublicKey(): PublicKeyInterface
166
    {
167 25
        if ($this->isPrivate()) {
168 17
            return $this->getPrivateKey()->getPublicKey();
169
        } else {
170 15
            return $this->key;
171
        }
172
    }
173
174
    /**
175
     * @return HierarchicalKey
176
     */
177 9
    public function withoutPrivateKey(): HierarchicalKey
178
    {
179 9
        $clone = clone $this;
180 9
        $clone->key = $clone->getPublicKey();
181 9
        return $clone;
182
    }
183
184
    /**
185
     * Return whether this is a private key
186
     *
187
     * @return bool
188
     */
189 29
    public function isPrivate(): bool
190
    {
191 29
        return $this->key->isPrivate();
192
    }
193
194
    /**
195
     * Return whether the key is hardened
196
     *
197
     * @return bool
198
     */
199 2
    public function isHardened(): bool
200
    {
201 2
        return ($this->sequence >> 31) === 1;
202
    }
203
204
    /**
205
     * Create a buffer containing data to be hashed hashed to yield the child offset
206
     *
207
     * @param int $sequence
208
     * @return BufferInterface
209
     * @throws \Exception
210
     */
211 20
    public function getHmacSeed(int $sequence): BufferInterface
212
    {
213 20
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
214 6
            throw new \InvalidArgumentException("Sequence is outside valid range, must be >= 0 && <= (2^31)-1");
215
        }
216
217 14
        if (($sequence >> 31) === 1) {
218 11
            if ($this->isPrivate() === false) {
219 2
                throw new \Exception("Can't derive a hardened key without the private key");
220
            }
221
222 9
            $data = "\x00{$this->getPrivateKey()->getBinary()}";
223
        } else {
224 12
            $data = $this->getPublicKey()->getBinary();
225
        }
226
227 12
        return new Buffer($data . pack("N", $sequence));
228
    }
229
230
    /**
231
     * Derive a child key
232
     *
233
     * @param int $sequence
234
     * @return HierarchicalKey
235
     * @throws \Exception
236
     */
237 17
    public function deriveChild(int $sequence): HierarchicalKey
238
    {
239 17
        $nextDepth = $this->depth + 1;
240 17
        if ($nextDepth > 255) {
241
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, cannot exceed 255');
242
        }
243
244 17
        $hash = Hash::hmac('sha512', $this->getHmacSeed($sequence), $this->chainCode);
245 12
        $offset = $hash->slice(0, 32);
246 12
        $chain = $hash->slice(32);
247
248 12
        if (false === $this->ecAdapter->validatePrivateKey($offset)) {
249 1
            return $this->deriveChild($sequence + 1);
250
        }
251
252 12
        $key = $this->isPrivate() ? $this->getPrivateKey() : $this->getPublicKey();
253 12
        $key = $key->tweakAdd($offset->getGmp());
254
255 12
        return new HierarchicalKey(
256 12
            $this->ecAdapter,
257 12
            $nextDepth,
258 12
            $this->getChildFingerprint(),
259 12
            $sequence,
260 12
            $chain,
261 12
            $key
262
        );
263
    }
264
265
    /**
266
     * @param array|\stdClass|\Traversable $list
267
     * @return HierarchicalKey
268
     */
269 7
    public function deriveFromList($list): HierarchicalKey
270
    {
271 7
        if (!is_array($list) && !$list instanceof \Traversable && !$list instanceof \stdClass) {
272
            throw new \InvalidArgumentException('List must be an array or \Traversable');
273
        }
274
275 7
        $key = $this;
276 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...
277 7
            $key = $key->deriveChild((int) $sequence);
278
        }
279
280 7
        return $key;
281
    }
282
283
    /**
284
     * Decodes a BIP32 path into actual 32bit sequence numbers and derives the child key
285
     *
286
     * @param string $path
287
     * @return HierarchicalKey
288
     * @throws \Exception
289
     */
290 7
    public function derivePath(string $path): HierarchicalKey
291
    {
292 7
        $sequences = new HierarchicalKeySequence();
293 7
        return $this->deriveFromList($sequences->decodePath($path));
294
    }
295
296
    /**
297
     * Serializes the instance according to whether it wraps a private or public key.
298
     * @param NetworkInterface $network
299
     * @return string
300
     */
301 11
    public function toExtendedKey(NetworkInterface $network = null): string
302
    {
303 11
        $network = $network ?: Bitcoin::getNetwork();
304
305 11
        $extendedSerializer = new Base58ExtendedKeySerializer(new ExtendedKeySerializer($this->ecAdapter));
306 11
        $extended = $extendedSerializer->serialize($network, $this);
307 11
        return $extended;
308
    }
309
310
    /**
311
     * Explicitly serialize as a private key. Throws an exception if
312
     * the key isn't private.
313
     *
314
     * @param NetworkInterface $network
315
     * @return string
316
     */
317 11
    public function toExtendedPrivateKey(NetworkInterface $network = null): string
318
    {
319 11
        if (!$this->isPrivate()) {
320 2
            throw new \LogicException('Cannot create extended private key from public');
321
        }
322
323 9
        return $this->toExtendedKey($network);
324
    }
325
326
    /**
327
     * Explicitly serialize as a public key. This will always work.
328
     *
329
     * @param NetworkInterface $network
330
     * @return string
331
     */
332 9
    public function toExtendedPublicKey(NetworkInterface $network = null): string
333
    {
334
        return $this
335 9
            ->withoutPrivateKey()
336 9
            ->toExtendedKey($network)
337
        ;
338
    }
339
}
340