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