Completed
Pull Request — master (#591)
by thomas
15:19
created

Bip39Mnemonic::entropyToWords()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5.0061

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 5
nop 1
dl 0
loc 27
ccs 15
cts 16
cp 0.9375
crap 5.0061
rs 8.439
c 0
b 0
f 0
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
    /**
27
     * @param EcAdapterInterface $ecAdapter
28
     * @param Bip39WordListInterface $wordList
29
     */
30 6
    public function __construct(EcAdapterInterface $ecAdapter, Bip39WordListInterface $wordList)
31
    {
32 6
        $this->ecAdapter = $ecAdapter;
33 6
        $this->wordList = $wordList;
34 6
    }
35
36
    /**
37
     * Creates a new Bip39 mnemonic string.
38
     *
39
     * @param int $entropySize
40
     * @return string
41
     * @throws \BitWasp\Bitcoin\Exceptions\RandomBytesFailure
42
     */
43
    public function create($entropySize = 512): string
44
    {
45
        $random = new Random();
46
        $entropy = $random->bytes($entropySize / 8);
47
48
        return $this->entropyToMnemonic($entropy);
49
    }
50
51
    /**
52
     * @param BufferInterface $entropy
53
     * @param integer $CSlen
54
     * @return string
55
     */
56 49
    private function calculateChecksum(BufferInterface $entropy, int $CSlen): string
57
    {
58 49
        $entHash = Hash::sha256($entropy);
59
60
        // Convert byte string to padded binary string of 0/1's.
61 49
        $hashBits = str_pad(gmp_strval($entHash->getGmp(), 2), 256, '0', STR_PAD_LEFT);
62
63
        // Take $CSlen bits for the checksum
64 49
        $checksumBits = substr($hashBits, 0, $CSlen);
65
66 49
        return $checksumBits;
67
    }
68
69
    /**
70
     * @param BufferInterface $entropy
71
     * @return string[]
72
     */
73 26
    public function entropyToWords(BufferInterface $entropy): array
74
    {
75 26
        if ($entropy->getSize() === 0) {
76
            throw new \InvalidArgumentException('Invalid entropy, empty');
77
        }
78 26
        if ($entropy->getSize() > 1024) {
79 1
            throw new \InvalidArgumentException('Invalid entropy, max 1024 bytes');
80
        }
81 25
        if ($entropy->getSize() % 4 !== 0) {
82 1
            throw new \InvalidArgumentException('Invalid entropy, must be multitude of 4 bytes');
83
        }
84
85 24
        $math = $this->ecAdapter->getMath();
86
87 24
        $ENT = $entropy->getSize() * 8;
88 24
        $CS = $ENT / 32;
89
90 24
        $bits = gmp_strval($entropy->getGmp(), 2) . $this->calculateChecksum($entropy, $CS);
91 24
        $bits = str_pad($bits, ($ENT + $CS), '0', STR_PAD_LEFT);
92
93 24
        $result = [];
94 24
        foreach (str_split($bits, 11) as $bit) {
95 24
            $result[] = $this->wordList->getWord((int) $math->baseConvert($bit, 2, 10));
96
        }
97
98 24
        return $result;
99
    }
100
101
    /**
102
     * @param BufferInterface $entropy
103
     * @return string
104
     */
105 26
    public function entropyToMnemonic(BufferInterface $entropy): string
106
    {
107 26
        return implode(' ', $this->entropyToWords($entropy));
108
    }
109
110
    /**
111
     * @param string $mnemonic
112
     * @return BufferInterface
113
     */
114 27
    public function mnemonicToEntropy(string $mnemonic): BufferInterface
115
    {
116 27
        $math = $this->ecAdapter->getMath();
117 27
        $words = explode(' ', $mnemonic);
118
119 27
        if (count($words) % 3 !== 0) {
120 1
            throw new \InvalidArgumentException('Invalid mnemonic');
121
        }
122
123 26
        $bits = array();
124 26
        foreach ($words as $word) {
125 26
            $idx = $this->wordList->getIndex($word);
126 26
            $bits[] = str_pad($math->baseConvert($idx, 10, 2), 11, '0', STR_PAD_LEFT);
127
        }
128
129 26
        $bits = implode('', $bits);
130
131
        // max entropy is 1024; (1024×8)+((1024×8)÷32) = 8448
132 26
        if (strlen($bits) > 8448) {
133 1
            throw new \InvalidArgumentException('Invalid mnemonic, too long');
134
        }
135
136 25
        $CS = strlen($bits) / 33;
137 25
        $ENT = strlen($bits) - $CS;
138
139 25
        $csBits = substr($bits, -1 * $CS);
140 25
        $entBits = substr($bits, 0, -1 * $CS);
141
142 25
        $binary = '';
143 25
        $bitsInChar = 8;
144 25
        for ($i = 0; $i < $ENT; $i += $bitsInChar) {
145
            // Extract 8 bits at a time, convert to hex, pad, and convert to binary.
146 25
            $eBits = substr($entBits, $i, $bitsInChar);
147 25
            $binary .= pack("H*", (str_pad($math->baseConvert($eBits, 2, 16), 2, '0', STR_PAD_LEFT)));
148
        }
149
150 25
        $entropy = new Buffer($binary, null, $math);
151 25
        if ($csBits !== $this->calculateChecksum($entropy, $CS)) {
152 1
            throw new \InvalidArgumentException('Checksum does not match');
153
        }
154
155 24
        return $entropy;
156
    }
157
}
158