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