Completed
Push — master ( 4c26fd...5c635d )
by thomas
106:46 queued 98:38
created

Bip39Mnemonic   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 144
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 90.38%

Importance

Changes 0
Metric Value
dl 0
loc 144
ccs 47
cts 52
cp 0.9038
rs 10
c 0
b 0
f 0
wmc 15
lcom 1
cbo 5

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A create() 0 7 1
A calculateChecksum() 0 12 1
B entropyToWords() 0 25 5
A entropyToMnemonic() 0 4 1
B mnemonicToEntropy() 0 45 6
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[] - array of words from the word list
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) bindec($bit));
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
        // Mnemonic sizes are multiples of 3 words
117 27
        if (count($words) % 3 !== 0) {
118 1
            throw new \InvalidArgumentException('Invalid mnemonic');
119
        }
120
121
        // Build up $bits from the list of words
122 26
        $bits = '';
123 26
        foreach ($words as $word) {
124 26
            $idx = $this->wordList->getIndex($word);
125
            // Mnemonic bit sizes are multiples of 33 bits
126 26
            $bits .= str_pad(decbin($idx), 11, '0', STR_PAD_LEFT);
127
        }
128
129
        // Max entropy is 1024bytes; (1024×8)+((1024×8)÷32) = 8448 bits
130 26
        if (strlen($bits) > 8448) {
131 1
            throw new \InvalidArgumentException('Invalid mnemonic, too long');
132
        }
133
134
        // Every 32 bits of ENT adds a 1 CS bit.
135 25
        $CS = strlen($bits) / 33;
136 25
        $ENT = strlen($bits) - $CS;
137
138
        // Checksum bits
139 25
        $csBits = substr($bits, $ENT, $CS);
140
141
        // Split $ENT bits into 8 bit words to be packed
142 25
        assert($ENT % 8 === 0);
143 25
        $entArray = str_split(substr($bits, 0, $ENT), 8);
144 25
        $chars = [];
145 25
        for ($i = 0; $i < $ENT / 8; $i++) {
146 25
            $chars[] = (int) bindec($entArray[$i]);
147
        }
148
149
        // Check checksum
150 25
        $entropy = new Buffer(pack("C*", ...$chars));
151 25
        if (hash_equals($csBits, $this->calculateChecksum($entropy, $CS))) {
152 24
            return $entropy;
153
        } else {
154 1
            throw new \InvalidArgumentException('Checksum does not match');
155
        }
156
    }
157
}
158