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