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