Completed
Pull Request — master (#446)
by thomas
105:00 queued 101:56
created

Bip39Mnemonic::entropyToWords()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5.005

Importance

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