Completed
Pull Request — master (#321)
by thomas
17:40
created

HierarchicalKey   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 13

Test Coverage

Coverage 97.83%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 302
ccs 90
cts 92
cp 0.9783
rs 9.2
wmc 34
lcom 2
cbo 13

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 3
A getDepth() 0 4 1
A getSequence() 0 4 1
A getFingerprint() 0 8 2
A getChildFingerprint() 0 4 1
A getChainCode() 0 4 1
A getPrivateKey() 0 8 2
A getPublicKey() 0 8 2
A toPublic() 0 8 2
A isPrivate() 0 4 1
A isHardened() 0 4 1
A getHmacSeed() 0 17 3
A deriveChild() 0 22 3
B deriveFromList() 0 13 5
A derivePath() 0 5 1
A toExtendedKey() 0 8 2
A toExtendedPrivateKey() 0 8 2
A toExtendedPublicKey() 0 5 1
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 27
    public function getChainCode()
131
    {
132 27
        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
        $hardened = ($sequence >> 31) === 1;
203 21
        if ($hardened) {
204 15
            if ($this->isPrivate() === false) {
205 3
                throw new \Exception("Can't derive a hardened key without the private key");
206
            }
207
208 12
            $buffer = Buffertools::concat(new Buffer("\x00"), $this->getPrivateKey()->getBuffer());
209 8
        } else {
210 18
            $buffer = $this->getPublicKey()->getBuffer();
211
        }
212
213 18
        return (new Parser($buffer))
214 18
            ->writeBytes(4, Buffer::int($sequence, 4))
215 18
            ->getBuffer();
216
    }
217
218
    /**
219
     * Derive a child key
220
     *
221
     * @param int $sequence
222
     * @return HierarchicalKey
223
     * @throws \Exception
224
     */
225 21
    public function deriveChild($sequence)
226
    {
227 21
        $chain = $this->getChainCode();
228
229 21
        $hash = Hash::hmac('sha512', $this->getHmacSeed($sequence), $chain);
230 18
        $offset = $hash->slice(0, 32);
231 18
        $chain = $hash->slice(32);
232 18
        $key = $this->isPrivate() ? $this->getPrivateKey() : $this->getPublicKey();
233
234 18
        if (false === $this->ecAdapter->validatePrivateKey($offset)) {
235 3
            return $this->deriveChild($sequence + 1);
236
        }
237
238 18
        return new HierarchicalKey(
239 18
            $this->ecAdapter,
240 18
            $this->getDepth() + 1,
241 18
            $this->getChildFingerprint(),
242 12
            $sequence,
243 12
            $chain,
244 18
            $key->tweakAdd(gmp_init($offset->getInt(), 10))
245 12
        );
246
    }
247
248
    /**
249
     * @param array|\stdClass|\Traversable $list
250
     * @return HierarchicalKey
251
     */
252 9
    public function deriveFromList($list)
253
    {
254 9
        if (!is_array($list) && !$list instanceof \Traversable && !$list instanceof \stdClass) {
255
            throw new \InvalidArgumentException('List must be an array or \Traversable');
256
        }
257
258 9
        $key = $this;
259 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...
260 9
            $key = $key->deriveChild($sequence);
261 6
        }
262
263 9
        return $key;
264
    }
265
266
    /**
267
     * Decodes a BIP32 path into actual 32bit sequence numbers and derives the child key
268
     *
269
     * @param string $path
270
     * @return HierarchicalKey
271
     * @throws \Exception
272
     */
273 9
    public function derivePath($path)
274
    {
275 9
        $sequences = new HierarchicalKeySequence();
276 9
        return $this->deriveFromList($sequences->decodePath($path));
277
    }
278
279
    /**
280
     * Serializes the instance according to whether it wraps a private or public key.
281
     * @param NetworkInterface $network
282
     * @return string
283
     */
284 15
    public function toExtendedKey(NetworkInterface $network = null)
285
    {
286 15
        $network = $network ?: Bitcoin::getNetwork();
287
288 15
        $extendedSerializer = new Base58ExtendedKeySerializer(new ExtendedKeySerializer($this->ecAdapter, $network));
289 15
        $extended = $extendedSerializer->serialize($this);
290 15
        return $extended;
291
    }
292
293
    /**
294
     * Explicitly serialize as a private key. Throws an exception if
295
     * the key isn't private.
296
     *
297
     * @param NetworkInterface $network
298
     * @return string
299
     */
300 15
    public function toExtendedPrivateKey(NetworkInterface $network = null)
301
    {
302 15
        if (!$this->isPrivate()) {
303 3
            throw new \LogicException('Cannot create extended private key from public');
304
        }
305
306 12
        return $this->toExtendedKey($network);
307
    }
308
309
    /**
310
     * Explicitly serialize as a public key. This will always work.
311
     *
312
     * @param NetworkInterface $network
313
     * @return string
314
     */
315 12
    public function toExtendedPublicKey(NetworkInterface $network = null)
316
    {
317 12
        $clone = clone($this);
318 12
        return $clone->toPublic()->toExtendedKey($network);
319
    }
320
}
321