xref: /dokuwiki/vendor/paragonie/constant_time_encoding/src/Base32.php (revision 6b69197a9f73c0170a87a2eecfac4f353db0779b)
1<?php
2declare(strict_types=1);
3namespace ParagonIE\ConstantTime;
4
5use InvalidArgumentException;
6use RangeException;
7use TypeError;
8
9/**
10 *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
11 *  Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
12 *
13 *  Permission is hereby granted, free of charge, to any person obtaining a copy
14 *  of this software and associated documentation files (the "Software"), to deal
15 *  in the Software without restriction, including without limitation the rights
16 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 *  copies of the Software, and to permit persons to whom the Software is
18 *  furnished to do so, subject to the following conditions:
19 *
20 *  The above copyright notice and this permission notice shall be included in all
21 *  copies or substantial portions of the Software.
22 *
23 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 *  SOFTWARE.
30 */
31
32/**
33 * Class Base32
34 * [A-Z][2-7]
35 *
36 * @package ParagonIE\ConstantTime
37 */
38abstract class Base32 implements EncoderInterface
39{
40    /**
41     * Decode a Base32-encoded string into raw binary
42     *
43     * @param string $encodedString
44     * @param bool $strictPadding
45     * @return string
46     */
47    public static function decode(string $encodedString, bool $strictPadding = false): string
48    {
49        return static::doDecode($encodedString, false, $strictPadding);
50    }
51
52    /**
53     * Decode an uppercase Base32-encoded string into raw binary
54     *
55     * @param string $src
56     * @param bool $strictPadding
57     * @return string
58     */
59    public static function decodeUpper(string $src, bool $strictPadding = false): string
60    {
61        return static::doDecode($src, true, $strictPadding);
62    }
63
64    /**
65     * Encode into Base32 (RFC 4648)
66     *
67     * @param string $binString
68     * @return string
69     * @throws TypeError
70     */
71    public static function encode(string $binString): string
72    {
73        return static::doEncode($binString, false, true);
74    }
75    /**
76     * Encode into Base32 (RFC 4648)
77     *
78     * @param string $src
79     * @return string
80     * @throws TypeError
81     */
82    public static function encodeUnpadded(string $src): string
83    {
84        return static::doEncode($src, false, false);
85    }
86
87    /**
88     * Encode into uppercase Base32 (RFC 4648)
89     *
90     * @param string $src
91     * @return string
92     * @throws TypeError
93     */
94    public static function encodeUpper(string $src): string
95    {
96        return static::doEncode($src, true, true);
97    }
98
99    /**
100     * Encode into uppercase Base32 (RFC 4648)
101     *
102     * @param string $src
103     * @return string
104     * @throws TypeError
105     */
106    public static function encodeUpperUnpadded(string $src): string
107    {
108        return static::doEncode($src, true, false);
109    }
110
111    /**
112     * Uses bitwise operators instead of table-lookups to turn 5-bit integers
113     * into 8-bit integers.
114     *
115     * @param int $src
116     * @return int
117     */
118    protected static function decode5Bits(int $src): int
119    {
120        $ret = -1;
121
122        // if ($src > 96 && $src < 123) $ret += $src - 97 + 1; // -64
123        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96);
124
125        // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
126        $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
127
128        return $ret;
129    }
130
131    /**
132     * Uses bitwise operators instead of table-lookups to turn 5-bit integers
133     * into 8-bit integers.
134     *
135     * Uppercase variant.
136     *
137     * @param int $src
138     * @return int
139     */
140    protected static function decode5BitsUpper(int $src): int
141    {
142        $ret = -1;
143
144        // if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64
145        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
146
147        // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
148        $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
149
150        return $ret;
151    }
152
153    /**
154     * Uses bitwise operators instead of table-lookups to turn 8-bit integers
155     * into 5-bit integers.
156     *
157     * @param int $src
158     * @return string
159     */
160    protected static function encode5Bits(int $src): string
161    {
162        $diff = 0x61;
163
164        // if ($src > 25) $ret -= 72;
165        $diff -= ((25 - $src) >> 8) & 73;
166
167        return \pack('C', $src + $diff);
168    }
169
170    /**
171     * Uses bitwise operators instead of table-lookups to turn 8-bit integers
172     * into 5-bit integers.
173     *
174     * Uppercase variant.
175     *
176     * @param int $src
177     * @return string
178     */
179    protected static function encode5BitsUpper(int $src): string
180    {
181        $diff = 0x41;
182
183        // if ($src > 25) $ret -= 40;
184        $diff -= ((25 - $src) >> 8) & 41;
185
186        return \pack('C', $src + $diff);
187    }
188
189    /**
190     * @param string $encodedString
191     * @param bool $upper
192     * @return string
193     */
194    public static function decodeNoPadding(string $encodedString, bool $upper = false): string
195    {
196        $srcLen = Binary::safeStrlen($encodedString);
197        if ($srcLen === 0) {
198            return '';
199        }
200        if (($srcLen & 7) === 0) {
201            for ($j = 0; $j < 7 && $j < $srcLen; ++$j) {
202                if ($encodedString[$srcLen - $j - 1] === '=') {
203                    throw new InvalidArgumentException(
204                        "decodeNoPadding() doesn't tolerate padding"
205                    );
206                }
207            }
208        }
209        return static::doDecode(
210            $encodedString,
211            $upper,
212            true
213        );
214    }
215
216    /**
217     * Base32 decoding
218     *
219     * @param string $src
220     * @param bool $upper
221     * @param bool $strictPadding
222     * @return string
223     *
224     * @throws TypeError
225     * @psalm-suppress RedundantCondition
226     */
227    protected static function doDecode(
228        string $src,
229        bool $upper = false,
230        bool $strictPadding = false
231    ): string {
232        // We do this to reduce code duplication:
233        $method = $upper
234            ? 'decode5BitsUpper'
235            : 'decode5Bits';
236
237        // Remove padding
238        $srcLen = Binary::safeStrlen($src);
239        if ($srcLen === 0) {
240            return '';
241        }
242        if ($strictPadding) {
243            if (($srcLen & 7) === 0) {
244                for ($j = 0; $j < 7; ++$j) {
245                    if ($src[$srcLen - 1] === '=') {
246                        $srcLen--;
247                    } else {
248                        break;
249                    }
250                }
251            }
252            if (($srcLen & 7) === 1) {
253                throw new RangeException(
254                    'Incorrect padding'
255                );
256            }
257        } else {
258            $src = \rtrim($src, '=');
259            $srcLen = Binary::safeStrlen($src);
260        }
261
262        $err = 0;
263        $dest = '';
264        // Main loop (no padding):
265        for ($i = 0; $i + 8 <= $srcLen; $i += 8) {
266            /** @var array<int, int> $chunk */
267            $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 8));
268            /** @var int $c0 */
269            $c0 = static::$method($chunk[1]);
270            /** @var int $c1 */
271            $c1 = static::$method($chunk[2]);
272            /** @var int $c2 */
273            $c2 = static::$method($chunk[3]);
274            /** @var int $c3 */
275            $c3 = static::$method($chunk[4]);
276            /** @var int $c4 */
277            $c4 = static::$method($chunk[5]);
278            /** @var int $c5 */
279            $c5 = static::$method($chunk[6]);
280            /** @var int $c6 */
281            $c6 = static::$method($chunk[7]);
282            /** @var int $c7 */
283            $c7 = static::$method($chunk[8]);
284
285            $dest .= \pack(
286                'CCCCC',
287                (($c0 << 3) | ($c1 >> 2)             ) & 0xff,
288                (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
289                (($c3 << 4) | ($c4 >> 1)             ) & 0xff,
290                (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff,
291                (($c6 << 5) | ($c7     )             ) & 0xff
292            );
293            $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8;
294        }
295        // The last chunk, which may have padding:
296        if ($i < $srcLen) {
297            /** @var array<int, int> $chunk */
298            $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
299            /** @var int $c0 */
300            $c0 = static::$method($chunk[1]);
301
302            if ($i + 6 < $srcLen) {
303                /** @var int $c1 */
304                $c1 = static::$method($chunk[2]);
305                /** @var int $c2 */
306                $c2 = static::$method($chunk[3]);
307                /** @var int $c3 */
308                $c3 = static::$method($chunk[4]);
309                /** @var int $c4 */
310                $c4 = static::$method($chunk[5]);
311                /** @var int $c5 */
312                $c5 = static::$method($chunk[6]);
313                /** @var int $c6 */
314                $c6 = static::$method($chunk[7]);
315
316                $dest .= \pack(
317                    'CCCC',
318                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,
319                    (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
320                    (($c3 << 4) | ($c4 >> 1)             ) & 0xff,
321                    (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff
322                );
323                $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8;
324                if ($strictPadding) {
325                    $err |= ($c6 << 5) & 0xff;
326                }
327            } elseif ($i + 5 < $srcLen) {
328                /** @var int $c1 */
329                $c1 = static::$method($chunk[2]);
330                /** @var int $c2 */
331                $c2 = static::$method($chunk[3]);
332                /** @var int $c3 */
333                $c3 = static::$method($chunk[4]);
334                /** @var int $c4 */
335                $c4 = static::$method($chunk[5]);
336                /** @var int $c5 */
337                $c5 = static::$method($chunk[6]);
338
339                $dest .= \pack(
340                    'CCCC',
341                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,
342                    (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
343                    (($c3 << 4) | ($c4 >> 1)             ) & 0xff,
344                    (($c4 << 7) | ($c5 << 2)             ) & 0xff
345                );
346                $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8;
347            } elseif ($i + 4 < $srcLen) {
348                /** @var int $c1 */
349                $c1 = static::$method($chunk[2]);
350                /** @var int $c2 */
351                $c2 = static::$method($chunk[3]);
352                /** @var int $c3 */
353                $c3 = static::$method($chunk[4]);
354                /** @var int $c4 */
355                $c4 = static::$method($chunk[5]);
356
357                $dest .= \pack(
358                    'CCC',
359                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,
360                    (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
361                    (($c3 << 4) | ($c4 >> 1)             ) & 0xff
362                );
363                $err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8;
364                if ($strictPadding) {
365                    $err |= ($c4 << 7) & 0xff;
366                }
367            } elseif ($i + 3 < $srcLen) {
368                /** @var int $c1 */
369                $c1 = static::$method($chunk[2]);
370                /** @var int $c2 */
371                $c2 = static::$method($chunk[3]);
372                /** @var int $c3 */
373                $c3 = static::$method($chunk[4]);
374
375                $dest .= \pack(
376                    'CC',
377                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,
378                    (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff
379                );
380                $err |= ($c0 | $c1 | $c2 | $c3) >> 8;
381                if ($strictPadding) {
382                    $err |= ($c3 << 4) & 0xff;
383                }
384            } elseif ($i + 2 < $srcLen) {
385                /** @var int $c1 */
386                $c1 = static::$method($chunk[2]);
387                /** @var int $c2 */
388                $c2 = static::$method($chunk[3]);
389
390                $dest .= \pack(
391                    'CC',
392                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,
393                    (($c1 << 6) | ($c2 << 1)             ) & 0xff
394                );
395                $err |= ($c0 | $c1 | $c2) >> 8;
396                if ($strictPadding) {
397                    $err |= ($c2 << 6) & 0xff;
398                }
399            } elseif ($i + 1 < $srcLen) {
400                /** @var int $c1 */
401                $c1 = static::$method($chunk[2]);
402
403                $dest .= \pack(
404                    'C',
405                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff
406                );
407                $err |= ($c0 | $c1) >> 8;
408                if ($strictPadding) {
409                    $err |= ($c1 << 6) & 0xff;
410                }
411            } else {
412                $dest .= \pack(
413                    'C',
414                    (($c0 << 3)                          ) & 0xff
415                );
416                $err |= ($c0) >> 8;
417            }
418        }
419        $check = ($err === 0);
420        if (!$check) {
421            throw new RangeException(
422                'Base32::doDecode() only expects characters in the correct base32 alphabet'
423            );
424        }
425        return $dest;
426    }
427
428    /**
429     * Base32 Encoding
430     *
431     * @param string $src
432     * @param bool $upper
433     * @param bool $pad
434     * @return string
435     * @throws TypeError
436     */
437    protected static function doEncode(string $src, bool $upper = false, $pad = true): string
438    {
439        // We do this to reduce code duplication:
440        $method = $upper
441            ? 'encode5BitsUpper'
442            : 'encode5Bits';
443
444        $dest = '';
445        $srcLen = Binary::safeStrlen($src);
446
447        // Main loop (no padding):
448        for ($i = 0; $i + 5 <= $srcLen; $i += 5) {
449            /** @var array<int, int> $chunk */
450            $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5));
451            $b0 = $chunk[1];
452            $b1 = $chunk[2];
453            $b2 = $chunk[3];
454            $b3 = $chunk[4];
455            $b4 = $chunk[5];
456            $dest .=
457                static::$method(              ($b0 >> 3)  & 31) .
458                static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
459                static::$method((($b1 >> 1)             ) & 31) .
460                static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
461                static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
462                static::$method((($b3 >> 2)             ) & 31) .
463                static::$method((($b3 << 3) | ($b4 >> 5)) & 31) .
464                static::$method(  $b4                     & 31);
465        }
466        // The last chunk, which may have padding:
467        if ($i < $srcLen) {
468            /** @var array<int, int> $chunk */
469            $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
470            $b0 = $chunk[1];
471            if ($i + 3 < $srcLen) {
472                $b1 = $chunk[2];
473                $b2 = $chunk[3];
474                $b3 = $chunk[4];
475                $dest .=
476                    static::$method(              ($b0 >> 3)  & 31) .
477                    static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
478                    static::$method((($b1 >> 1)             ) & 31) .
479                    static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
480                    static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
481                    static::$method((($b3 >> 2)             ) & 31) .
482                    static::$method((($b3 << 3)             ) & 31);
483                if ($pad) {
484                    $dest .= '=';
485                }
486            } elseif ($i + 2 < $srcLen) {
487                $b1 = $chunk[2];
488                $b2 = $chunk[3];
489                $dest .=
490                    static::$method(              ($b0 >> 3)  & 31) .
491                    static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
492                    static::$method((($b1 >> 1)             ) & 31) .
493                    static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
494                    static::$method((($b2 << 1)             ) & 31);
495                if ($pad) {
496                    $dest .= '===';
497                }
498            } elseif ($i + 1 < $srcLen) {
499                $b1 = $chunk[2];
500                $dest .=
501                    static::$method(              ($b0 >> 3)  & 31) .
502                    static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
503                    static::$method((($b1 >> 1)             ) & 31) .
504                    static::$method((($b1 << 4)             ) & 31);
505                if ($pad) {
506                    $dest .= '====';
507                }
508            } else {
509                $dest .=
510                    static::$method(              ($b0 >> 3)  & 31) .
511                    static::$method( ($b0 << 2)               & 31);
512                if ($pad) {
513                    $dest .= '======';
514                }
515            }
516        }
517        return $dest;
518    }
519}
520