Bit-Wasp /
bitcoin-php
| 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
Loading history...
|
|||
| 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 |