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 Base64 34 * [A-Z][a-z][0-9]+/ 35 * 36 * @package ParagonIE\ConstantTime 37 */ 38abstract class Base64 implements EncoderInterface 39{ 40 /** 41 * Encode into Base64 42 * 43 * Base64 character set "[A-Z][a-z][0-9]+/" 44 * 45 * @param string $binString 46 * @return string 47 * 48 * @throws TypeError 49 */ 50 public static function encode( 51 #[\SensitiveParameter] 52 string $binString 53 ): string { 54 return static::doEncode($binString, true); 55 } 56 57 /** 58 * Encode into Base64, no = padding 59 * 60 * Base64 character set "[A-Z][a-z][0-9]+/" 61 * 62 * @param string $src 63 * @return string 64 * 65 * @throws TypeError 66 */ 67 public static function encodeUnpadded( 68 #[\SensitiveParameter] 69 string $src 70 ): string { 71 return static::doEncode($src, false); 72 } 73 74 /** 75 * @param string $src 76 * @param bool $pad Include = padding? 77 * @return string 78 * 79 * @throws TypeError 80 */ 81 protected static function doEncode( 82 #[\SensitiveParameter] 83 string $src, 84 bool $pad = true 85 ): string { 86 $dest = ''; 87 $srcLen = Binary::safeStrlen($src); 88 // Main loop (no padding): 89 for ($i = 0; $i + 3 <= $srcLen; $i += 3) { 90 /** @var array<int, int> $chunk */ 91 $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 3)); 92 $b0 = $chunk[1]; 93 $b1 = $chunk[2]; 94 $b2 = $chunk[3]; 95 96 $dest .= 97 static::encode6Bits( $b0 >> 2 ) . 98 static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . 99 static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) . 100 static::encode6Bits( $b2 & 63); 101 } 102 // The last chunk, which may have padding: 103 if ($i < $srcLen) { 104 /** @var array<int, int> $chunk */ 105 $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); 106 $b0 = $chunk[1]; 107 if ($i + 1 < $srcLen) { 108 $b1 = $chunk[2]; 109 $dest .= 110 static::encode6Bits($b0 >> 2) . 111 static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . 112 static::encode6Bits(($b1 << 2) & 63); 113 if ($pad) { 114 $dest .= '='; 115 } 116 } else { 117 $dest .= 118 static::encode6Bits( $b0 >> 2) . 119 static::encode6Bits(($b0 << 4) & 63); 120 if ($pad) { 121 $dest .= '=='; 122 } 123 } 124 } 125 return $dest; 126 } 127 128 /** 129 * decode from base64 into binary 130 * 131 * Base64 character set "./[A-Z][a-z][0-9]" 132 * 133 * @param string $encodedString 134 * @param bool $strictPadding 135 * @return string 136 * 137 * @throws RangeException 138 * @throws TypeError 139 */ 140 public static function decode( 141 #[\SensitiveParameter] 142 string $encodedString, 143 bool $strictPadding = false 144 ): string { 145 // Remove padding 146 $srcLen = Binary::safeStrlen($encodedString); 147 if ($srcLen === 0) { 148 return ''; 149 } 150 151 if ($strictPadding) { 152 if (($srcLen & 3) === 0) { 153 if ($encodedString[$srcLen - 1] === '=') { 154 $srcLen--; 155 if ($encodedString[$srcLen - 1] === '=') { 156 $srcLen--; 157 } 158 } 159 } 160 if (($srcLen & 3) === 1) { 161 throw new RangeException( 162 'Incorrect padding' 163 ); 164 } 165 if ($encodedString[$srcLen - 1] === '=') { 166 throw new RangeException( 167 'Incorrect padding' 168 ); 169 } 170 } else { 171 $encodedString = \rtrim($encodedString, '='); 172 $srcLen = Binary::safeStrlen($encodedString); 173 } 174 175 $err = 0; 176 $dest = ''; 177 // Main loop (no padding): 178 for ($i = 0; $i + 4 <= $srcLen; $i += 4) { 179 /** @var array<int, int> $chunk */ 180 $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, 4)); 181 $c0 = static::decode6Bits($chunk[1]); 182 $c1 = static::decode6Bits($chunk[2]); 183 $c2 = static::decode6Bits($chunk[3]); 184 $c3 = static::decode6Bits($chunk[4]); 185 186 $dest .= \pack( 187 'CCC', 188 ((($c0 << 2) | ($c1 >> 4)) & 0xff), 189 ((($c1 << 4) | ($c2 >> 2)) & 0xff), 190 ((($c2 << 6) | $c3 ) & 0xff) 191 ); 192 $err |= ($c0 | $c1 | $c2 | $c3) >> 8; 193 } 194 // The last chunk, which may have padding: 195 if ($i < $srcLen) { 196 /** @var array<int, int> $chunk */ 197 $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i)); 198 $c0 = static::decode6Bits($chunk[1]); 199 200 if ($i + 2 < $srcLen) { 201 $c1 = static::decode6Bits($chunk[2]); 202 $c2 = static::decode6Bits($chunk[3]); 203 $dest .= \pack( 204 'CC', 205 ((($c0 << 2) | ($c1 >> 4)) & 0xff), 206 ((($c1 << 4) | ($c2 >> 2)) & 0xff) 207 ); 208 $err |= ($c0 | $c1 | $c2) >> 8; 209 if ($strictPadding) { 210 $err |= ($c2 << 6) & 0xff; 211 } 212 } elseif ($i + 1 < $srcLen) { 213 $c1 = static::decode6Bits($chunk[2]); 214 $dest .= \pack( 215 'C', 216 ((($c0 << 2) | ($c1 >> 4)) & 0xff) 217 ); 218 $err |= ($c0 | $c1) >> 8; 219 if ($strictPadding) { 220 $err |= ($c1 << 4) & 0xff; 221 } 222 } elseif ($strictPadding) { 223 $err |= 1; 224 } 225 } 226 $check = ($err === 0); 227 if (!$check) { 228 throw new RangeException( 229 'Base64::decode() only expects characters in the correct base64 alphabet' 230 ); 231 } 232 return $dest; 233 } 234 235 /** 236 * @param string $encodedString 237 * @return string 238 */ 239 public static function decodeNoPadding( 240 #[\SensitiveParameter] 241 string $encodedString 242 ): string { 243 $srcLen = Binary::safeStrlen($encodedString); 244 if ($srcLen === 0) { 245 return ''; 246 } 247 if (($srcLen & 3) === 0) { 248 // If $strLen is not zero, and it is divisible by 4, then it's at least 4. 249 if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') { 250 throw new InvalidArgumentException( 251 "decodeNoPadding() doesn't tolerate padding" 252 ); 253 } 254 } 255 return static::decode( 256 $encodedString, 257 true 258 ); 259 } 260 261 /** 262 * Uses bitwise operators instead of table-lookups to turn 6-bit integers 263 * into 8-bit integers. 264 * 265 * Base64 character set: 266 * [A-Z] [a-z] [0-9] + / 267 * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f 268 * 269 * @param int $src 270 * @return int 271 */ 272 protected static function decode6Bits(int $src): int 273 { 274 $ret = -1; 275 276 // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 277 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); 278 279 // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 280 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); 281 282 // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 283 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); 284 285 // if ($src == 0x2b) $ret += 62 + 1; 286 $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63; 287 288 // if ($src == 0x2f) ret += 63 + 1; 289 $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64; 290 291 return $ret; 292 } 293 294 /** 295 * Uses bitwise operators instead of table-lookups to turn 8-bit integers 296 * into 6-bit integers. 297 * 298 * @param int $src 299 * @return string 300 */ 301 protected static function encode6Bits(int $src): string 302 { 303 $diff = 0x41; 304 305 // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 306 $diff += ((25 - $src) >> 8) & 6; 307 308 // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 309 $diff -= ((51 - $src) >> 8) & 75; 310 311 // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15 312 $diff -= ((61 - $src) >> 8) & 15; 313 314 // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3 315 $diff += ((62 - $src) >> 8) & 3; 316 317 return \pack('C', $src + $diff); 318 } 319} 320