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