1<?php
2declare(strict_types=1);
3namespace ParagonIE\ConstantTime;
4
5/**
6 *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
7 *  Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
8 *
9 *  Permission is hereby granted, free of charge, to any person obtaining a copy
10 *  of this software and associated documentation files (the "Software"), to deal
11 *  in the Software without restriction, including without limitation the rights
12 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 *  copies of the Software, and to permit persons to whom the Software is
14 *  furnished to do so, subject to the following conditions:
15 *
16 *  The above copyright notice and this permission notice shall be included in all
17 *  copies or substantial portions of the Software.
18 *
19 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 *  SOFTWARE.
26 */
27
28/**
29 * Class Base64
30 * [A-Z][a-z][0-9]+/
31 *
32 * @package ParagonIE\ConstantTime
33 */
34abstract class Base64 implements EncoderInterface
35{
36    /**
37     * Encode into Base64
38     *
39     * Base64 character set "[A-Z][a-z][0-9]+/"
40     *
41     * @param string $binString
42     * @return string
43     * @throws \TypeError
44     */
45    public static function encode(string $binString): string
46    {
47        return static::doEncode($binString, true);
48    }
49
50    /**
51     * Encode into Base64, no = padding
52     *
53     * Base64 character set "[A-Z][a-z][0-9]+/"
54     *
55     * @param string $src
56     * @return string
57     * @throws \TypeError
58     */
59    public static function encodeUnpadded(string $src): string
60    {
61        return static::doEncode($src, false);
62    }
63
64    /**
65     * @param string $src
66     * @param bool $pad   Include = padding?
67     * @return string
68     * @throws \TypeError
69     */
70    protected static function doEncode(string $src, bool $pad = true): string
71    {
72        $dest = '';
73        $srcLen = Binary::safeStrlen($src);
74        // Main loop (no padding):
75        for ($i = 0; $i + 3 <= $srcLen; $i += 3) {
76            /** @var array<int, int> $chunk */
77            $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 3));
78            $b0 = $chunk[1];
79            $b1 = $chunk[2];
80            $b2 = $chunk[3];
81
82            $dest .=
83                static::encode6Bits(               $b0 >> 2       ) .
84                static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
85                static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
86                static::encode6Bits(  $b2                     & 63);
87        }
88        // The last chunk, which may have padding:
89        if ($i < $srcLen) {
90            /** @var array<int, int> $chunk */
91            $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
92            $b0 = $chunk[1];
93            if ($i + 1 < $srcLen) {
94                $b1 = $chunk[2];
95                $dest .=
96                    static::encode6Bits($b0 >> 2) .
97                    static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
98                    static::encode6Bits(($b1 << 2) & 63);
99                if ($pad) {
100                    $dest .= '=';
101                }
102            } else {
103                $dest .=
104                    static::encode6Bits( $b0 >> 2) .
105                    static::encode6Bits(($b0 << 4) & 63);
106                if ($pad) {
107                    $dest .= '==';
108                }
109            }
110        }
111        return $dest;
112    }
113
114    /**
115     * decode from base64 into binary
116     *
117     * Base64 character set "./[A-Z][a-z][0-9]"
118     *
119     * @param string $encodedString
120     * @param bool $strictPadding
121     * @return string
122     * @throws \RangeException
123     * @throws \TypeError
124     * @psalm-suppress RedundantCondition
125     */
126    public static function decode(string $encodedString, bool $strictPadding = false): string
127    {
128        // Remove padding
129        $srcLen = Binary::safeStrlen($encodedString);
130        if ($srcLen === 0) {
131            return '';
132        }
133
134        if ($strictPadding) {
135            if (($srcLen & 3) === 0) {
136                if ($encodedString[$srcLen - 1] === '=') {
137                    $srcLen--;
138                    if ($encodedString[$srcLen - 1] === '=') {
139                        $srcLen--;
140                    }
141                }
142            }
143            if (($srcLen & 3) === 1) {
144                throw new \RangeException(
145                    'Incorrect padding'
146                );
147            }
148            if ($encodedString[$srcLen - 1] === '=') {
149                throw new \RangeException(
150                    'Incorrect padding'
151                );
152            }
153        } else {
154            $encodedString = \rtrim($encodedString, '=');
155            $srcLen = Binary::safeStrlen($encodedString);
156        }
157
158        $err = 0;
159        $dest = '';
160        // Main loop (no padding):
161        for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
162            /** @var array<int, int> $chunk */
163            $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, 4));
164            $c0 = static::decode6Bits($chunk[1]);
165            $c1 = static::decode6Bits($chunk[2]);
166            $c2 = static::decode6Bits($chunk[3]);
167            $c3 = static::decode6Bits($chunk[4]);
168
169            $dest .= \pack(
170                'CCC',
171                ((($c0 << 2) | ($c1 >> 4)) & 0xff),
172                ((($c1 << 4) | ($c2 >> 2)) & 0xff),
173                ((($c2 << 6) |  $c3      ) & 0xff)
174            );
175            $err |= ($c0 | $c1 | $c2 | $c3) >> 8;
176        }
177        // The last chunk, which may have padding:
178        if ($i < $srcLen) {
179            /** @var array<int, int> $chunk */
180            $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i));
181            $c0 = static::decode6Bits($chunk[1]);
182
183            if ($i + 2 < $srcLen) {
184                $c1 = static::decode6Bits($chunk[2]);
185                $c2 = static::decode6Bits($chunk[3]);
186                $dest .= \pack(
187                    'CC',
188                    ((($c0 << 2) | ($c1 >> 4)) & 0xff),
189                    ((($c1 << 4) | ($c2 >> 2)) & 0xff)
190                );
191                $err |= ($c0 | $c1 | $c2) >> 8;
192            } elseif ($i + 1 < $srcLen) {
193                $c1 = static::decode6Bits($chunk[2]);
194                $dest .= \pack(
195                    'C',
196                    ((($c0 << 2) | ($c1 >> 4)) & 0xff)
197                );
198                $err |= ($c0 | $c1) >> 8;
199            } elseif ($strictPadding) {
200                $err |= 1;
201            }
202        }
203        $check = ($err === 0);
204        if (!$check) {
205            throw new \RangeException(
206                'Base64::decode() only expects characters in the correct base64 alphabet'
207            );
208        }
209        return $dest;
210    }
211
212    /**
213     * Uses bitwise operators instead of table-lookups to turn 6-bit integers
214     * into 8-bit integers.
215     *
216     * Base64 character set:
217     * [A-Z]      [a-z]      [0-9]      +     /
218     * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
219     *
220     * @param int $src
221     * @return int
222     */
223    protected static function decode6Bits(int $src): int
224    {
225        $ret = -1;
226
227        // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
228        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
229
230        // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
231        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
232
233        // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
234        $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
235
236        // if ($src == 0x2b) $ret += 62 + 1;
237        $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63;
238
239        // if ($src == 0x2f) ret += 63 + 1;
240        $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64;
241
242        return $ret;
243    }
244
245    /**
246     * Uses bitwise operators instead of table-lookups to turn 8-bit integers
247     * into 6-bit integers.
248     *
249     * @param int $src
250     * @return string
251     */
252    protected static function encode6Bits(int $src): string
253    {
254        $diff = 0x41;
255
256        // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
257        $diff += ((25 - $src) >> 8) & 6;
258
259        // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
260        $diff -= ((51 - $src) >> 8) & 75;
261
262        // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15
263        $diff -= ((61 - $src) >> 8) & 15;
264
265        // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3
266        $diff += ((62 - $src) >> 8) & 3;
267
268        return \pack('C', $src + $diff);
269    }
270}
271