Completed
Pull Request — master (#503)
by thomas
49:19 queued 47:08
created

Bech32::createChecksum()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace BitWasp\Bitcoin;
4
5
use BitWasp\Bitcoin\Exceptions\Bech32Exception;
6
use BitWasp\Bitcoin\Script\WitnessProgram;
7
use BitWasp\Buffertools\Buffer;
8
9
class Bech32
10
{
11
    /**
12
     * @var string
13
     */
14
    protected static $charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
15
16
    /**
17
     * @var array
18
     */
19
    protected static $charsetKey = [
20
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
21
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
22
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
23
        15, -1, 10, 17, 21, 20, 26, 30,  7,  5, -1, -1, -1, -1, -1, -1,
24
        -1, 29, -1, 24, 13, 25,  9,  8, 23, -1, 18, 22, 31, 27, 19, -1,
25
        1,  0,  3, 16, 11, 28, 12, 14,  6,  4,  2, -1, -1, -1, -1, -1,
26
        -1, 29, -1, 24, 13, 25,  9,  8, 23, -1, 18, 22, 31, 27, 19, -1,
27
        1,  0,  3, 16, 11, 28, 12, 14,  6,  4,  2, -1, -1, -1, -1, -1
28
    ];
29
30
    /**
31
     * @var array
32
     */
33
    protected static $generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
34
35
    /**
36
     * @param int[] $values
37
     * @param int $numValues
38
     * @return int
39
     */
40 48
    public static function polyMod(array $values, $numValues)
41
    {
42 48
        $chk = 1;
43 48
        for ($i = 0; $i < $numValues; $i++) {
44 48
            $top = $chk >> 25;
45 48
            $chk = ($chk & 0x1ffffff) << 5 ^ $values[$i];
46
47 48
            for ($j = 0; $j < 5; $j++) {
48 48
                $value = (($top >> $j) & 1) ? self::$generator[$j] : 0;
49 48
                $chk ^= $value;
50
            }
51
        }
52
53 48
        return $chk;
54
    }
55
56
    /**
57
     * Expands the human readable part into a character array for checksumming.
58
     * @param string $hrp
59
     * @param int $hrpLen
60
     * @return array
61
     */
62 48
    public static function hrpExpand($hrp, $hrpLen)
63
    {
64 48
        $expand1 = [];
65 48
        $expand2 = [];
66 48
        for ($i = 0; $i < $hrpLen; $i++) {
67 48
            $o = ord($hrp[$i]);
68 48
            $expand1[] = $o >> 5;
69 48
            $expand2[] = $o & 31;
70
        }
71
72 48
        return array_merge($expand1, [0], $expand2);
73
    }
74
75
    /**
76
     * Converts words of $fromBits bits to $toBits bits in size.
77
     *
78
     * @param int[] $data - character array of data to convert
79
     * @param int $inLen - number of elements in array
80
     * @param int $fromBits - word (bit count) size of provided data
81
     * @param int $toBits - requested word size (bit count)
82
     * @param bool $pad - whether to pad (only when encoding)
83
     * @return array
84
     * @throws Bech32Exception
85
     */
86 34
    public static function convertBits(array $data, $inLen, $fromBits, $toBits, $pad = true)
87
    {
88 34
        $acc = 0;
89 34
        $bits = 0;
90 34
        $ret = [];
91 34
        $maxv = (1 << $toBits) - 1;
92 34
        $maxacc = (1 << ($fromBits + $toBits - 1)) - 1;
93
94 34
        for ($i = 0; $i < $inLen; $i++) {
95 34
            $value = $data[$i];
96 34
            if ($value < 0 || $value >> $fromBits) {
97 2
                throw new Bech32Exception('Invalid value for convert bits');
98
            }
99
100 32
            $acc = (($acc << $fromBits) | $value) & $maxacc;
101 32
            $bits += $fromBits;
102
103 32
            while ($bits >= $toBits) {
104 32
                $bits -= $toBits;
105 32
                $ret[] = (($acc >> $bits) & $maxv);
106
            }
107
        }
108
109 32
        if ($pad) {
110 12
            if ($bits) {
111 12
                $ret[] = ($acc << $toBits - $bits) & $maxv;
112
            }
113 32
        } else if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) {
114 4
            throw new Bech32Exception('Invalid data');
115
        }
116
117 28
        return $ret;
118
    }
119
120
    /**
121
     * @param string $hrp
122
     * @param int[] $convertedDataChars
123
     * @return array
124
     */
125 12
    public static function createChecksum($hrp, array $convertedDataChars)
126
    {
127 12
        $values = array_merge(self::hrpExpand($hrp, strlen($hrp)), $convertedDataChars);
128 12
        $polyMod = self::polyMod(array_merge($values, [0, 0, 0, 0, 0, 0]), count($values) + 6) ^ 1;
129 12
        $results = [];
130 12
        for ($i = 0; $i < 6; $i++) {
131 12
            $results[$i] = ($polyMod >> 5 * (5 - $i)) & 31;
132
        }
133
134 12
        return $results;
135
    }
136
137
    /**
138
     * Verifies the checksum given $hrp and $convertedDataChars.
139
     *
140
     * @param string $hrp
141
     * @param int[] $convertedDataChars
142
     * @return bool
143
     */
144 48
    public static function verifyChecksum($hrp, array $convertedDataChars)
145
    {
146 48
        $expandHrp = self::hrpExpand($hrp, strlen($hrp));
147 48
        $r = array_merge($expandHrp, $convertedDataChars);
148 48
        $poly = self::polyMod($r, count($r));
149 48
        return $poly === 1;
150
    }
151
152
    /**
153
     * @param string $hrp
154
     * @param array $combinedDataChars
155
     * @return string
156
     */
157 12
    public static function encode($hrp, array $combinedDataChars)
158
    {
159 12
        $checksum = self::createChecksum($hrp, $combinedDataChars);
160 12
        $characters = array_merge($combinedDataChars, $checksum);
161
162 12
        $encoded = [];
163 12
        for ($i = 0, $n = count($characters); $i < $n; $i++) {
164 12
            $encoded[$i] = self::$charset[$characters[$i]];
165
        }
166
167 12
        return "{$hrp}1" . implode('', $encoded);
168
    }
169
170
    /**
171
     * Validates a bech32 string and returns [$hrp, $dataChars] if
172
     * the conversion was successful. An exception is thrown on invalid
173
     * data.
174
     *
175
     * @param string $sBech - the bech32 encoded string
176
     * @return array - returns [$hrp, $dataChars]
177
     * @throws Bech32Exception
178
     */
179 58
    public static function decode($sBech)
180
    {
181 58
        $length = strlen($sBech);
182 58
        if ($length > 90) {
183 2
            throw new Bech32Exception('Bech32 string cannot exceed 90 characters in length');
184
        }
185
186 56
        $chars = array_values(unpack('C*', $sBech));
187
188 56
        $haveUpper = false;
189 56
        $haveLower = false;
190 56
        $positionOne = -1;
191
192 56
        for ($i = 0; $i < $length; $i++) {
193 56
            $x = $chars[$i];
194 56
            if ($x < 33 || $x > 126) {
195 2
                throw new Bech32Exception('Out of range character in bech32 string');
196
            }
197
198 54
            if ($x >= 0x61 && $x <= 0x7a) {
199 44
                $haveLower = true;
200
            }
201
202 54
            if ($x >= 0x41 && $x <= 0x5a) {
203 14
                $haveUpper = true;
204 14
                $x = $chars[$i] = $x + 0x20;
205
            }
206
207
            // find location of last '1' character
208 54
            if ($x === 0x31) {
209 52
                $positionOne = $i;
210
            }
211
        }
212
213 54
        if ($haveUpper && $haveLower) {
214 4
            throw new Bech32Exception('Data contains mixture of higher/lower case characters');
215
        }
216
217 50
        if ($positionOne < 1 || ($positionOne + 7) > $length) {
218 2
            throw new Bech32Exception('Invalid location for `1` character');
219
        }
220
221 48
        $hrp = [];
222 48
        for ($i = 0; $i < $positionOne; $i++) {
223 48
            $hrp[$i] = chr($chars[$i]);
224
        }
225
226 48
        $hrp = implode('', $hrp);
227 48
        $data = [];
228 48
        for ($i = $positionOne + 1; $i < $length; $i++) {
229 48
            $data[] = ($chars[$i] & 0x80) ? -1 : self::$charsetKey[$chars[$i]];
230
        }
231
232 48
        if (!self::verifyChecksum($hrp, $data)) {
233 14
            throw new Bech32Exception('Invalid bech32 checksum');
234
        }
235
236 44
        return [$hrp, array_slice($data, 0, -6)];
237
    }
238
239
    /**
240
     * Takes the $hrp and $witnessProgram and produces a native segwit
241
     * address.
242
     *
243
     * @param string $hrp
244
     * @param WitnessProgram $witnessProgram
245
     * @return string
246
     */
247 12
    public static function encodeSegwit($hrp, WitnessProgram $witnessProgram)
248
    {
249 12
        $programChars = array_values(unpack('C*', $witnessProgram->getProgram()->getBinary()));
250 12
        $programBits = self::convertBits($programChars, count($programChars), 8, 5, true);
251 12
        $encodeData = array_merge([$witnessProgram->getVersion()], $programBits);
252
253 12
        return self::encode($hrp, $encodeData);
254
    }
255
256
    /**
257
     * Decodes the provided $bech32 string, validating against
258
     * the chosen prefix.
259
     *
260
     * @param string $hrp
261
     * @param string $bech32
262
     * @return WitnessProgram
263
     * @throws Bech32Exception
264
     */
265 38
    public static function decodeSegwit($hrp, $bech32)
266
    {
267 38
        list ($hrpGot, $data) = self::decode($bech32);
268 34
        if ($hrpGot !== $hrp) {
269 14
            throw new Bech32Exception('Invalid prefix for address');
270
        }
271
272 32
        $decoded = self::convertBits(array_slice($data, 1), count($data) - 1, 5, 8, false);
273 28
        $decodeLen = count($decoded);
274 28
        if ($decodeLen < 2 || $decodeLen > 40) {
275 4
            throw new Bech32Exception('Invalid segwit address');
276
        }
277
278 24
        if ($data[0] > 16) {
279 2
            throw new Bech32Exception('Invalid witness program version');
280
        }
281
282 22
        $bytes = '';
283 22
        foreach ($decoded as $char) {
284 22
            $bytes .= chr($char);
285
        }
286 22
        $decoded = new Buffer($bytes);
287
288 22
        if (0 === $data[0]) {
289 16
            return WitnessProgram::v0($decoded);
290
        }
291
292 6
        return new WitnessProgram($data[0], $decoded);
293
    }
294
}
295