Completed
Pull Request — master (#591)
by thomas
22:54 queued 09:12
created

Bip39Mnemonic::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 9.4285
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
        $ENT = $entropy->getSize() * 8;
86 24
        $CS = $ENT / 32;
87
88 24
        $bits = gmp_strval($entropy->getGmp(), 2) . $this->calculateChecksum($entropy, $CS);
89 24
        $bits = str_pad($bits, ($ENT + $CS), '0', STR_PAD_LEFT);
90
91 24
        $result = [];
92 24
        foreach (str_split($bits, 11) as $bit) {
93 24
            $result[] = $this->wordList->getWord((int) gmp_strval(gmp_init($bit, 2), 10));
94
        }
95
96 24
        return $result;
97
    }
98
99
    /**
100
     * @param BufferInterface $entropy
101
     * @return string
102
     */
103 26
    public function entropyToMnemonic(BufferInterface $entropy): string
104
    {
105 26
        return implode(' ', $this->entropyToWords($entropy));
106
    }
107
108
    /**
109
     * @param string $mnemonic
110
     * @return BufferInterface
111
     */
112 27
    public function mnemonicToEntropy(string $mnemonic): BufferInterface
113
    {
114 27
        $words = explode(' ', $mnemonic);
115
116 27
        if (count($words) % 3 !== 0) {
117 1
            throw new \InvalidArgumentException('Invalid mnemonic');
118
        }
119
120 26
        $bits = array();
121 26
        foreach ($words as $word) {
122 26
            $idx = $this->wordList->getIndex($word);
123 26
            $bits[] = str_pad(gmp_strval(gmp_init($idx, 10), 2), 11, '0', STR_PAD_LEFT);
124
        }
125
126 26
        $bits = implode('', $bits);
127
128
        // max entropy is 1024; (1024×8)+((1024×8)÷32) = 8448
129 26
        if (strlen($bits) > 8448) {
130 1
            throw new \InvalidArgumentException('Invalid mnemonic, too long');
131
        }
132
133 25
        $CS = strlen($bits) / 33;
134 25
        $ENT = strlen($bits) - $CS;
135
136 25
        $csBits = substr($bits, -1 * $CS);
137 25
        $entBits = substr($bits, 0, -1 * $CS);
138
139 25
        $binary = '';
140 25
        $bitsInChar = 8;
141 25
        for ($i = 0; $i < $ENT; $i += $bitsInChar) {
142
            // Extract 8 bits at a time, convert to hex, pad, and convert to binary.
143 25
            $eBits = substr($entBits, $i, $bitsInChar);
144 25
            $binary .= pack("H*", (str_pad(gmp_strval(gmp_init($eBits, 2), 16), 2, '0', STR_PAD_LEFT)));
145
        }
146
147 25
        $entropy = new Buffer($binary);
148 25
        if ($csBits !== $this->calculateChecksum($entropy, $CS)) {
149 1
            throw new \InvalidArgumentException('Checksum does not match');
150
        }
151
152 24
        return $entropy;
153
    }
154
}
155