vanilla /
garden
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * @author Todd Burry <[email protected]> |
||
| 4 | * @copyright 2009-2014 Vanilla Forums Inc. |
||
| 5 | * @license MIT |
||
| 6 | */ |
||
| 7 | |||
| 8 | namespace Garden; |
||
| 9 | |||
| 10 | |||
| 11 | /** |
||
| 12 | * A class that provides functionality for encrypting and signing strings and then decoding those secure strings. |
||
| 13 | */ |
||
| 14 | class SecureString { |
||
| 15 | const SEP = '.'; |
||
| 16 | const EOS = '-'; |
||
| 17 | const STRICT = 'strict'; |
||
| 18 | |||
| 19 | protected $timestampExpiry; |
||
| 20 | |||
| 21 | /** |
||
| 22 | * Initialize an instance of the {@link SecureString} class. |
||
| 23 | */ |
||
| 24 | 440 | public function __construct() { |
|
| 25 | 440 | $this->timestampExpiry = strtotime('1 day', 0); |
|
| 26 | 440 | } |
|
| 27 | |||
| 28 | /** |
||
| 29 | * Get or set the timestamp expiry in seconds. |
||
| 30 | * |
||
| 31 | * @param int $value Set a new expiry value in seconds. |
||
| 32 | * @return int Returns the current timestamp expiry. |
||
| 33 | */ |
||
| 34 | 1 | public function timestampExpiry($value = 0) { |
|
| 35 | 1 | if ($value) { |
|
| 36 | 1 | $this->timestampExpiry = $value; |
|
| 37 | 1 | } |
|
| 38 | 1 | return $this->timestampExpiry; |
|
| 39 | } |
||
| 40 | |||
| 41 | /** |
||
| 42 | * Encode a string using a secure specification. |
||
| 43 | * |
||
| 44 | * @param mixed $data The data to encode. This must be data that supports json serialization. |
||
| 45 | * @param array $spec An array specifying how the string should be encoded. |
||
| 46 | * The array should be in the form ['spec1' => 'password', 'spec2' => 'password']. |
||
| 47 | * @param bool $throw Whether or not to throw an exception on error. |
||
| 48 | * @return string|null Returns the encoded string or null if there is an error. |
||
| 49 | */ |
||
| 50 | 438 | public function encode($data, array $spec, $throw = false) { |
|
| 51 | 438 | $str = json_encode($data, JSON_UNESCAPED_SLASHES); |
|
| 52 | |||
| 53 | 438 | $first = true; |
|
| 54 | 438 | foreach ($spec as $name => $password) { |
|
| 55 | 438 | if ($name === self::STRICT) { |
|
| 56 | // Strict is just an option so continue. |
||
| 57 | 1 | continue; |
|
| 58 | } |
||
| 59 | |||
| 60 | 438 | $supported = $this->supportedInfo($name, $throw); |
|
| 61 | 438 | if ($supported === null) { |
|
| 62 | 1 | return null; |
|
| 63 | } |
||
| 64 | |||
| 65 | 437 | list($encode,, $method, $encodeFirst) = $supported; |
|
| 66 | |||
| 67 | 437 | if ($first) { |
|
| 68 | 437 | if ($encodeFirst) { |
|
| 69 | 195 | $str = static::base64urlEncode($str); |
|
| 70 | 195 | } |
|
| 71 | 437 | $this->pushString($str, self::EOS); |
|
| 72 | 437 | } |
|
| 73 | |||
| 74 | View Code Duplication | switch ($encode) { |
|
| 75 | 437 | case 'encrypt': |
|
| 76 | 290 | $str = $this->encrypt($str, $method, $password, '', $throw); |
|
| 77 | 290 | break; |
|
| 78 | 292 | case 'hmac': |
|
| 79 | 292 | $str = $this->hmac($str, $method, $password, 0, $throw); |
|
| 80 | 292 | break; |
|
| 81 | default: |
||
| 82 | return $this->exception($throw, "Invalid method $encode.", 500); |
||
|
0 ignored issues
–
show
Bug
Compatibility
introduced
by
Loading history...
|
|||
| 83 | } |
||
| 84 | // Return on error. |
||
| 85 | 437 | if (!$str) { |
|
| 86 | return null; |
||
| 87 | } |
||
| 88 | |||
| 89 | 437 | $this->pushString($str, $name); |
|
| 90 | |||
| 91 | 437 | $first = false; |
|
| 92 | 437 | } |
|
| 93 | |||
| 94 | 437 | return $str; |
|
| 95 | } |
||
| 96 | |||
| 97 | /** |
||
| 98 | * Decode a string that was encoded using {@link SecureString::encode()}. |
||
| 99 | * |
||
| 100 | * @param string $str The encoded string. |
||
| 101 | * @param array $spec An array specifying how the string should be encoded. |
||
| 102 | * @param bool $throw Whether or not to throw an exception on error. |
||
| 103 | * @return string|null Returns the decoded string or null of there was a problem. |
||
| 104 | */ |
||
| 105 | 439 | public function decode($str, array $spec, $throw = false) { |
|
| 106 | 439 | $decodeFirst = false; |
|
| 107 | 439 | $wstr = $str; // wstr means working string |
|
| 108 | 439 | $used = []; |
|
| 109 | |||
| 110 | 439 | $strict = true; |
|
| 111 | 439 | if (array_key_exists(self::STRICT, $spec)) { |
|
| 112 | 2 | $strict = (bool)$spec[self::STRICT]; |
|
| 113 | 2 | unset($spec[self::STRICT]); |
|
| 114 | 2 | } |
|
| 115 | |||
| 116 | 439 | while ($token = $this->popString($wstr)) { |
|
| 117 | 439 | if ($token === self::EOS) { |
|
| 118 | 147 | if ($decodeFirst) { |
|
| 119 | 65 | $wstr = static::base64urlDecode($wstr); |
|
| 120 | 65 | } |
|
| 121 | 147 | break; |
|
| 122 | } |
||
| 123 | |||
| 124 | try { |
||
| 125 | 438 | $supported = $this->supportedInfo($token, true); |
|
| 126 | 438 | } catch (\Exception $ex) { |
|
| 127 | 2 | return $this->exception($throw, $ex->getMessage(), 403); |
|
|
0 ignored issues
–
show
|
|||
| 128 | } |
||
| 129 | |||
| 130 | 437 | if (!isset($spec[$token])) { |
|
| 131 | 144 | return $this->exception($throw, "You did not provide a password for $token.", 403); |
|
|
0 ignored issues
–
show
|
|||
| 132 | } |
||
| 133 | 293 | $password = $spec[$token]; |
|
| 134 | 293 | array_unshift($used, $token); |
|
| 135 | |||
| 136 | 293 | list(, $decode, $method, $decodeFirst) = $supported; |
|
| 137 | |||
| 138 | View Code Duplication | switch ($decode) { |
|
| 139 | 293 | case 'decrypt': |
|
| 140 | 162 | $wstr = $this->decrypt($wstr, $method, $password, $throw); |
|
| 141 | 162 | break; |
|
| 142 | 180 | case 'verifyHmac': |
|
| 143 | 180 | $wstr = $this->verifyHmac($wstr, $method, $password, $throw); |
|
| 144 | 180 | break; |
|
| 145 | default: |
||
| 146 | return $this->exception($throw, "Invalid method $decode.", 500); |
||
|
0 ignored issues
–
show
|
|||
| 147 | } |
||
| 148 | |||
| 149 | 293 | if ($wstr === null) { |
|
| 150 | 146 | return null; |
|
| 151 | } |
||
| 152 | 147 | } |
|
| 153 | |||
| 154 | // Make sure that the string was secured at all. |
||
| 155 | 147 | if ($strict && array_keys($spec) != $used) { |
|
| 156 | 1 | return $this->exception($throw, 'The string is not fully secure.', 403); |
|
|
0 ignored issues
–
show
|
|||
| 157 | 147 | } elseif (empty($used)) { |
|
| 158 | 1 | return $this->exception($throw, 'The string is not secure.', 403); |
|
|
0 ignored issues
–
show
|
|||
| 159 | } |
||
| 160 | |||
| 161 | 146 | $data = json_decode($wstr, true); |
|
| 162 | 146 | if ($data === null) { |
|
| 163 | return $this->exception($throw, 'The final string is not valid json.', 403); |
||
|
0 ignored issues
–
show
|
|||
| 164 | } |
||
| 165 | |||
| 166 | 146 | return $data; |
|
| 167 | } |
||
| 168 | |||
| 169 | /** |
||
| 170 | * Generate a random string suitable for use as an encryption or signature key. |
||
| 171 | * |
||
| 172 | * @param int $len The number of characters in the key. |
||
| 173 | * @return string Returns a base64url encoded string representing the random key. |
||
| 174 | */ |
||
| 175 | 2 | public static function generateRandomKey($len = 32) { |
|
| 176 | 2 | $bytes = ceil($len * 3 / 4); |
|
| 177 | |||
| 178 | 2 | return substr(self::base64urlEncode(openssl_random_pseudo_bytes($bytes)), 0, $len); |
|
| 179 | } |
||
| 180 | |||
| 181 | /** |
||
| 182 | * Base64 Encode a string, but make it suitable to be passed in a url. |
||
| 183 | * |
||
| 184 | * @param string $str The string to encode. |
||
| 185 | * @return string The encoded string. |
||
| 186 | */ |
||
| 187 | 437 | protected static function base64urlEncode($str) { |
|
| 188 | 437 | return trim(strtr(base64_encode($str), '+/', '-_'), '='); |
|
| 189 | } |
||
| 190 | |||
| 191 | /** |
||
| 192 | * Decode a string that was encoded using base64UrlEncode(). |
||
| 193 | * |
||
| 194 | * @param string $str The encoded string. |
||
| 195 | * @return string The decoded string. |
||
| 196 | */ |
||
| 197 | 293 | protected static function base64urlDecode($str) { |
|
| 198 | 293 | return base64_decode(strtr($str, '-_', '+/')); |
|
| 199 | } |
||
| 200 | |||
| 201 | /** |
||
| 202 | * Encrypt a string with {@link openssl_encrypt()}. |
||
| 203 | * |
||
| 204 | * @param string $str The string to encrypt. |
||
| 205 | * @param string $method The encryption cipher. |
||
| 206 | * @param string $password The encryption password. |
||
| 207 | * @param string $iv The input vector. |
||
| 208 | * @param bool $throw Whether or not to throw an exception on error. |
||
| 209 | * @return string Returns the encrypted string. |
||
| 210 | */ |
||
| 211 | 290 | View Code Duplication | protected function encrypt($str, $method, $password, $iv = '', $throw = false) { |
| 212 | 290 | if ($iv === '') { |
|
| 213 | 290 | $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method)); |
|
| 214 | 290 | } |
|
| 215 | // Encrypt the string. |
||
| 216 | 290 | $encrypted = openssl_encrypt($str, $method, $password, OPENSSL_RAW_DATA, $iv); |
|
| 217 | |||
| 218 | 290 | if ($encrypted === false) { |
|
| 219 | return $this->exception($throw, "Error encrypting the string.", 400); |
||
|
0 ignored issues
–
show
|
|||
| 220 | } |
||
| 221 | |||
| 222 | 290 | $str = static::base64urlEncode($encrypted); |
|
| 223 | 290 | $this->pushString($str, static::base64urlEncode($iv)); |
|
| 224 | |||
| 225 | 290 | return $str; |
|
| 226 | } |
||
| 227 | |||
| 228 | /** |
||
| 229 | * Decrypt a string with {@link openssl_decrypt()}. |
||
| 230 | * |
||
| 231 | * @param string $str The base64 url encoded encrypted string. |
||
| 232 | * @param string $method The encryption cipher. |
||
| 233 | * @param string $password The password to decyrpt the string. |
||
| 234 | * @param bool $throw Whether or not to decode the string on exception. |
||
| 235 | * @return string|null Returns the decrypted string or null on error. |
||
| 236 | */ |
||
| 237 | 162 | protected function decrypt($str, $method, $password, $throw = false) { |
|
| 238 | 162 | $iv = static::base64urlDecode($this->popString($str)); |
|
| 239 | 162 | $encrypted = static::base64urlDecode($this->popString($str)); |
|
| 240 | |||
| 241 | try { |
||
| 242 | 162 | $decrypted = openssl_decrypt($encrypted, $method, $password, true, $iv); |
|
| 243 | 162 | } catch (\Exception $ex) { |
|
| 244 | return $this->exception($throw, "Error decrypting the string.", 403); |
||
| 245 | } |
||
| 246 | |||
| 247 | 162 | if ($decrypted === false) { |
|
| 248 | 64 | return $this->exception($throw, "Error decrypting the string.", 403); |
|
| 249 | } |
||
| 250 | |||
| 251 | 98 | return $decrypted; |
|
| 252 | } |
||
| 253 | |||
| 254 | /** |
||
| 255 | * Get information about a supported spec name. |
||
| 256 | * |
||
| 257 | * @param string $name The name of the spec. |
||
| 258 | * @param bool $throw Whether or not throw an exception on error. |
||
| 259 | * @return array|null Returns an array in the form [encode, decode, method, encodeFirst]. |
||
| 260 | * Returns null on error. |
||
| 261 | */ |
||
| 262 | 439 | protected function supportedInfo($name, $throw = false) { |
|
| 263 | switch ($name) { |
||
| 264 | 439 | case 'aes128': |
|
| 265 | 439 | case 'aes256': |
|
| 266 | 291 | $cipher = 'aes-'.substr($name, 3).'-cbc'; |
|
| 267 | 291 | return ['encrypt', 'decrypt', $cipher, false]; |
|
| 268 | 295 | case 'hsha1': |
|
| 269 | 295 | case 'hsha256': |
|
| 270 | 292 | $hash = substr($name, 1); |
|
| 271 | 292 | return ['hmac', 'verifyHmac', $hash, true]; |
|
| 272 | } |
||
| 273 | 3 | return $this->exception($throw, "Spec $name not supported.", 400); |
|
| 274 | } |
||
| 275 | |||
| 276 | /** |
||
| 277 | * Sign a string with hmac and a hash method. |
||
| 278 | * |
||
| 279 | * @param string $str The string to sign. |
||
| 280 | * @param string $method The hash method used to sign the string. |
||
| 281 | * @param string $password The password used to hmac hash the string with. |
||
| 282 | * @param int $timestamp The timestamp used to sign the string with or 0 to use the current time. |
||
| 283 | * @param bool $throw Whether or not the throw an exception on error. |
||
| 284 | * @return string Returns the string with signing information or null on error. |
||
| 285 | */ |
||
| 286 | 292 | View Code Duplication | protected function hmac($str, $method, $password, $timestamp = 0, $throw = false) { |
| 287 | 292 | if ($timestamp === 0) { |
|
| 288 | 292 | $timestamp = time(); |
|
| 289 | 292 | } |
|
| 290 | // Add the timestamp to the string. |
||
| 291 | 292 | static::pushString($str, $timestamp); |
|
| 292 | |||
| 293 | // Sign the string. |
||
| 294 | 292 | $signature = hash_hmac($method, $str, $password, true); |
|
| 295 | 292 | if ($signature === false) { |
|
| 296 | return $this->exception($throw, "Invalid hash method $method.", 400); |
||
|
0 ignored issues
–
show
|
|||
| 297 | } |
||
| 298 | |||
| 299 | // Add the signature to the string. |
||
| 300 | 292 | static::pushString($str, static::base64urlEncode($signature)); |
|
| 301 | |||
| 302 | 292 | return $str; |
|
| 303 | } |
||
| 304 | |||
| 305 | /** |
||
| 306 | * Verify the signature on a secure string. |
||
| 307 | * |
||
| 308 | * @param string $str The string to verify. |
||
| 309 | * @param string $method The hashing algorithm that the string was signed with. |
||
| 310 | * @param string $password The password used to sign the string. |
||
| 311 | * @param bool $throw Whether or not to throw an exeptio on error. |
||
| 312 | * @return bool Returns the string without the signing information or null on error. |
||
| 313 | */ |
||
| 314 | 180 | protected function verifyHmac($str, $method, $password, $throw = false) { |
|
| 315 | // Grab the signature from the string. |
||
| 316 | 180 | $signature = $this->popString($str); |
|
| 317 | 180 | if (!$signature) { |
|
|
0 ignored issues
–
show
The expression
$signature of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
Loading history...
|
|||
| 318 | return $this->exception($throw, "The signature is missing.", 403); |
||
| 319 | } |
||
| 320 | 180 | $signature = static::base64urlDecode($signature); |
|
| 321 | |||
| 322 | // Recalculate the signature to compare. |
||
| 323 | 180 | $calcSignature = hash_hmac($method, $str, $password, true); |
|
| 324 | |||
| 325 | 180 | if (strlen($signature) !== strlen($calcSignature)) { |
|
| 326 | return $this->exception($throw, "The signature is invalid.", 403); |
||
| 327 | } |
||
| 328 | |||
| 329 | // Do a double hmac comparison to prevent timing attacks. |
||
| 330 | // https://www.isecpartners.com/blog/2011/february/double-hmac-verification.aspx |
||
| 331 | 180 | $dblSignature = hash_hmac($method, $signature, $password, true); |
|
| 332 | 180 | $dblCalcSignature = hash_hmac($method, $calcSignature, $password, true); |
|
| 333 | |||
| 334 | 180 | if ($dblSignature !== $dblCalcSignature) { |
|
| 335 | 81 | return $this->exception($throw, "The signature is invalid.", 403); |
|
| 336 | } |
||
| 337 | |||
| 338 | // Grab the timestamp and verify it. |
||
| 339 | 99 | $timestamp = $this->popString($str); |
|
| 340 | 99 | if (!$this->verifyTimestamp($timestamp, $throw)) { |
|
| 341 | 1 | return null; |
|
| 342 | } |
||
| 343 | |||
| 344 | 98 | return $str; |
|
| 345 | } |
||
| 346 | |||
| 347 | /** |
||
| 348 | * Verify that a timestamp hasn't expired. |
||
| 349 | * |
||
| 350 | * @param string $timestamp The unix timestamp to verify. |
||
| 351 | * @param bool $throw Whether or not to throw an exception on error. |
||
| 352 | * @return bool Returns true if the timestamp is valid or false otherwise. |
||
| 353 | */ |
||
| 354 | 99 | protected function verifyTimestamp($timestamp, $throw = false) { |
|
| 355 | 99 | if (!is_numeric($timestamp)) { |
|
| 356 | return (bool)$this->exception($throw, "Invalid timestamp.", 403); |
||
| 357 | } |
||
| 358 | 99 | $intTimestamp = (int)$timestamp; |
|
| 359 | 99 | $now = time(); |
|
| 360 | 99 | if ($intTimestamp + $this->timestampExpiry <= $now) { |
|
| 361 | 1 | return (bool)$this->exception($throw, "The timestamp has expired.", 403); |
|
| 362 | } |
||
| 363 | |||
| 364 | 98 | return true; |
|
| 365 | } |
||
| 366 | |||
| 367 | /** |
||
| 368 | * Pop a string off of the end of an encoded secure string. |
||
| 369 | * |
||
| 370 | * @param string &$str The main string to pop. |
||
| 371 | * @return string|null Returns the popped string or null if {@link $str} is empty. |
||
| 372 | */ |
||
| 373 | 439 | protected function popString(&$str) { |
|
| 374 | 439 | if ($str === '') { |
|
| 375 | return null; |
||
| 376 | } |
||
| 377 | |||
| 378 | 439 | $pos = strrpos($str, '.'); |
|
| 379 | 439 | if ($pos !== false) { |
|
| 380 | 439 | $result = substr($str, $pos + 1); |
|
| 381 | 439 | $str = substr($str, 0, $pos); |
|
| 382 | 439 | } else { |
|
| 383 | 162 | $result = $str; |
|
| 384 | 162 | $str = ''; |
|
| 385 | } |
||
| 386 | 439 | return $result; |
|
| 387 | } |
||
| 388 | |||
| 389 | /** |
||
| 390 | * Pushes a string on to the end of an encoded secure string. |
||
| 391 | * |
||
| 392 | * @param string &$str The main string to push to. |
||
| 393 | * @param string|array $item The string or array of strings to push on to the end of {@link $str}. |
||
| 394 | */ |
||
| 395 | 437 | protected function pushString(&$str, $item) { |
|
| 396 | 437 | if ($str) { |
|
| 397 | 437 | $str .= static::SEP; |
|
| 398 | 437 | } |
|
| 399 | 437 | $str .= implode(static::SEP, (array)$item); |
|
| 400 | 437 | } |
|
| 401 | |||
| 402 | /** |
||
| 403 | * Throw an exception or return false. |
||
| 404 | * |
||
| 405 | * @param bool $throw Whether or not to throw an exception. |
||
| 406 | * @param string $message The exception message. |
||
| 407 | * @param int $code The exception code. |
||
| 408 | * @return bool Returns false if {@link $throw} is false. |
||
| 409 | * @throws \Exception Throws an exception with {@link $message} and {@link $code}. |
||
| 410 | */ |
||
| 411 | 295 | protected function exception($throw, $message, $code) { |
|
| 412 | 295 | if ($throw) { |
|
| 413 | 295 | throw new \Exception($message, $code); |
|
| 414 | } |
||
| 415 | 294 | return null; |
|
| 416 | } |
||
| 417 | |||
| 418 | /** |
||
| 419 | * Twiddle a value in an encoded secure string to another value. |
||
| 420 | * |
||
| 421 | * This method is mainly for testing so that an invalid string can be created. |
||
| 422 | * |
||
| 423 | * @param string $string A valid cookie to twiddle. |
||
| 424 | * @param int $index The index of the new value. |
||
| 425 | * @param string $value The new value. This will be base64url encoded. |
||
| 426 | * @param bool $encode Whether or not to base64 url encode the value. |
||
| 427 | * @return string Returns the new encoded cookie. |
||
| 428 | */ |
||
| 429 | 2 | public function twiddle($string, $index, $value, $encode = false) { |
|
| 430 | 2 | $parts = explode(static::SEP, $string); |
|
| 431 | |||
| 432 | 2 | if ($encode) { |
|
| 433 | $value = static::base64urlEncode($value); |
||
| 434 | } |
||
| 435 | 2 | $parts[$index] = $value; |
|
| 436 | |||
| 437 | 2 | return implode(static::SEP, $parts); |
|
| 438 | } |
||
| 439 | } |
||
| 440 |