PluginProbe
WP-Stateless – Google Cloud Storage / 3.2.2
WP-Stateless – Google Cloud Storage v3.2.2
4.4.3 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.3.0 2.3.1 2.3.2 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 All 62 releases
wp-stateless / vendor / firebase / php-jwt / src / JWT.php

JWT.php in WP-Stateless – Google Cloud Storage 3.2.2, at vendor/firebase/php-jwt/src/JWT.php

631 lines 21.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Firebase\JWT;
4
5 use ArrayAccess;
6 use DomainException;
7 use Exception;
8 use InvalidArgumentException;
9 use OpenSSLAsymmetricKey;
10 use OpenSSLCertificate;
11 use TypeError;
12 use UnexpectedValueException;
13 use DateTime;
14 use stdClass;
15
16 /**
17 * JSON Web Token implementation, based on this spec:
18 * https://tools.ietf.org/html/rfc7519
19 *
20 * PHP version 5
21 *
22 * @category Authentication
23 * @package Authentication_JWT
24 * @author Neuman Vong <neuman@twilio.com>
25 * @author Anant Narayanan <anant@php.net>
26 * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
27 * @link https://github.com/firebase/php-jwt
28 */
29 class JWT
30 {
31 private const ASN1_INTEGER = 0x02;
32 private const ASN1_SEQUENCE = 0x10;
33 private const ASN1_BIT_STRING = 0x03;
34
35 /**
36 * When checking nbf, iat or expiration times,
37 * we want to provide some extra leeway time to
38 * account for clock skew.
39 *
40 * @var int
41 */
42 public static $leeway = 0;
43
44 /**
45 * Allow the current timestamp to be specified.
46 * Useful for fixing a value within unit testing.
47 * Will default to PHP time() value if null.
48 *
49 * @var ?int
50 */
51 public static $timestamp = null;
52
53 /**
54 * @var array<string, string[]>
55 */
56 public static $supported_algs = [
57 'ES384' => ['openssl', 'SHA384'],
58 'ES256' => ['openssl', 'SHA256'],
59 'HS256' => ['hash_hmac', 'SHA256'],
60 'HS384' => ['hash_hmac', 'SHA384'],
61 'HS512' => ['hash_hmac', 'SHA512'],
62 'RS256' => ['openssl', 'SHA256'],
63 'RS384' => ['openssl', 'SHA384'],
64 'RS512' => ['openssl', 'SHA512'],
65 'EdDSA' => ['sodium_crypto', 'EdDSA'],
66 ];
67
68 /**
69 * Decodes a JWT string into a PHP object.
70 *
71 * @param string $jwt The JWT
72 * @param Key|array<string, Key> $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects.
73 * If the algorithm used is asymmetric, this is the public key
74 * Each Key object contains an algorithm and matching key.
75 * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
76 * 'HS512', 'RS256', 'RS384', and 'RS512'
77 *
78 * @return stdClass The JWT's payload as a PHP object
79 *
80 * @throws InvalidArgumentException Provided key/key-array was empty
81 * @throws DomainException Provided JWT is malformed
82 * @throws UnexpectedValueException Provided JWT was invalid
83 * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
84 * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
85 * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
86 * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
87 *
88 * @uses jsonDecode
89 * @uses urlsafeB64Decode
90 */
91 public static function decode(
92 string $jwt,
93 $keyOrKeyArray
94 ): stdClass {
95 // Validate JWT
96 $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
97
98 if (empty($keyOrKeyArray)) {
99 throw new InvalidArgumentException('Key may not be empty');
100 }
101 $tks = \explode('.', $jwt);
102 if (\count($tks) != 3) {
103 throw new UnexpectedValueException('Wrong number of segments');
104 }
105 list($headb64, $bodyb64, $cryptob64) = $tks;
106 $headerRaw = static::urlsafeB64Decode($headb64);
107 if (null === ($header = static::jsonDecode($headerRaw))) {
108 throw new UnexpectedValueException('Invalid header encoding');
109 }
110 $payloadRaw = static::urlsafeB64Decode($bodyb64);
111 if (null === ($payload = static::jsonDecode($payloadRaw))) {
112 throw new UnexpectedValueException('Invalid claims encoding');
113 }
114 if (is_array($payload)) {
115 // prevent PHP Fatal Error in edge-cases when payload is empty array
116 $payload = (object) $payload;
117 }
118 if (!$payload instanceof stdClass) {
119 throw new UnexpectedValueException('Payload must be a JSON object');
120 }
121 $sig = static::urlsafeB64Decode($cryptob64);
122 if (empty($header->alg)) {
123 throw new UnexpectedValueException('Empty algorithm');
124 }
125 if (empty(static::$supported_algs[$header->alg])) {
126 throw new UnexpectedValueException('Algorithm not supported');
127 }
128
129 $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
130
131 // Check the algorithm
132 if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
133 // See issue #351
134 throw new UnexpectedValueException('Incorrect key for this algorithm');
135 }
136 if ($header->alg === 'ES256' || $header->alg === 'ES384') {
137 // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
138 $sig = self::signatureToDER($sig);
139 }
140 if (!self::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) {
141 throw new SignatureInvalidException('Signature verification failed');
142 }
143
144 // Check the nbf if it is defined. This is the time that the
145 // token can actually be used. If it's not yet that time, abort.
146 if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
147 throw new BeforeValidException(
148 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf)
149 );
150 }
151
152 // Check that this token has been created before 'now'. This prevents
153 // using tokens that have been created for later use (and haven't
154 // correctly used the nbf claim).
155 if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
156 throw new BeforeValidException(
157 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat)
158 );
159 }
160
161 // Check if this token has expired.
162 if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
163 throw new ExpiredException('Expired token');
164 }
165
166 return $payload;
167 }
168
169 /**
170 * Converts and signs a PHP object or array into a JWT string.
171 *
172 * @param array<mixed> $payload PHP array
173 * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
174 * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
175 * 'HS512', 'RS256', 'RS384', and 'RS512'
176 * @param string $keyId
177 * @param array<string, string> $head An array with header elements to attach
178 *
179 * @return string A signed JWT
180 *
181 * @uses jsonEncode
182 * @uses urlsafeB64Encode
183 */
184 public static function encode(
185 array $payload,
186 $key,
187 string $alg,
188 string $keyId = null,
189 array $head = null
190 ): string {
191 $header = ['typ' => 'JWT', 'alg' => $alg];
192 if ($keyId !== null) {
193 $header['kid'] = $keyId;
194 }
195 if (isset($head) && \is_array($head)) {
196 $header = \array_merge($head, $header);
197 }
198 $segments = [];
199 $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
200 $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
201 $signing_input = \implode('.', $segments);
202
203 $signature = static::sign($signing_input, $key, $alg);
204 $segments[] = static::urlsafeB64Encode($signature);
205
206 return \implode('.', $segments);
207 }
208
209 /**
210 * Sign a string with a given key and algorithm.
211 *
212 * @param string $msg The message to sign
213 * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
214 * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
215 * 'HS512', 'RS256', 'RS384', and 'RS512'
216 *
217 * @return string An encrypted message
218 *
219 * @throws DomainException Unsupported algorithm or bad key was specified
220 */
221 public static function sign(
222 string $msg,
223 $key,
224 string $alg
225 ): string {
226 if (empty(static::$supported_algs[$alg])) {
227 throw new DomainException('Algorithm not supported');
228 }
229 list($function, $algorithm) = static::$supported_algs[$alg];
230 switch ($function) {
231 case 'hash_hmac':
232 if (!is_string($key)) {
233 throw new InvalidArgumentException('key must be a string when using hmac');
234 }
235 return \hash_hmac($algorithm, $msg, $key, true);
236 case 'openssl':
237 $signature = '';
238 $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line
239 if (!$success) {
240 throw new DomainException("OpenSSL unable to sign data");
241 }
242 if ($alg === 'ES256') {
243 $signature = self::signatureFromDER($signature, 256);
244 } elseif ($alg === 'ES384') {
245 $signature = self::signatureFromDER($signature, 384);
246 }
247 return $signature;
248 case 'sodium_crypto':
249 if (!function_exists('sodium_crypto_sign_detached')) {
250 throw new DomainException('libsodium is not available');
251 }
252 if (!is_string($key)) {
253 throw new InvalidArgumentException('key must be a string when using EdDSA');
254 }
255 try {
256 // The last non-empty line is used as the key.
257 $lines = array_filter(explode("\n", $key));
258 $key = base64_decode((string) end($lines));
259 return sodium_crypto_sign_detached($msg, $key);
260 } catch (Exception $e) {
261 throw new DomainException($e->getMessage(), 0, $e);
262 }
263 }
264
265 throw new DomainException('Algorithm not supported');
266 }
267
268 /**
269 * Verify a signature with the message, key and method. Not all methods
270 * are symmetric, so we must have a separate verify and sign method.
271 *
272 * @param string $msg The original message (header and body)
273 * @param string $signature The original signature
274 * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
275 * @param string $alg The algorithm
276 *
277 * @return bool
278 *
279 * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
280 */
281 private static function verify(
282 string $msg,
283 string $signature,
284 $keyMaterial,
285 string $alg
286 ): bool {
287 if (empty(static::$supported_algs[$alg])) {
288 throw new DomainException('Algorithm not supported');
289 }
290
291 list($function, $algorithm) = static::$supported_algs[$alg];
292 switch ($function) {
293 case 'openssl':
294 $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line
295 if ($success === 1) {
296 return true;
297 } elseif ($success === 0) {
298 return false;
299 }
300 // returns 1 on success, 0 on failure, -1 on error.
301 throw new DomainException(
302 'OpenSSL error: ' . \openssl_error_string()
303 );
304 case 'sodium_crypto':
305 if (!function_exists('sodium_crypto_sign_verify_detached')) {
306 throw new DomainException('libsodium is not available');
307 }
308 if (!is_string($keyMaterial)) {
309 throw new InvalidArgumentException('key must be a string when using EdDSA');
310 }
311 try {
312 // The last non-empty line is used as the key.
313 $lines = array_filter(explode("\n", $keyMaterial));
314 $key = base64_decode((string) end($lines));
315 return sodium_crypto_sign_verify_detached($signature, $msg, $key);
316 } catch (Exception $e) {
317 throw new DomainException($e->getMessage(), 0, $e);
318 }
319 case 'hash_hmac':
320 default:
321 if (!is_string($keyMaterial)) {
322 throw new InvalidArgumentException('key must be a string when using hmac');
323 }
324 $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
325 return self::constantTimeEquals($hash, $signature);
326 }
327 }
328
329 /**
330 * Decode a JSON string into a PHP object.
331 *
332 * @param string $input JSON string
333 *
334 * @return mixed The decoded JSON string
335 *
336 * @throws DomainException Provided string was invalid JSON
337 */
338 public static function jsonDecode(string $input)
339 {
340 $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
341
342 if ($errno = \json_last_error()) {
343 self::handleJsonError($errno);
344 } elseif ($obj === null && $input !== 'null') {
345 throw new DomainException('Null result with non-null input');
346 }
347 return $obj;
348 }
349
350 /**
351 * Encode a PHP array into a JSON string.
352 *
353 * @param array<mixed> $input A PHP array
354 *
355 * @return string JSON representation of the PHP array
356 *
357 * @throws DomainException Provided object could not be encoded to valid JSON
358 */
359 public static function jsonEncode(array $input): string
360 {
361 if (PHP_VERSION_ID >= 50400) {
362 $json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
363 } else {
364 // PHP 5.3 only
365 $json = \json_encode($input);
366 }
367 if ($errno = \json_last_error()) {
368 self::handleJsonError($errno);
369 } elseif ($json === 'null' && $input !== null) {
370 throw new DomainException('Null result with non-null input');
371 }
372 if ($json === false) {
373 throw new DomainException('Provided object could not be encoded to valid JSON');
374 }
375 return $json;
376 }
377
378 /**
379 * Decode a string with URL-safe Base64.
380 *
381 * @param string $input A Base64 encoded string
382 *
383 * @return string A decoded string
384 *
385 * @throws InvalidArgumentException invalid base64 characters
386 */
387 public static function urlsafeB64Decode(string $input): string
388 {
389 $remainder = \strlen($input) % 4;
390 if ($remainder) {
391 $padlen = 4 - $remainder;
392 $input .= \str_repeat('=', $padlen);
393 }
394 return \base64_decode(\strtr($input, '-_', '+/'));
395 }
396
397 /**
398 * Encode a string with URL-safe Base64.
399 *
400 * @param string $input The string you want encoded
401 *
402 * @return string The base64 encode of what you passed in
403 */
404 public static function urlsafeB64Encode(string $input): string
405 {
406 return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
407 }
408
409
410 /**
411 * Determine if an algorithm has been provided for each Key
412 *
413 * @param Key|array<string, Key> $keyOrKeyArray
414 * @param string|null $kid
415 *
416 * @throws UnexpectedValueException
417 *
418 * @return Key
419 */
420 private static function getKey(
421 $keyOrKeyArray,
422 ?string $kid
423 ): Key {
424 if ($keyOrKeyArray instanceof Key) {
425 return $keyOrKeyArray;
426 }
427
428 foreach ($keyOrKeyArray as $keyId => $key) {
429 if (!$key instanceof Key) {
430 throw new TypeError(
431 '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
432 . 'array of Firebase\JWT\Key keys'
433 );
434 }
435 }
436 if (!isset($kid)) {
437 throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
438 }
439 if (!isset($keyOrKeyArray[$kid])) {
440 throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
441 }
442
443 return $keyOrKeyArray[$kid];
444 }
445
446 /**
447 * @param string $left The string of known length to compare against
448 * @param string $right The user-supplied string
449 * @return bool
450 */
451 public static function constantTimeEquals(string $left, string $right): bool
452 {
453 if (\function_exists('hash_equals')) {
454 return \hash_equals($left, $right);
455 }
456 $len = \min(self::safeStrlen($left), self::safeStrlen($right));
457
458 $status = 0;
459 for ($i = 0; $i < $len; $i++) {
460 $status |= (\ord($left[$i]) ^ \ord($right[$i]));
461 }
462 $status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
463
464 return ($status === 0);
465 }
466
467 /**
468 * Helper method to create a JSON error.
469 *
470 * @param int $errno An error number from json_last_error()
471 *
472 * @throws DomainException
473 *
474 * @return void
475 */
476 private static function handleJsonError(int $errno): void
477 {
478 $messages = [
479 JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
480 JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
481 JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
482 JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
483 JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
484 ];
485 throw new DomainException(
486 isset($messages[$errno])
487 ? $messages[$errno]
488 : 'Unknown JSON error: ' . $errno
489 );
490 }
491
492 /**
493 * Get the number of bytes in cryptographic strings.
494 *
495 * @param string $str
496 *
497 * @return int
498 */
499 private static function safeStrlen(string $str): int
500 {
501 if (\function_exists('mb_strlen')) {
502 return \mb_strlen($str, '8bit');
503 }
504 return \strlen($str);
505 }
506
507 /**
508 * Convert an ECDSA signature to an ASN.1 DER sequence
509 *
510 * @param string $sig The ECDSA signature to convert
511 * @return string The encoded DER object
512 */
513 private static function signatureToDER(string $sig): string
514 {
515 // Separate the signature into r-value and s-value
516 $length = max(1, (int) (\strlen($sig) / 2));
517 list($r, $s) = \str_split($sig, $length > 0 ? $length : 1);
518
519 // Trim leading zeros
520 $r = \ltrim($r, "\x00");
521 $s = \ltrim($s, "\x00");
522
523 // Convert r-value and s-value from unsigned big-endian integers to
524 // signed two's complement
525 if (\ord($r[0]) > 0x7f) {
526 $r = "\x00" . $r;
527 }
528 if (\ord($s[0]) > 0x7f) {
529 $s = "\x00" . $s;
530 }
531
532 return self::encodeDER(
533 self::ASN1_SEQUENCE,
534 self::encodeDER(self::ASN1_INTEGER, $r) .
535 self::encodeDER(self::ASN1_INTEGER, $s)
536 );
537 }
538
539 /**
540 * Encodes a value into a DER object.
541 *
542 * @param int $type DER tag
543 * @param string $value the value to encode
544 *
545 * @return string the encoded object
546 */
547 private static function encodeDER(int $type, string $value): string
548 {
549 $tag_header = 0;
550 if ($type === self::ASN1_SEQUENCE) {
551 $tag_header |= 0x20;
552 }
553
554 // Type
555 $der = \chr($tag_header | $type);
556
557 // Length
558 $der .= \chr(\strlen($value));
559
560 return $der . $value;
561 }
562
563 /**
564 * Encodes signature from a DER object.
565 *
566 * @param string $der binary signature in DER format
567 * @param int $keySize the number of bits in the key
568 *
569 * @return string the signature
570 */
571 private static function signatureFromDER(string $der, int $keySize): string
572 {
573 // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
574 list($offset, $_) = self::readDER($der);
575 list($offset, $r) = self::readDER($der, $offset);
576 list($offset, $s) = self::readDER($der, $offset);
577
578 // Convert r-value and s-value from signed two's compliment to unsigned
579 // big-endian integers
580 $r = \ltrim($r, "\x00");
581 $s = \ltrim($s, "\x00");
582
583 // Pad out r and s so that they are $keySize bits long
584 $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
585 $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
586
587 return $r . $s;
588 }
589
590 /**
591 * Reads binary DER-encoded data and decodes into a single object
592 *
593 * @param string $der the binary data in DER format
594 * @param int $offset the offset of the data stream containing the object
595 * to decode
596 *
597 * @return array{int, string|null} the new offset and the decoded object
598 */
599 private static function readDER(string $der, int $offset = 0): array
600 {
601 $pos = $offset;
602 $size = \strlen($der);
603 $constructed = (\ord($der[$pos]) >> 5) & 0x01;
604 $type = \ord($der[$pos++]) & 0x1f;
605
606 // Length
607 $len = \ord($der[$pos++]);
608 if ($len & 0x80) {
609 $n = $len & 0x1f;
610 $len = 0;
611 while ($n-- && $pos < $size) {
612 $len = ($len << 8) | \ord($der[$pos++]);
613 }
614 }
615
616 // Value
617 if ($type == self::ASN1_BIT_STRING) {
618 $pos++; // Skip the first contents octet (padding indicator)
619 $data = \substr($der, $pos, $len - 1);
620 $pos += $len - 1;
621 } elseif (!$constructed) {
622 $data = \substr($der, $pos, $len);
623 $pos += $len;
624 } else {
625 $data = null;
626 }
627
628 return [$pos, $data];
629 }
630 }
631