Completed
Pull Request — master (#591)
by thomas
30:19 queued 12s
created

HierarchicalKey::getFingerprint()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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