Bip39Mnemonic::mnemonicToEntropy()   B
last analyzed

Complexity

Conditions 6
Paths 11

Size

Total Lines 40
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
c 1
b 0
f 0
nc 11
nop 1
dl 0
loc 40
ccs 21
cts 21
cp 1
crap 6
rs 8.9617
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
    const MIN_ENTROPY_BYTE_LEN = 16;
27
    const MAX_ENTROPY_BYTE_LEN = 32;
28
    const DEFAULT_ENTROPY_BYTE_LEN = self::MAX_ENTROPY_BYTE_LEN;
29
30
    private $validEntropySizes = [
31
        self::MIN_ENTROPY_BYTE_LEN * 8, 160, 192, 224, self::MAX_ENTROPY_BYTE_LEN * 8,
32
    ];
33
34
    /**
35
     * @param EcAdapterInterface $ecAdapter
36
     * @param Bip39WordListInterface $wordList
37
     */
38 6
    public function __construct(EcAdapterInterface $ecAdapter, Bip39WordListInterface $wordList)
39
    {
40 6
        $this->ecAdapter = $ecAdapter;
41 6
        $this->wordList = $wordList;
42 6
    }
43
44
    /**
45
     * Creates a new Bip39 mnemonic string.
46
     *
47
     * @param int $entropySize
48
     * @return string
49
     * @throws \BitWasp\Bitcoin\Exceptions\RandomBytesFailure
50
     */
51
    public function create(int $entropySize = null): string
52
    {
53
        if (null === $entropySize) {
54
            $entropySize = self::DEFAULT_ENTROPY_BYTE_LEN * 8;
55
        }
56
57
        if (!in_array($entropySize, $this->validEntropySizes)) {
58
            throw new \InvalidArgumentException("Invalid entropy length");
59
        }
60
61
        $random = new Random();
62
        $entropy = $random->bytes($entropySize / 8);
63
64
        return $this->entropyToMnemonic($entropy);
65
    }
66
67
    /**
68
     * @param BufferInterface $entropy
69
     * @param integer $CSlen
70
     * @return string
71
     */
72 49
    private function calculateChecksum(BufferInterface $entropy, int $CSlen): string
73
    {
74
        // entropy range (128, 256) yields (4, 8) bits of checksum
75 49
        $checksumChar = ord(Hash::sha256($entropy)->getBinary()[0]);
76 49
        $cs = '';
77 49
        for ($i = 0; $i < $CSlen; $i++) {
78 49
            $cs .= $checksumChar >> (7 - $i) & 1;
79
        }
80
81 49
        return $cs;
82
    }
83
84
    /**
85
     * @param BufferInterface $entropy
86
     * @return string[] - array of words from the word list
87
     */
88 26
    public function entropyToWords(BufferInterface $entropy): array
89
    {
90 26
        $ENT = $entropy->getSize() * 8;
91 26
        if (!in_array($entropy->getSize() * 8, $this->validEntropySizes)) {
92 2
            throw new \InvalidArgumentException("Invalid entropy length");
93
        }
94
95 24
        $CS = $ENT >> 5; // divide by 32, convinces static analysis result is an integer
96 24
        $bits = gmp_strval($entropy->getGmp(), 2) . $this->calculateChecksum($entropy, $CS);
97 24
        $bits = str_pad($bits, ($ENT + $CS), '0', STR_PAD_LEFT);
98
99 24
        $result = [];
100 24
        foreach (str_split($bits, 11) as $bit) {
101 24
            $result[] = $this->wordList->getWord(bindec($bit));
0 ignored issues
show
Bug introduced by
It seems like bindec($bit) can also be of type double; however, parameter $index of BitWasp\Bitcoin\Mnemonic...istInterface::getWord() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

101
            $result[] = $this->wordList->getWord(/** @scrutinizer ignore-type */ bindec($bit));
Loading history...
102
        }
103
104 24
        return $result;
105
    }
106
107
    /**
108
     * @param BufferInterface $entropy
109
     * @return string
110
     */
111 26
    public function entropyToMnemonic(BufferInterface $entropy): string
112
    {
113 26
        return implode(' ', $this->entropyToWords($entropy));
114
    }
115
116
    /**
117
     * @param string $mnemonic
118
     * @return BufferInterface
119
     */
120 27
    public function mnemonicToEntropy(string $mnemonic): BufferInterface
121
    {
122 27
        $words = explode(' ', $mnemonic);
123
124
        // Mnemonic sizes are multiples of 3 words
125 27
        if (count($words) % 3 !== 0) {
126 1
            throw new \InvalidArgumentException('Invalid mnemonic');
127
        }
128
129
        // Build up $bits from the list of words
130 26
        $bits = '';
131 26
        foreach ($words as $word) {
132 26
            $idx = $this->wordList->getIndex($word);
133
            // Mnemonic bit sizes are multiples of 33 bits
134 26
            $bits .= str_pad(decbin($idx), 11, '0', STR_PAD_LEFT);
135
        }
136
137
        // Every 32 bits of ENT adds a 1 CS bit.
138 26
        $CS = strlen($bits) / 33;
139 26
        $ENT = strlen($bits) - $CS;
140 26
        if (!in_array($ENT, $this->validEntropySizes)) {
141 1
            throw new \InvalidArgumentException('Invalid mnemonic - entropy size is invalid');
142
        }
143
144
        // Checksum bits
145 25
        $csBits = substr($bits, $ENT, $CS);
146
147
        // Split $ENT bits into 8 bit words to be packed
148 25
        $entArray = str_split(substr($bits, 0, $ENT), 8);
149 25
        $chars = [];
150 25
        for ($i = 0; $i < $ENT / 8; $i++) {
151 25
            $chars[] = bindec($entArray[$i]);
152
        }
153
154
        // Check checksum
155 25
        $entropy = new Buffer(pack("C*", ...$chars));
156 25
        if (hash_equals($csBits, $this->calculateChecksum($entropy, $CS))) {
157 24
            return $entropy;
158
        } else {
159 1
            throw new \InvalidArgumentException('Checksum does not match');
160
        }
161
    }
162
}
163