22
33namespace Firebase \JWT ;
44
5+ use ArrayAccess ;
56use DomainException ;
67use Exception ;
78use InvalidArgumentException ;
@@ -58,11 +59,13 @@ class JWT
5859 * Decodes a JWT string into a PHP object.
5960 *
6061 * @param string $jwt The JWT
61- * @param string |array|resource $key The key, or map of keys .
62+ * @param Key |array<Key> $keyOrKeyArray The Key or array of Key objects .
6263 * If the algorithm used is asymmetric, this is the public key
63- * @param array $allowed_algs List of supported verification algorithms
64+ * Each Key object contains an algorithm and matching key.
6465 * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
6566 * 'HS512', 'RS256', 'RS384', and 'RS512'
67+ * @param array $allowed_algs [DEPRECATED] List of supported verification algorithms. Only
68+ * should be used for backwards compatibility.
6669 *
6770 * @return object The JWT's payload as a PHP object
6871 *
@@ -76,11 +79,11 @@ class JWT
7679 * @uses jsonDecode
7780 * @uses urlsafeB64Decode
7881 */
79- public static function decode ($ jwt , $ key , array $ allowed_algs = array ())
82+ public static function decode ($ jwt , $ keyOrKeyArray , array $ allowed_algs = array ())
8083 {
8184 $ timestamp = \is_null (static ::$ timestamp ) ? \time () : static ::$ timestamp ;
8285
83- if (empty ($ key )) {
86+ if (empty ($ keyOrKeyArray )) {
8487 throw new InvalidArgumentException ('Key may not be empty ' );
8588 }
8689 $ tks = \explode ('. ' , $ jwt );
@@ -103,27 +106,32 @@ public static function decode($jwt, $key, array $allowed_algs = array())
103106 if (empty (static ::$ supported_algs [$ header ->alg ])) {
104107 throw new UnexpectedValueException ('Algorithm not supported ' );
105108 }
106- if (!\in_array ($ header ->alg , $ allowed_algs )) {
107- throw new UnexpectedValueException ('Algorithm not allowed ' );
109+
110+ list ($ keyMaterial , $ algorithm ) = self ::getKeyMaterialAndAlgorithm (
111+ $ keyOrKeyArray ,
112+ empty ($ header ->kid ) ? null : $ header ->kid
113+ );
114+
115+ if (empty ($ algorithm )) {
116+ // Use deprecated "allowed_algs" to determine if the algorithm is supported.
117+ // This opens up the possibility of an attack in some implementations.
118+ // @see https://github.com/firebase/php-jwt/issues/351
119+ if (!\in_array ($ header ->alg , $ allowed_algs )) {
120+ throw new UnexpectedValueException ('Algorithm not allowed ' );
121+ }
122+ } else {
123+ // Check the algorithm
124+ if (!self ::constantTimeEquals ($ algorithm , $ header ->alg )) {
125+ // See issue #351
126+ throw new UnexpectedValueException ('Incorrect key for this algorithm ' );
127+ }
108128 }
109129 if ($ header ->alg === 'ES256 ' || $ header ->alg === 'ES384 ' ) {
110130 // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
111131 $ sig = self ::signatureToDER ($ sig );
112132 }
113133
114- if (\is_array ($ key ) || $ key instanceof \ArrayAccess) {
115- if (isset ($ header ->kid )) {
116- if (!isset ($ key [$ header ->kid ])) {
117- throw new UnexpectedValueException ('"kid" invalid, unable to lookup correct key ' );
118- }
119- $ key = $ key [$ header ->kid ];
120- } else {
121- throw new UnexpectedValueException ('"kid" empty, unable to lookup correct key ' );
122- }
123- }
124-
125- // Check the signature
126- if (!static ::verify ("$ headb64. $ bodyb64 " , $ sig , $ key , $ header ->alg )) {
134+ if (!static ::verify ("$ headb64. $ bodyb64 " , $ sig , $ keyMaterial , $ header ->alg )) {
127135 throw new SignatureInvalidException ('Signature verification failed ' );
128136 }
129137
@@ -285,18 +293,7 @@ private static function verify($msg, $signature, $key, $alg)
285293 case 'hash_hmac ' :
286294 default :
287295 $ hash = \hash_hmac ($ algorithm , $ msg , $ key , true );
288- if (\function_exists ('hash_equals ' )) {
289- return \hash_equals ($ signature , $ hash );
290- }
291- $ len = \min (static ::safeStrlen ($ signature ), static ::safeStrlen ($ hash ));
292-
293- $ status = 0 ;
294- for ($ i = 0 ; $ i < $ len ; $ i ++) {
295- $ status |= (\ord ($ signature [$ i ]) ^ \ord ($ hash [$ i ]));
296- }
297- $ status |= (static ::safeStrlen ($ signature ) ^ static ::safeStrlen ($ hash ));
298-
299- return ($ status === 0 );
296+ return self ::constantTimeEquals ($ signature , $ hash );
300297 }
301298 }
302299
@@ -384,6 +381,69 @@ public static function urlsafeB64Encode($input)
384381 return \str_replace ('= ' , '' , \strtr (\base64_encode ($ input ), '+/ ' , '-_ ' ));
385382 }
386383
384+
385+ /**
386+ * Determine if an algorithm has been provided for each Key
387+ *
388+ * @param string|array $keyOrKeyArray
389+ * @param string|null $kid
390+ *
391+ * @return an array containing the keyMaterial and algorithm
392+ */
393+ private static function getKeyMaterialAndAlgorithm ($ keyOrKeyArray , $ kid = null )
394+ {
395+ if (is_string ($ keyOrKeyArray )) {
396+ return array ($ keyOrKeyArray , null );
397+ }
398+
399+ if ($ keyOrKeyArray instanceof Key) {
400+ return array ($ keyOrKeyArray ->getKeyMaterial (), $ keyOrKeyArray ->getAlgorithm ());
401+ }
402+
403+ if (is_array ($ keyOrKeyArray ) || $ keyOrKeyArray instanceof ArrayAccess) {
404+ if (!isset ($ kid )) {
405+ throw new UnexpectedValueException ('"kid" empty, unable to lookup correct key ' );
406+ }
407+ if (!isset ($ keyOrKeyArray [$ kid ])) {
408+ throw new UnexpectedValueException ('"kid" invalid, unable to lookup correct key ' );
409+ }
410+
411+ $ key = $ keyOrKeyArray [$ kid ];
412+
413+ if ($ key instanceof Key) {
414+ return array ($ key ->getKeyMaterial (), $ key ->getAlgorithm ());
415+ }
416+
417+ return array ($ key , null );
418+ }
419+
420+ throw new UnexpectedValueException (
421+ '$keyOrKeyArray must be a string key, an array of string keys, '
422+ . 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys '
423+ );
424+ }
425+
426+ /**
427+ * @param string $left
428+ * @param string $right
429+ * @return bool
430+ */
431+ public static function constantTimeEquals ($ left , $ right )
432+ {
433+ if (\function_exists ('hash_equals ' )) {
434+ return \hash_equals ($ left , $ right );
435+ }
436+ $ len = \min (static ::safeStrlen ($ left ), static ::safeStrlen ($ right ));
437+
438+ $ status = 0 ;
439+ for ($ i = 0 ; $ i < $ len ; $ i ++) {
440+ $ status |= (\ord ($ left [$ i ]) ^ \ord ($ right [$ i ]));
441+ }
442+ $ status |= (static ::safeStrlen ($ left ) ^ static ::safeStrlen ($ right ));
443+
444+ return ($ status === 0 );
445+ }
446+
387447 /**
388448 * Helper method to create a JSON error.
389449 *
0 commit comments