Completed
Pull Request — master (#321)
by thomas
16:12 queued 06:51
created

HierarchicalKey::deriveFromList()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.0488

Importance

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