PluginProbe
Media Cloud Sync / 1.2.2
Media Cloud Sync v1.2.2
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / sdk / google / firebase / php-jwt / src / JWT.php

JWT.php in Media Cloud Sync 1.2.2, at includes/sdk/google/firebase/php-jwt/src/JWT.php

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