PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-jwt/src/class-jwt.php +443 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,443 @@
1 +<?php
2 +/**
3 + * JSON Web Token implementation, based on this spec:
4 + * https://tools.ietf.org/html/rfc7519
5 + *
6 + * @package automattic/jetpack-jwt
7 + */
8 +
9 +namespace Automattic\Jetpack;
10 +
11 +use DomainException;
12 +use InvalidArgumentException;
13 +use UnexpectedValueException;
14 +
15 +/**
16 + * JSON Web Token implementation, based on this spec:
17 + * https://tools.ietf.org/html/rfc7519
18 + *
19 + * PHP version 5
20 + *
21 + * @category Authentication
22 + * @package Authentication_JWT
23 + * @author Neuman Vong <[email protected]>
24 + * @author Anant Narayanan <[email protected]>
25 + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
26 + * @link https://github.com/firebase/php-jwt
27 + */
28 +class JWT {
29 +
30 + const PACKAGE_VERSION = '0.3.0';
31 + /**
32 + * When checking nbf, iat or expiration times,
33 + * we want to provide some extra leeway time to
34 + * account for clock skew.
35 + *
36 + * @var int $leeway The leeway value.
37 + */
38 + public static $leeway = 0;
39 +
40 + /**
41 + * Allow the current timestamp to be specified.
42 + * Useful for fixing a value within unit testing.
43 + *
44 + * Will default to PHP time() value if null.
45 + *
46 + * @var string $timestamp The timestamp.
47 + */
48 + public static $timestamp = null;
49 +
50 + /**
51 + * Supported algorithms.
52 + *
53 + * @var array $supported_algs Supported algorithms.
54 + */
55 + public static $supported_algs = array(
56 + 'HS256' => array( 'hash_hmac', 'SHA256' ),
57 + 'HS512' => array( 'hash_hmac', 'SHA512' ),
58 + 'HS384' => array( 'hash_hmac', 'SHA384' ),
59 + 'RS256' => array( 'openssl', 'SHA256' ),
60 + 'RS384' => array( 'openssl', 'SHA384' ),
61 + 'RS512' => array( 'openssl', 'SHA512' ),
62 + );
63 +
64 + /**
65 + * Decodes a JWT string into a PHP object.
66 + *
67 + * @param string $jwt The JWT.
68 + * @param string|array $key The key, or map of keys.
69 + * If the algorithm used is asymmetric, this is the public key.
70 + * @param array $allowed_algs List of supported verification algorithms.
71 + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'.
72 + * @param bool $as_array Whether to return the result as an associative array.
73 + *
74 + * @return object|array The JWT's payload as a PHP object or array.
75 + *
76 + * @throws UnexpectedValueException Provided JWT was invalid.
77 + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed.
78 + * @throws InvalidArgumentException Provided JWT is trying to be used before it's eligible as defined by 'nbf'.
79 + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'.
80 + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim.
81 + *
82 + * @uses json_decode
83 + * @uses urlsafe_b64_decode
84 + */
85 + public static function decode( $jwt, $key, array $allowed_algs = array(), $as_array = false ) {
86 + $timestamp = static::$timestamp === null ? time() : static::$timestamp;
87 +
88 + if ( empty( $key ) ) {
89 + throw new InvalidArgumentException( 'Key may not be empty' );
90 + }
91 +
92 + $tks = explode( '.', $jwt );
93 + if ( count( $tks ) !== 3 ) {
94 + throw new UnexpectedValueException( 'Wrong number of segments' );
95 + }
96 +
97 + list( $headb64, $bodyb64, $cryptob64 ) = $tks;
98 +
99 + $header = static::json_decode( static::urlsafe_b64_decode( $headb64 ) );
100 + if ( null === $header ) {
101 + throw new UnexpectedValueException( 'Invalid header encoding' );
102 + }
103 +
104 + $payload = static::json_decode( static::urlsafe_b64_decode( $bodyb64 ), $as_array );
105 + if ( null === $payload ) {
106 + throw new UnexpectedValueException( 'Invalid claims encoding' );
107 + }
108 +
109 + $sig = static::urlsafe_b64_decode( $cryptob64 );
110 + if ( false === $sig ) {
111 + throw new UnexpectedValueException( 'Invalid signature encoding' );
112 + }
113 +
114 + if ( empty( $header->alg ) ) {
115 + throw new UnexpectedValueException( 'Empty algorithm' );
116 + }
117 +
118 + if ( empty( static::$supported_algs[ $header->alg ] ) ) {
119 + throw new UnexpectedValueException( 'Algorithm not supported' );
120 + }
121 +
122 + if ( ! in_array( $header->alg, $allowed_algs, true ) ) {
123 + throw new UnexpectedValueException( 'Algorithm not allowed' );
124 + }
125 +
126 + if ( is_array( $key ) || $key instanceof \ArrayAccess ) {
127 + if ( isset( $header->kid ) ) {
128 + if ( ! isset( $key[ $header->kid ] ) ) {
129 + throw new UnexpectedValueException( '"kid" invalid, unable to lookup correct key' );
130 + }
131 + $key = $key[ $header->kid ];
132 + } else {
133 + throw new UnexpectedValueException( '"kid" empty, unable to lookup correct key' );
134 + }
135 + }
136 +
137 + // Check the signature.
138 + if ( ! static::verify( "$headb64.$bodyb64", $sig, $key, $header->alg ) ) {
139 + throw new SignatureInvalidException( 'Signature verification failed' );
140 + }
141 +
142 + $nbf = ! $as_array && isset( $payload->nbf ) ? $payload->nbf : null;
143 + if ( $as_array && isset( $payload['nbf'] ) ) {
144 + $nbf = $payload['nbf'];
145 + }
146 + // Check if the nbf if it is defined. This is the time that the
147 + // token can actually be used. If it's not yet that time, abort.
148 + if ( $nbf > ( $timestamp + static::$leeway ) ) {
149 + throw new BeforeValidException(
150 + 'Cannot handle token prior to ' . gmdate( 'Y-m-d\\TH:i:sO', $nbf )
151 + );
152 + }
153 +
154 + $iat = ! $as_array && isset( $payload->iat ) ? $payload->iat : null;
155 + if ( $as_array && isset( $payload['iat'] ) ) {
156 + $iat = $payload['iat'];
157 + }
158 + // Check that this token has been created before 'now'. This prevents
159 + // using tokens that have been created for later use (and haven't
160 + // correctly used the nbf claim).
161 + if ( $iat > ( $timestamp + static::$leeway ) ) {
162 + throw new BeforeValidException(
163 + 'Cannot handle token prior to ' . gmdate( 'Y-m-d\\TH:i:sO', $iat )
164 + );
165 + }
166 +
167 + $exp = ! $as_array && isset( $payload->exp ) ? $payload->exp : null;
168 + if ( $as_array && isset( $payload['exp'] ) ) {
169 + $exp = $payload['exp'];
170 + }
171 +
172 + // Check if this token has expired.
173 + if ( $exp && ( $timestamp - static::$leeway ) >= $exp ) {
174 + throw new ExpiredException( 'Expired token' );
175 + }
176 +
177 + return $payload;
178 + }
179 +
180 + /**
181 + * Converts and signs a PHP object or array into a JWT string.
182 + *
183 + * @param object|array $payload PHP object or array.
184 + * @param string $key The secret key.
185 + * If the algorithm used is asymmetric, this is the private key.
186 + * @param string $alg The signing algorithm.
187 + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'.
188 + * @param mixed $key_id The key ID.
189 + * @param array $head An array with header elements to attach.
190 + *
191 + * @return string A signed JWT
192 + *
193 + * @uses json_encode
194 + * @uses urlsafe_b64_decode
195 + */
196 + public static function encode( $payload, $key, $alg = 'HS256', $key_id = null, $head = null ) {
197 + $header = array(
198 + 'typ' => 'JWT',
199 + 'alg' => $alg,
200 + );
201 +
202 + if ( null !== $key_id ) {
203 + $header['kid'] = $key_id;
204 + }
205 +
206 + if ( isset( $head ) && is_array( $head ) ) {
207 + $header = array_merge( $head, $header );
208 + }
209 +
210 + $segments = array();
211 + $segments[] = static::urlsafe_b64_encode( static::json_encode( $header ) );
212 + $segments[] = static::urlsafe_b64_encode( static::json_encode( $payload ) );
213 + $signing_input = implode( '.', $segments );
214 +
215 + $signature = static::sign( $signing_input, $key, $alg );
216 + $segments[] = static::urlsafe_b64_encode( $signature );
217 +
218 + return implode( '.', $segments );
219 + }
220 +
221 + /**
222 + * Sign a string with a given key and algorithm.
223 + *
224 + * @param string $msg The message to sign.
225 + * @param string|resource $key The secret key.
226 + * @param string $alg The signing algorithm.
227 + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'.
228 + *
229 + * @return string An encrypted message
230 + *
231 + * @throws DomainException Unsupported algorithm was specified.
232 + */
233 + public static function sign( $msg, $key, $alg = 'HS256' ) {
234 + if ( empty( static::$supported_algs[ $alg ] ) ) {
235 + throw new DomainException( 'Algorithm not supported' );
236 + }
237 + list($function, $algorithm) = static::$supported_algs[ $alg ];
238 + switch ( $function ) {
239 + case 'hash_hmac':
240 + return hash_hmac( $algorithm, $msg, $key, true );
241 + case 'openssl':
242 + $signature = '';
243 + $success = openssl_sign( $msg, $signature, $key, $algorithm );
244 + if ( ! $success ) {
245 + throw new DomainException( 'OpenSSL unable to sign data' );
246 + } else {
247 + return $signature;
248 + }
249 + }
250 + }
251 +
252 + /**
253 + * Verify a signature with the message, key and method. Not all methods
254 + * are symmetric, so we must have a separate verify and sign method.
255 + *
256 + * @param string $msg The original message (header and body).
257 + * @param string $signature The original signature.
258 + * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key.
259 + * @param string $alg The algorithm.
260 + *
261 + * @return bool
262 + *
263 + * @throws DomainException Invalid Algorithm or OpenSSL failure.
264 + */
265 + private static function verify( $msg, $signature, $key, $alg ) {
266 + if ( empty( static::$supported_algs[ $alg ] ) ) {
267 + throw new DomainException( 'Algorithm not supported' );
268 + }
269 +
270 + list($function, $algorithm) = static::$supported_algs[ $alg ];
271 + switch ( $function ) {
272 + case 'openssl':
273 + $success = openssl_verify( $msg, $signature, $key, $algorithm );
274 +
275 + if ( 1 === $success ) {
276 + return true;
277 + } elseif ( 0 === $success ) {
278 + return false;
279 + }
280 +
281 + // returns 1 on success, 0 on failure, -1 on error.
282 + throw new DomainException(
283 + 'OpenSSL error: ' . openssl_error_string()
284 + );
285 + case 'hash_hmac':
286 + default:
287 + $hash = hash_hmac( $algorithm, $msg, $key, true );
288 +
289 + if ( function_exists( 'hash_equals' ) ) {
290 + return hash_equals( $signature, $hash );
291 + }
292 +
293 + $len = min( static::safe_strlen( $signature ), static::safe_strlen( $hash ) );
294 +
295 + $status = 0;
296 +
297 + for ( $i = 0; $i < $len; $i++ ) {
298 + $status |= ( ord( $signature[ $i ] ) ^ ord( $hash[ $i ] ) );
299 + }
300 +
301 + $status |= ( static::safe_strlen( $signature ) ^ static::safe_strlen( $hash ) );
302 +
303 + return ( 0 === $status );
304 + }
305 + }
306 +
307 + /**
308 + * Decode a JSON string into a PHP object.
309 + *
310 + * @param string $input JSON string.
311 + * @param bool $as_array Whether to return the result as an associative array.
312 + *
313 + * @return object|array Object or Array representation of JSON string
314 + *
315 + * @throws DomainException Provided string was invalid JSON.
316 + */
317 + public static function json_decode( $input, $as_array = false ) {
318 + $obj = json_decode( $input, $as_array, 512, JSON_BIGINT_AS_STRING );
319 + $errno = json_last_error();
320 +
321 + if ( $errno ) {
322 + static::handle_json_error( $errno );
323 + } elseif ( null === $obj && 'null' !== $input ) {
324 + throw new DomainException( 'Null result with non-null input' );
325 + } elseif ( $obj === null ) {
326 + throw new DomainException( 'Null result' );
327 + }
328 +
329 + return $obj;
330 + }
331 +
332 + /**
333 + * Encode a PHP object into a JSON string.
334 + *
335 + * @param object|array $input A PHP object or array.
336 + *
337 + * @return string JSON representation of the PHP object or array.
338 + *
339 + * @throws DomainException Provided object could not be encoded to valid JSON.
340 + */
341 + public static function json_encode( $input ) {
342 + $json = \wp_json_encode( $input, JSON_UNESCAPED_SLASHES );
343 + $errno = json_last_error();
344 +
345 + if ( $errno ) {
346 + static::handle_json_error( $errno );
347 + } elseif ( 'null' === $json && null !== $input ) {
348 + throw new DomainException( 'Null result with non-null input' );
349 + }
350 + return $json;
351 + }
352 +
353 + /**
354 + * Decode a string with URL-safe Base64.
355 + *
356 + * @param string $input A Base64 encoded string.
357 + *
358 + * @return string A decoded string
359 + */
360 + public static function urlsafe_b64_decode( $input ) {
361 + $remainder = strlen( $input ) % 4;
362 + if ( $remainder ) {
363 + $padlen = 4 - $remainder;
364 + $input .= str_repeat( '=', $padlen );
365 + }
366 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
367 + return base64_decode( strtr( $input, '-_', '+/' ) );
368 + }
369 +
370 + /**
371 + * Encode a string with URL-safe Base64.
372 + *
373 + * @param string $input The string you want encoded.
374 + *
375 + * @return string The base64 encode of what you passed in
376 + */
377 + public static function urlsafe_b64_encode( $input ) {
378 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
379 + return str_replace( '=', '', strtr( base64_encode( $input ), '+/', '-_' ) );
380 + }
381 +
382 + /**
383 + * Helper method to create a JSON error.
384 + *
385 + * @param int $errno An error number from json_last_error().
386 + * @throws DomainException .
387 + *
388 + * @return never
389 + */
390 + private static function handle_json_error( $errno ) {
391 + $messages = array(
392 + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
393 + JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
394 + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
395 + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
396 + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters',
397 + );
398 + throw new DomainException(
399 + $messages[ $errno ] ?? 'Unknown JSON error: ' . $errno
400 + );
401 + }
402 +
403 + /**
404 + * Get the number of bytes in cryptographic strings.
405 + *
406 + * @param string $str .
407 + *
408 + * @return int
409 + */
410 + private static function safe_strlen( $str ) {
411 + if ( function_exists( 'mb_strlen' ) ) {
412 + return mb_strlen( $str, '8bit' );
413 + }
414 + return strlen( $str );
415 + }
416 +}
417 +
418 +// phpcs:disable
419 +if ( ! class_exists( 'SignatureInvalidException' ) ) {
420 + /**
421 + * SignatureInvalidException
422 + *
423 + * @package Automattic\Jetpack\Extensions\Premium_Content
424 + */
425 + class SignatureInvalidException extends \UnexpectedValueException { }
426 +}
427 +if ( ! class_exists( 'ExpiredException' ) ) {
428 + /**
429 + * ExpiredException
430 + *
431 + * @package Automattic\Jetpack\Extensions\Premium_Content
432 + */
433 + class ExpiredException extends \UnexpectedValueException { }
434 +}
435 +if ( ! class_exists( 'BeforeValidException' ) ) {
436 + /**
437 + * BeforeValidException
438 + *
439 + * @package Automattic\Jetpack\Extensions\Premium_Content
440 + */
441 + class BeforeValidException extends \UnexpectedValueException { }
442 +}
443 +// phpcs:enable