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