1<?php 2declare(strict_types=1); 3namespace ParagonIE\ConstantTime; 4 5use InvalidArgumentException; 6use Override; 7use RangeException; 8use SensitiveParameter; 9use SodiumException; 10use TypeError; 11use function extension_loaded; 12use function pack; 13use function rtrim; 14use function sodium_base642bin; 15use function sodium_bin2base64; 16use function strlen; 17use function substr; 18use function unpack; 19use const SODIUM_BASE64_VARIANT_ORIGINAL; 20use const SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING; 21use const SODIUM_BASE64_VARIANT_URLSAFE; 22use const SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING; 23 24/** 25 * Copyright (c) 2016 - 2022 Paragon Initiative Enterprises. 26 * Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com) 27 * 28 * Permission is hereby granted, free of charge, to any person obtaining a copy 29 * of this software and associated documentation files (the "Software"), to deal 30 * in the Software without restriction, including without limitation the rights 31 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 * copies of the Software, and to permit persons to whom the Software is 33 * furnished to do so, subject to the following conditions: 34 * 35 * The above copyright notice and this permission notice shall be included in all 36 * copies or substantial portions of the Software. 37 * 38 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 * SOFTWARE. 45 */ 46 47/** 48 * Class Base64 49 * [A-Z][a-z][0-9]+/ 50 * 51 * @package ParagonIE\ConstantTime 52 */ 53abstract class Base64 implements EncoderInterface 54{ 55 /** 56 * Encode into Base64 57 * 58 * Base64 character set "[A-Z][a-z][0-9]+/" 59 * 60 * @param string $binString 61 * @return string 62 * 63 * @throws TypeError 64 */ 65 #[Override] 66 public static function encode( 67 #[SensitiveParameter] 68 string $binString 69 ): string { 70 if (extension_loaded('sodium')) { 71 $variant = match(static::class) { 72 Base64::class => SODIUM_BASE64_VARIANT_ORIGINAL, 73 Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE, 74 default => 0, 75 }; 76 if ($variant > 0) { 77 try { 78 return sodium_bin2base64($binString, $variant); 79 } catch (SodiumException $ex) { 80 throw new RangeException($ex->getMessage(), $ex->getCode(), $ex); 81 } 82 } 83 } 84 return static::doEncode($binString, true); 85 } 86 87 /** 88 * Encode into Base64, no = padding 89 * 90 * Base64 character set "[A-Z][a-z][0-9]+/" 91 * 92 * @param string $src 93 * @return string 94 * 95 * @throws TypeError 96 * @api 97 */ 98 public static function encodeUnpadded( 99 #[SensitiveParameter] 100 string $src 101 ): string { 102 if (extension_loaded('sodium')) { 103 $variant = match(static::class) { 104 Base64::class => SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING, 105 Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, 106 default => 0, 107 }; 108 if ($variant > 0) { 109 try { 110 return sodium_bin2base64($src, $variant); 111 } catch (SodiumException $ex) { 112 throw new RangeException($ex->getMessage(), $ex->getCode(), $ex); 113 } 114 } 115 } 116 return static::doEncode($src, false); 117 } 118 119 /** 120 * @param string $src 121 * @param bool $pad Include = padding? 122 * @return string 123 * 124 * @throws TypeError 125 */ 126 protected static function doEncode( 127 #[SensitiveParameter] 128 string $src, 129 bool $pad = true 130 ): string { 131 $dest = ''; 132 $srcLen = strlen($src); 133 // Main loop (no padding): 134 for ($i = 0; $i + 3 <= $srcLen; $i += 3) { 135 /** @var array<int, int> $chunk */ 136 $chunk = unpack('C*', substr($src, $i, 3)); 137 $b0 = $chunk[1]; 138 $b1 = $chunk[2]; 139 $b2 = $chunk[3]; 140 141 $dest .= 142 static::encode6Bits( $b0 >> 2 ) . 143 static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . 144 static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) . 145 static::encode6Bits( $b2 & 63); 146 } 147 // The last chunk, which may have padding: 148 if ($i < $srcLen) { 149 /** @var array<int, int> $chunk */ 150 $chunk = unpack('C*', substr($src, $i, $srcLen - $i)); 151 $b0 = $chunk[1]; 152 if ($i + 1 < $srcLen) { 153 $b1 = $chunk[2]; 154 $dest .= 155 static::encode6Bits($b0 >> 2) . 156 static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . 157 static::encode6Bits(($b1 << 2) & 63); 158 if ($pad) { 159 $dest .= '='; 160 } 161 } else { 162 $dest .= 163 static::encode6Bits( $b0 >> 2) . 164 static::encode6Bits(($b0 << 4) & 63); 165 if ($pad) { 166 $dest .= '=='; 167 } 168 } 169 } 170 return $dest; 171 } 172 173 /** 174 * decode from base64 into binary 175 * 176 * Base64 character set "./[A-Z][a-z][0-9]" 177 * 178 * @param string $encodedString 179 * @param bool $strictPadding 180 * @return string 181 * 182 * @throws RangeException 183 * @throws TypeError 184 */ 185 #[Override] 186 public static function decode( 187 #[SensitiveParameter] 188 string $encodedString, 189 bool $strictPadding = false 190 ): string { 191 // Remove padding 192 $srcLen = strlen($encodedString); 193 if ($srcLen === 0) { 194 return ''; 195 } 196 197 if ($strictPadding) { 198 if (($srcLen & 3) === 0) { 199 if ($encodedString[$srcLen - 1] === '=') { 200 $srcLen--; 201 if ($encodedString[$srcLen - 1] === '=') { 202 $srcLen--; 203 } 204 } 205 } 206 if (($srcLen & 3) === 1) { 207 throw new RangeException( 208 'Incorrect padding' 209 ); 210 } 211 if ($encodedString[$srcLen - 1] === '=') { 212 throw new RangeException( 213 'Incorrect padding' 214 ); 215 } 216 if (extension_loaded('sodium')) { 217 $variant = match(static::class) { 218 Base64::class => SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING, 219 Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, 220 default => 0, 221 }; 222 if ($variant > 0) { 223 try { 224 return sodium_base642bin(substr($encodedString, 0, $srcLen), $variant); 225 } catch (SodiumException $ex) { 226 throw new RangeException($ex->getMessage(), $ex->getCode(), $ex); 227 } 228 } 229 } 230 } else { 231 // Just remove all padding. 232 $encodedString = rtrim($encodedString, '='); 233 $srcLen = strlen($encodedString); 234 } 235 236 $err = 0; 237 $dest = ''; 238 // Main loop (no padding): 239 for ($i = 0; $i + 4 <= $srcLen; $i += 4) { 240 /** @var array<int, int> $chunk */ 241 $chunk = unpack('C*', substr($encodedString, $i, 4)); 242 $c0 = static::decode6Bits($chunk[1]); 243 $c1 = static::decode6Bits($chunk[2]); 244 $c2 = static::decode6Bits($chunk[3]); 245 $c3 = static::decode6Bits($chunk[4]); 246 247 $dest .= pack( 248 'CCC', 249 ((($c0 << 2) | ($c1 >> 4)) & 0xff), 250 ((($c1 << 4) | ($c2 >> 2)) & 0xff), 251 ((($c2 << 6) | $c3 ) & 0xff) 252 ); 253 $err |= ($c0 | $c1 | $c2 | $c3) >> 8; 254 } 255 // The last chunk, which may have padding: 256 if ($i < $srcLen) { 257 /** @var array<int, int> $chunk */ 258 $chunk = unpack('C*', substr($encodedString, $i, $srcLen - $i)); 259 $c0 = static::decode6Bits($chunk[1]); 260 261 if ($i + 2 < $srcLen) { 262 $c1 = static::decode6Bits($chunk[2]); 263 $c2 = static::decode6Bits($chunk[3]); 264 $dest .= pack( 265 'CC', 266 ((($c0 << 2) | ($c1 >> 4)) & 0xff), 267 ((($c1 << 4) | ($c2 >> 2)) & 0xff) 268 ); 269 $err |= ($c0 | $c1 | $c2) >> 8; 270 if ($strictPadding) { 271 $err |= ($c2 << 6) & 0xff; 272 } 273 } elseif ($i + 1 < $srcLen) { 274 $c1 = static::decode6Bits($chunk[2]); 275 $dest .= pack( 276 'C', 277 ((($c0 << 2) | ($c1 >> 4)) & 0xff) 278 ); 279 $err |= ($c0 | $c1) >> 8; 280 if ($strictPadding) { 281 $err |= ($c1 << 4) & 0xff; 282 } 283 } elseif ($strictPadding) { 284 $err |= 1; 285 } 286 } 287 $check = ($err === 0); 288 if (!$check) { 289 throw new RangeException( 290 'Base64::decode() only expects characters in the correct base64 alphabet' 291 ); 292 } 293 return $dest; 294 } 295 296 /** 297 * @param string $encodedString 298 * @return string 299 * @api 300 */ 301 public static function decodeNoPadding( 302 #[SensitiveParameter] 303 string $encodedString 304 ): string { 305 $srcLen = strlen($encodedString); 306 if ($srcLen === 0) { 307 return ''; 308 } 309 if (($srcLen & 3) === 0) { 310 // If $strLen is not zero, and it is divisible by 4, then it's at least 4. 311 if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') { 312 throw new InvalidArgumentException( 313 "decodeNoPadding() doesn't tolerate padding" 314 ); 315 } 316 } 317 return static::decode( 318 $encodedString, 319 true 320 ); 321 } 322 323 /** 324 * Uses bitwise operators instead of table-lookups to turn 6-bit integers 325 * into 8-bit integers. 326 * 327 * Base64 character set: 328 * [A-Z] [a-z] [0-9] + / 329 * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f 330 * 331 * @param int $src 332 * @return int 333 */ 334 protected static function decode6Bits(int $src): int 335 { 336 $ret = -1; 337 338 // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 339 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); 340 341 // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 342 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); 343 344 // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 345 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); 346 347 // if ($src == 0x2b) $ret += 62 + 1; 348 $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63; 349 350 // if ($src == 0x2f) ret += 63 + 1; 351 $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64; 352 353 return $ret; 354 } 355 356 /** 357 * Uses bitwise operators instead of table-lookups to turn 8-bit integers 358 * into 6-bit integers. 359 * 360 * @param int $src 361 * @return string 362 */ 363 protected static function encode6Bits(int $src): string 364 { 365 $diff = 0x41; 366 367 // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 368 $diff += ((25 - $src) >> 8) & 6; 369 370 // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 371 $diff -= ((51 - $src) >> 8) & 75; 372 373 // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15 374 $diff -= ((61 - $src) >> 8) & 15; 375 376 // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3 377 $diff += ((62 - $src) >> 8) & 3; 378 379 return pack('C', $src + $diff); 380 } 381} 382