Completed
Pull Request — master (#371)
by Ruben de
21:14
created

Bip39Mnemonic::entropyToWords()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5.0042

Importance

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