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