Completed
Pull Request — master (#743)
by thomas
24:24
created

Bip39Mnemonic   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 147
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 84.91%

Importance

Changes 0
Metric Value
dl 0
loc 147
ccs 45
cts 53
cp 0.8491
rs 10
c 0
b 0
f 0
wmc 16
lcom 1
cbo 5

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A create() 0 15 3
A calculateChecksum() 0 11 2
A entropyToWords() 0 18 3
A entropyToMnemonic() 0 4 1
B mnemonicToEntropy() 0 42 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Mnemonic\Bip39;
6
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
8
use BitWasp\Bitcoin\Crypto\Hash;
9
use BitWasp\Bitcoin\Crypto\Random\Random;
10
use BitWasp\Bitcoin\Mnemonic\MnemonicInterface;
11
use BitWasp\Buffertools\Buffer;
12
use BitWasp\Buffertools\BufferInterface;
13
14
class Bip39Mnemonic implements MnemonicInterface
15
{
16
    /**
17
     * @var EcAdapterInterface
18
     */
19
    private $ecAdapter;
20
21
    /**
22
     * @var Bip39WordListInterface
23
     */
24
    private $wordList;
25
26
    const DEFAULT_ENTROPY_LEN = 256;
27
28
    private $validEntropySizes = [
29
        128, 160, 192, 224, 256,
30
    ];
31
32
    /**
33
     * @param EcAdapterInterface $ecAdapter
34
     * @param Bip39WordListInterface $wordList
35
     */
36 6
    public function __construct(EcAdapterInterface $ecAdapter, Bip39WordListInterface $wordList)
37
    {
38 6
        $this->ecAdapter = $ecAdapter;
39 6
        $this->wordList = $wordList;
40 6
    }
41
42
    /**
43
     * Creates a new Bip39 mnemonic string.
44
     *
45
     * @param int $entropySize
46
     * @return string
47
     * @throws \BitWasp\Bitcoin\Exceptions\RandomBytesFailure
48
     */
49
    public function create(int $entropySize = null): string
50
    {
51
        if (null === $entropySize) {
52
            $entropySize = self::DEFAULT_ENTROPY_LEN;
53
        }
54
55
        if (!in_array($entropySize, $this->validEntropySizes)) {
56
            throw new \InvalidArgumentException("Invalid entropy length");
57
        }
58
59
        $random = new Random();
60
        $entropy = $random->bytes($entropySize / 8);
61
62
        return $this->entropyToMnemonic($entropy);
63
    }
64
65
    /**
66
     * @param BufferInterface $entropy
67
     * @param integer $CSlen
68
     * @return string
69
     */
70 49
    private function calculateChecksum(BufferInterface $entropy, int $CSlen): string
71
    {
72 49
        $entHash = Hash::sha256($entropy);
73 49
        $checksumChar = ord($entHash->getBinary()[0]);
74 49
        $cs = '';
75 49
        for ($i = 0; $i < $CSlen; $i++) {
76 49
            $cs .= $checksumChar >> (7 - $i) & 1;
77
        }
78
79 49
        return $cs;
80
    }
81
82
    /**
83
     * @param BufferInterface $entropy
84
     * @return string[] - array of words from the word list
85
     */
86 26
    public function entropyToWords(BufferInterface $entropy): array
87
    {
88 26
        $ENT = $entropy->getSize() * 8;
89 26
        if (!in_array($entropy->getSize() * 8, $this->validEntropySizes)) {
90 2
            throw new \InvalidArgumentException("Invalid entropy length");
91
        }
92
93 24
        $CS = $ENT / 32;
94 24
        $bits = gmp_strval($entropy->getGmp(), 2) . $this->calculateChecksum($entropy, $CS);
95 24
        $bits = str_pad($bits, ($ENT + $CS), '0', STR_PAD_LEFT);
96
97 24
        $result = [];
98 24
        foreach (str_split($bits, 11) as $bit) {
99 24
            $result[] = $this->wordList->getWord((int) bindec($bit));
100
        }
101
102 24
        return $result;
103
    }
104
105
    /**
106
     * @param BufferInterface $entropy
107
     * @return string
108
     */
109 26
    public function entropyToMnemonic(BufferInterface $entropy): string
110
    {
111 26
        return implode(' ', $this->entropyToWords($entropy));
112
    }
113
114
    /**
115
     * @param string $mnemonic
116
     * @return BufferInterface
117
     */
118 27
    public function mnemonicToEntropy(string $mnemonic): BufferInterface
119
    {
120 27
        $words = explode(' ', $mnemonic);
121
122
        // Mnemonic sizes are multiples of 3 words
123 27
        if (count($words) % 3 !== 0) {
124 1
            throw new \InvalidArgumentException('Invalid mnemonic');
125
        }
126
127
        // Build up $bits from the list of words
128 26
        $bits = '';
129 26
        foreach ($words as $word) {
130 26
            $idx = $this->wordList->getIndex($word);
131
            // Mnemonic bit sizes are multiples of 33 bits
132 26
            $bits .= str_pad(decbin($idx), 11, '0', STR_PAD_LEFT);
133
        }
134
135
        // Every 32 bits of ENT adds a 1 CS bit.
136 26
        $CS = strlen($bits) / 33;
137 26
        $ENT = strlen($bits) - $CS;
138 26
        if (!in_array($ENT, $this->validEntropySizes)) {
139 1
            throw new \InvalidArgumentException('Invalid mnemonic - entropy size is invalid');
140
        }
141
142
        // Checksum bits
143 25
        $csBits = substr($bits, $ENT, $CS);
144
145
        // Split $ENT bits into 8 bit words to be packed
146 25
        $entArray = str_split(substr($bits, 0, $ENT), 8);
147 25
        $chars = [];
148 25
        for ($i = 0; $i < $ENT / 8; $i++) {
149 25
            $chars[] = (int) bindec($entArray[$i]);
150
        }
151
152
        // Check checksum
153 25
        $entropy = new Buffer(pack("C*", ...$chars));
154 25
        if (hash_equals($csBits, $this->calculateChecksum($entropy, $CS))) {
155 24
            return $entropy;
156
        } else {
157 1
            throw new \InvalidArgumentException('Checksum does not match');
158
        }
159
    }
160
}
161