Browse code

added appinfo/info.xml appinfo/signature.json CHANGELOG.txt lib/AppInfo/Application.php css/style.css providers/Plivo

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

removed appinfo/info.xml appinfo/signature.json CHANGELOG.txt lib/AppInfo/Application.php css/style.css providers/Plivo

DoubleBastionAdmin authored on 05/11/2025 13:12:22
Showing 1 changed files
1 1
deleted file mode 100644
... ...
@@ -1,512 +0,0 @@
1
-<?php
2
-
3
-namespace Firebase\JWT;
4
-
5
-use \DomainException;
6
-use \InvalidArgumentException;
7
-use \UnexpectedValueException;
8
-use \DateTime;
9
-
10
-/**
11
- * JSON Web Token implementation, based on this spec:
12
- * https://tools.ietf.org/html/rfc7519
13
- *
14
- * PHP version 5
15
- *
16
- * @category Authentication
17
- * @package  Authentication_JWT
18
- * @author   Neuman Vong <neuman@twilio.com>
19
- * @author   Anant Narayanan <anant@php.net>
20
- * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
21
- * @link     https://github.com/firebase/php-jwt
22
- */
23
-class JWT
24
-{
25
-    const ASN1_INTEGER = 0x02;
26
-    const ASN1_SEQUENCE = 0x10;
27
-    const ASN1_BIT_STRING = 0x03;
28
-
29
-    /**
30
-     * When checking nbf, iat or expiration times,
31
-     * we want to provide some extra leeway time to
32
-     * account for clock skew.
33
-     */
34
-    public static $leeway = 0;
35
-
36
-    /**
37
-     * Allow the current timestamp to be specified.
38
-     * Useful for fixing a value within unit testing.
39
-     *
40
-     * Will default to PHP time() value if null.
41
-     */
42
-    public static $timestamp = null;
43
-
44
-    public static $supported_algs = array(
45
-        'ES256' => array('openssl', 'SHA256'),
46
-        'HS256' => array('hash_hmac', 'SHA256'),
47
-        'HS384' => array('hash_hmac', 'SHA384'),
48
-        'HS512' => array('hash_hmac', 'SHA512'),
49
-        'RS256' => array('openssl', 'SHA256'),
50
-        'RS384' => array('openssl', 'SHA384'),
51
-        'RS512' => array('openssl', 'SHA512'),
52
-    );
53
-
54
-    /**
55
-     * Decodes a JWT string into a PHP object.
56
-     *
57
-     * @param string                    $jwt            The JWT
58
-     * @param string|array|resource     $key            The key, or map of keys.
59
-     *                                                  If the algorithm used is asymmetric, this is the public key
60
-     * @param array                     $allowed_algs   List of supported verification algorithms
61
-     *                                                  Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
62
-     *
63
-     * @return object The JWT's payload as a PHP object
64
-     *
65
-     * @throws UnexpectedValueException     Provided JWT was invalid
66
-     * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
67
-     * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
68
-     * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
69
-     * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
70
-     *
71
-     * @uses jsonDecode
72
-     * @uses urlsafeB64Decode
73
-     */
74
-    public static function decode($jwt, $key, array $allowed_algs = array())
75
-    {
76
-        $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
77
-
78
-        if (empty($key)) {
79
-            throw new InvalidArgumentException('Key may not be empty');
80
-        }
81
-        $tks = \explode('.', $jwt);
82
-        if (\count($tks) != 3) {
83
-            throw new UnexpectedValueException('Wrong number of segments');
84
-        }
85
-        list($headb64, $bodyb64, $cryptob64) = $tks;
86
-        if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
87
-            throw new UnexpectedValueException('Invalid header encoding');
88
-        }
89
-        if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
90
-            throw new UnexpectedValueException('Invalid claims encoding');
91
-        }
92
-        if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
93
-            throw new UnexpectedValueException('Invalid signature encoding');
94
-        }
95
-        if (empty($header->alg)) {
96
-            throw new UnexpectedValueException('Empty algorithm');
97
-        }
98
-        if (empty(static::$supported_algs[$header->alg])) {
99
-            throw new UnexpectedValueException('Algorithm not supported');
100
-        }
101
-        if (!\in_array($header->alg, $allowed_algs)) {
102
-            throw new UnexpectedValueException('Algorithm not allowed');
103
-        }
104
-        if ($header->alg === 'ES256') {
105
-            // OpenSSL expects an ASN.1 DER sequence for ES256 signatures
106
-            $sig = self::signatureToDER($sig);
107
-        }
108
-
109
-        if (\is_array($key) || $key instanceof \ArrayAccess) {
110
-            if (isset($header->kid)) {
111
-                if (!isset($key[$header->kid])) {
112
-                    throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
113
-                }
114
-                $key = $key[$header->kid];
115
-            } else {
116
-                throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
117
-            }
118
-        }
119
-
120
-        // Check the signature
121
-        if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
122
-            throw new SignatureInvalidException('Signature verification failed');
123
-        }
124
-
125
-        // Check the nbf if it is defined. This is the time that the
126
-        // token can actually be used. If it's not yet that time, abort.
127
-        if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
128
-            throw new BeforeValidException(
129
-                'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf)
130
-            );
131
-        }
132
-
133
-        // Check that this token has been created before 'now'. This prevents
134
-        // using tokens that have been created for later use (and haven't
135
-        // correctly used the nbf claim).
136
-        if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
137
-            throw new BeforeValidException(
138
-                'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat)
139
-            );
140
-        }
141
-
142
-        // Check if this token has expired.
143
-        if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
144
-            throw new ExpiredException('Expired token');
145
-        }
146
-
147
-        return $payload;
148
-    }
149
-
150
-    /**
151
-     * Converts and signs a PHP object or array into a JWT string.
152
-     *
153
-     * @param object|array  $payload    PHP object or array
154
-     * @param string        $key        The secret key.
155
-     *                                  If the algorithm used is asymmetric, this is the private key
156
-     * @param string        $alg        The signing algorithm.
157
-     *                                  Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
158
-     * @param mixed         $keyId
159
-     * @param array         $head       An array with header elements to attach
160
-     *
161
-     * @return string A signed JWT
162
-     *
163
-     * @uses jsonEncode
164
-     * @uses urlsafeB64Encode
165
-     */
166
-    public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
167
-    {
168
-        $header = array('typ' => 'JWT', 'alg' => $alg);
169
-        if ($keyId !== null) {
170
-            $header['kid'] = $keyId;
171
-        }
172
-        if (isset($head) && \is_array($head)) {
173
-            $header = \array_merge($head, $header);
174
-        }
175
-        $segments = array();
176
-        $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
177
-        $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
178
-        $signing_input = \implode('.', $segments);
179
-
180
-        $signature = static::sign($signing_input, $key, $alg);
181
-        $segments[] = static::urlsafeB64Encode($signature);
182
-
183
-        return \implode('.', $segments);
184
-    }
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   $key    The secret key
191
-     * @param string            $alg    The signing algorithm.
192
-     *                                  Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
193
-     *
194
-     * @return string An encrypted message
195
-     *
196
-     * @throws DomainException Unsupported algorithm was specified
197
-     */
198
-    public static function sign($msg, $key, $alg = 'HS256')
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
-                return \hash_hmac($algorithm, $msg, $key, true);
207
-            case 'openssl':
208
-                $signature = '';
209
-                $success = \openssl_sign($msg, $signature, $key, $algorithm);
210
-                if (!$success) {
211
-                    throw new DomainException("OpenSSL unable to sign data");
212
-                } else {
213
-                    if ($alg === 'ES256') {
214
-                        $signature = self::signatureFromDER($signature, 256);
215
-                    }
216
-                    return $signature;
217
-                }
218
-        }
219
-    }
220
-
221
-    /**
222
-     * Verify a signature with the message, key and method. Not all methods
223
-     * are symmetric, so we must have a separate verify and sign method.
224
-     *
225
-     * @param string            $msg        The original message (header and body)
226
-     * @param string            $signature  The original signature
227
-     * @param string|resource   $key        For HS*, a string key works. for RS*, must be a resource of an openssl public key
228
-     * @param string            $alg        The algorithm
229
-     *
230
-     * @return bool
231
-     *
232
-     * @throws DomainException Invalid Algorithm or OpenSSL failure
233
-     */
234
-    private static function verify($msg, $signature, $key, $alg)
235
-    {
236
-        if (empty(static::$supported_algs[$alg])) {
237
-            throw new DomainException('Algorithm not supported');
238
-        }
239
-
240
-        list($function, $algorithm) = static::$supported_algs[$alg];
241
-        switch ($function) {
242
-            case 'openssl':
243
-                $success = \openssl_verify($msg, $signature, $key, $algorithm);
244
-                if ($success === 1) {
245
-                    return true;
246
-                } elseif ($success === 0) {
247
-                    return false;
248
-                }
249
-                // returns 1 on success, 0 on failure, -1 on error.
250
-                throw new DomainException(
251
-                    'OpenSSL error: ' . \openssl_error_string()
252
-                );
253
-            case 'hash_hmac':
254
-            default:
255
-                $hash = \hash_hmac($algorithm, $msg, $key, true);
256
-                if (\function_exists('hash_equals')) {
257
-                    return \hash_equals($signature, $hash);
258
-                }
259
-                $len = \min(static::safeStrlen($signature), static::safeStrlen($hash));
260
-
261
-                $status = 0;
262
-                for ($i = 0; $i < $len; $i++) {
263
-                    $status |= (\ord($signature[$i]) ^ \ord($hash[$i]));
264
-                }
265
-                $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));
266
-
267
-                return ($status === 0);
268
-        }
269
-    }
270
-
271
-    /**
272
-     * Decode a JSON string into a PHP object.
273
-     *
274
-     * @param string $input JSON string
275
-     *
276
-     * @return object Object representation of JSON string
277
-     *
278
-     * @throws DomainException Provided string was invalid JSON
279
-     */
280
-    public static function jsonDecode($input)
281
-    {
282
-        if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
283
-            /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
284
-             * to specify that large ints (like Steam Transaction IDs) should be treated as
285
-             * strings, rather than the PHP default behaviour of converting them to floats.
286
-             */
287
-            $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
288
-        } else {
289
-            /** Not all servers will support that, however, so for older versions we must
290
-             * manually detect large ints in the JSON string and quote them (thus converting
291
-             *them to strings) before decoding, hence the preg_replace() call.
292
-             */
293
-            $max_int_length = \strlen((string) PHP_INT_MAX) - 1;
294
-            $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
295
-            $obj = \json_decode($json_without_bigints);
296
-        }
297
-
298
-        if ($errno = \json_last_error()) {
299
-            static::handleJsonError($errno);
300
-        } elseif ($obj === null && $input !== 'null') {
301
-            throw new DomainException('Null result with non-null input');
302
-        }
303
-        return $obj;
304
-    }
305
-
306
-    /**
307
-     * Encode a PHP object into a JSON string.
308
-     *
309
-     * @param object|array $input A PHP object or array
310
-     *
311
-     * @return string JSON representation of the PHP object or array
312
-     *
313
-     * @throws DomainException Provided object could not be encoded to valid JSON
314
-     */
315
-    public static function jsonEncode($input)
316
-    {
317
-        $json = \json_encode($input);
318
-        if ($errno = \json_last_error()) {
319
-            static::handleJsonError($errno);
320
-        } elseif ($json === 'null' && $input !== null) {
321
-            throw new DomainException('Null result with non-null input');
322
-        }
323
-        return $json;
324
-    }
325
-
326
-    /**
327
-     * Decode a string with URL-safe Base64.
328
-     *
329
-     * @param string $input A Base64 encoded string
330
-     *
331
-     * @return string A decoded string
332
-     */
333
-    public static function urlsafeB64Decode($input)
334
-    {
335
-        $remainder = \strlen($input) % 4;
336
-        if ($remainder) {
337
-            $padlen = 4 - $remainder;
338
-            $input .= \str_repeat('=', $padlen);
339
-        }
340
-        return \base64_decode(\strtr($input, '-_', '+/'));
341
-    }
342
-
343
-    /**
344
-     * Encode a string with URL-safe Base64.
345
-     *
346
-     * @param string $input The string you want encoded
347
-     *
348
-     * @return string The base64 encode of what you passed in
349
-     */
350
-    public static function urlsafeB64Encode($input)
351
-    {
352
-        return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
353
-    }
354
-
355
-    /**
356
-     * Helper method to create a JSON error.
357
-     *
358
-     * @param int $errno An error number from json_last_error()
359
-     *
360
-     * @return void
361
-     */
362
-    private static function handleJsonError($errno)
363
-    {
364
-        $messages = array(
365
-            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
366
-            JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
367
-            JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
368
-            JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
369
-            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
370
-        );
371
-        throw new DomainException(
372
-            isset($messages[$errno])
373
-            ? $messages[$errno]
374
-            : 'Unknown JSON error: ' . $errno
375
-        );
376
-    }
377
-
378
-    /**
379
-     * Get the number of bytes in cryptographic strings.
380
-     *
381
-     * @param string $str
382
-     *
383
-     * @return int
384
-     */
385
-    private static function safeStrlen($str)
386
-    {
387
-        if (\function_exists('mb_strlen')) {
388
-            return \mb_strlen($str, '8bit');
389
-        }
390
-        return \strlen($str);
391
-    }
392
-
393
-    /**
394
-     * Convert an ECDSA signature to an ASN.1 DER sequence
395
-     *
396
-     * @param   string $sig The ECDSA signature to convert
397
-     * @return  string The encoded DER object
398
-     */
399
-    private static function signatureToDER($sig)
400
-    {
401
-        // Separate the signature into r-value and s-value
402
-        list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2));
403
-
404
-        // Trim leading zeros
405
-        $r = \ltrim($r, "\x00");
406
-        $s = \ltrim($s, "\x00");
407
-
408
-        // Convert r-value and s-value from unsigned big-endian integers to
409
-        // signed two's complement
410
-        if (\ord($r[0]) > 0x7f) {
411
-            $r = "\x00" . $r;
412
-        }
413
-        if (\ord($s[0]) > 0x7f) {
414
-            $s = "\x00" . $s;
415
-        }
416
-
417
-        return self::encodeDER(
418
-            self::ASN1_SEQUENCE,
419
-            self::encodeDER(self::ASN1_INTEGER, $r) .
420
-            self::encodeDER(self::ASN1_INTEGER, $s)
421
-        );
422
-    }
423
-
424
-    /**
425
-     * Encodes a value into a DER object.
426
-     *
427
-     * @param   int     $type DER tag
428
-     * @param   string  $value the value to encode
429
-     * @return  string  the encoded object
430
-     */
431
-    private static function encodeDER($type, $value)
432
-    {
433
-        $tag_header = 0;
434
-        if ($type === self::ASN1_SEQUENCE) {
435
-            $tag_header |= 0x20;
436
-        }
437
-
438
-        // Type
439
-        $der = \chr($tag_header | $type);
440
-
441
-        // Length
442
-        $der .= \chr(\strlen($value));
443
-
444
-        return $der . $value;
445
-    }
446
-
447
-    /**
448
-     * Encodes signature from a DER object.
449
-     *
450
-     * @param   string  $der binary signature in DER format
451
-     * @param   int     $keySize the number of bits in the key
452
-     * @return  string  the signature
453
-     */
454
-    private static function signatureFromDER($der, $keySize)
455
-    {
456
-        // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
457
-        list($offset, $_) = self::readDER($der);
458
-        list($offset, $r) = self::readDER($der, $offset);
459
-        list($offset, $s) = self::readDER($der, $offset);
460
-
461
-        // Convert r-value and s-value from signed two's compliment to unsigned
462
-        // big-endian integers
463
-        $r = \ltrim($r, "\x00");
464
-        $s = \ltrim($s, "\x00");
465
-
466
-        // Pad out r and s so that they are $keySize bits long
467
-        $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
468
-        $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
469
-
470
-        return $r . $s;
471
-    }
472
-
473
-    /**
474
-     * Reads binary DER-encoded data and decodes into a single object
475
-     *
476
-     * @param string $der the binary data in DER format
477
-     * @param int $offset the offset of the data stream containing the object
478
-     * to decode
479
-     * @return array [$offset, $data] the new offset and the decoded object
480
-     */
481
-    private static function readDER($der, $offset = 0)
482
-    {
483
-        $pos = $offset;
484
-        $size = \strlen($der);
485
-        $constructed = (\ord($der[$pos]) >> 5) & 0x01;
486
-        $type = \ord($der[$pos++]) & 0x1f;
487
-
488
-        // Length
489
-        $len = \ord($der[$pos++]);
490
-        if ($len & 0x80) {
491
-            $n = $len & 0x1f;
492
-            $len = 0;
493
-            while ($n-- && $pos < $size) {
494
-                $len = ($len << 8) | \ord($der[$pos++]);
495
-            }
496
-        }
497
-
498
-        // Value
499
-        if ($type == self::ASN1_BIT_STRING) {
500
-            $pos++; // Skip the first contents octet (padding indicator)
501
-            $data = \substr($der, $pos, $len - 1);
502
-            $pos += $len - 1;
503
-        } elseif (!$constructed) {
504
-            $data = \substr($der, $pos, $len);
505
-            $pos += $len;
506
-        } else {
507
-            $data = null;
508
-        }
509
-
510
-        return array($pos, $data);
511
-    }
512
-}
Browse code

Added README.md appinfo/info.xml appinfo/signature.json lib/Controller/AuthorApiController.php and the providers directory

DoubleBastionAdmin authored on 20/08/2022 16:33:00
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,512 @@
1
+<?php
2
+
3
+namespace Firebase\JWT;
4
+
5
+use \DomainException;
6
+use \InvalidArgumentException;
7
+use \UnexpectedValueException;
8
+use \DateTime;
9
+
10
+/**
11
+ * JSON Web Token implementation, based on this spec:
12
+ * https://tools.ietf.org/html/rfc7519
13
+ *
14
+ * PHP version 5
15
+ *
16
+ * @category Authentication
17
+ * @package  Authentication_JWT
18
+ * @author   Neuman Vong <neuman@twilio.com>
19
+ * @author   Anant Narayanan <anant@php.net>
20
+ * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
21
+ * @link     https://github.com/firebase/php-jwt
22
+ */
23
+class JWT
24
+{
25
+    const ASN1_INTEGER = 0x02;
26
+    const ASN1_SEQUENCE = 0x10;
27
+    const ASN1_BIT_STRING = 0x03;
28
+
29
+    /**
30
+     * When checking nbf, iat or expiration times,
31
+     * we want to provide some extra leeway time to
32
+     * account for clock skew.
33
+     */
34
+    public static $leeway = 0;
35
+
36
+    /**
37
+     * Allow the current timestamp to be specified.
38
+     * Useful for fixing a value within unit testing.
39
+     *
40
+     * Will default to PHP time() value if null.
41
+     */
42
+    public static $timestamp = null;
43
+
44
+    public static $supported_algs = array(
45
+        'ES256' => array('openssl', 'SHA256'),
46
+        'HS256' => array('hash_hmac', 'SHA256'),
47
+        'HS384' => array('hash_hmac', 'SHA384'),
48
+        'HS512' => array('hash_hmac', 'SHA512'),
49
+        'RS256' => array('openssl', 'SHA256'),
50
+        'RS384' => array('openssl', 'SHA384'),
51
+        'RS512' => array('openssl', 'SHA512'),
52
+    );
53
+
54
+    /**
55
+     * Decodes a JWT string into a PHP object.
56
+     *
57
+     * @param string                    $jwt            The JWT
58
+     * @param string|array|resource     $key            The key, or map of keys.
59
+     *                                                  If the algorithm used is asymmetric, this is the public key
60
+     * @param array                     $allowed_algs   List of supported verification algorithms
61
+     *                                                  Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
62
+     *
63
+     * @return object The JWT's payload as a PHP object
64
+     *
65
+     * @throws UnexpectedValueException     Provided JWT was invalid
66
+     * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
67
+     * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
68
+     * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
69
+     * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
70
+     *
71
+     * @uses jsonDecode
72
+     * @uses urlsafeB64Decode
73
+     */
74
+    public static function decode($jwt, $key, array $allowed_algs = array())
75
+    {
76
+        $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
77
+
78
+        if (empty($key)) {
79
+            throw new InvalidArgumentException('Key may not be empty');
80
+        }
81
+        $tks = \explode('.', $jwt);
82
+        if (\count($tks) != 3) {
83
+            throw new UnexpectedValueException('Wrong number of segments');
84
+        }
85
+        list($headb64, $bodyb64, $cryptob64) = $tks;
86
+        if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
87
+            throw new UnexpectedValueException('Invalid header encoding');
88
+        }
89
+        if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
90
+            throw new UnexpectedValueException('Invalid claims encoding');
91
+        }
92
+        if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
93
+            throw new UnexpectedValueException('Invalid signature encoding');
94
+        }
95
+        if (empty($header->alg)) {
96
+            throw new UnexpectedValueException('Empty algorithm');
97
+        }
98
+        if (empty(static::$supported_algs[$header->alg])) {
99
+            throw new UnexpectedValueException('Algorithm not supported');
100
+        }
101
+        if (!\in_array($header->alg, $allowed_algs)) {
102
+            throw new UnexpectedValueException('Algorithm not allowed');
103
+        }
104
+        if ($header->alg === 'ES256') {
105
+            // OpenSSL expects an ASN.1 DER sequence for ES256 signatures
106
+            $sig = self::signatureToDER($sig);
107
+        }
108
+
109
+        if (\is_array($key) || $key instanceof \ArrayAccess) {
110
+            if (isset($header->kid)) {
111
+                if (!isset($key[$header->kid])) {
112
+                    throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
113
+                }
114
+                $key = $key[$header->kid];
115
+            } else {
116
+                throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
117
+            }
118
+        }
119
+
120
+        // Check the signature
121
+        if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
122
+            throw new SignatureInvalidException('Signature verification failed');
123
+        }
124
+
125
+        // Check the nbf if it is defined. This is the time that the
126
+        // token can actually be used. If it's not yet that time, abort.
127
+        if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
128
+            throw new BeforeValidException(
129
+                'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf)
130
+            );
131
+        }
132
+
133
+        // Check that this token has been created before 'now'. This prevents
134
+        // using tokens that have been created for later use (and haven't
135
+        // correctly used the nbf claim).
136
+        if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
137
+            throw new BeforeValidException(
138
+                'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat)
139
+            );
140
+        }
141
+
142
+        // Check if this token has expired.
143
+        if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
144
+            throw new ExpiredException('Expired token');
145
+        }
146
+
147
+        return $payload;
148
+    }
149
+
150
+    /**
151
+     * Converts and signs a PHP object or array into a JWT string.
152
+     *
153
+     * @param object|array  $payload    PHP object or array
154
+     * @param string        $key        The secret key.
155
+     *                                  If the algorithm used is asymmetric, this is the private key
156
+     * @param string        $alg        The signing algorithm.
157
+     *                                  Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
158
+     * @param mixed         $keyId
159
+     * @param array         $head       An array with header elements to attach
160
+     *
161
+     * @return string A signed JWT
162
+     *
163
+     * @uses jsonEncode
164
+     * @uses urlsafeB64Encode
165
+     */
166
+    public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
167
+    {
168
+        $header = array('typ' => 'JWT', 'alg' => $alg);
169
+        if ($keyId !== null) {
170
+            $header['kid'] = $keyId;
171
+        }
172
+        if (isset($head) && \is_array($head)) {
173
+            $header = \array_merge($head, $header);
174
+        }
175
+        $segments = array();
176
+        $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
177
+        $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
178
+        $signing_input = \implode('.', $segments);
179
+
180
+        $signature = static::sign($signing_input, $key, $alg);
181
+        $segments[] = static::urlsafeB64Encode($signature);
182
+
183
+        return \implode('.', $segments);
184
+    }
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   $key    The secret key
191
+     * @param string            $alg    The signing algorithm.
192
+     *                                  Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
193
+     *
194
+     * @return string An encrypted message
195
+     *
196
+     * @throws DomainException Unsupported algorithm was specified
197
+     */
198
+    public static function sign($msg, $key, $alg = 'HS256')
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
+                return \hash_hmac($algorithm, $msg, $key, true);
207
+            case 'openssl':
208
+                $signature = '';
209
+                $success = \openssl_sign($msg, $signature, $key, $algorithm);
210
+                if (!$success) {
211
+                    throw new DomainException("OpenSSL unable to sign data");
212
+                } else {
213
+                    if ($alg === 'ES256') {
214
+                        $signature = self::signatureFromDER($signature, 256);
215
+                    }
216
+                    return $signature;
217
+                }
218
+        }
219
+    }
220
+
221
+    /**
222
+     * Verify a signature with the message, key and method. Not all methods
223
+     * are symmetric, so we must have a separate verify and sign method.
224
+     *
225
+     * @param string            $msg        The original message (header and body)
226
+     * @param string            $signature  The original signature
227
+     * @param string|resource   $key        For HS*, a string key works. for RS*, must be a resource of an openssl public key
228
+     * @param string            $alg        The algorithm
229
+     *
230
+     * @return bool
231
+     *
232
+     * @throws DomainException Invalid Algorithm or OpenSSL failure
233
+     */
234
+    private static function verify($msg, $signature, $key, $alg)
235
+    {
236
+        if (empty(static::$supported_algs[$alg])) {
237
+            throw new DomainException('Algorithm not supported');
238
+        }
239
+
240
+        list($function, $algorithm) = static::$supported_algs[$alg];
241
+        switch ($function) {
242
+            case 'openssl':
243
+                $success = \openssl_verify($msg, $signature, $key, $algorithm);
244
+                if ($success === 1) {
245
+                    return true;
246
+                } elseif ($success === 0) {
247
+                    return false;
248
+                }
249
+                // returns 1 on success, 0 on failure, -1 on error.
250
+                throw new DomainException(
251
+                    'OpenSSL error: ' . \openssl_error_string()
252
+                );
253
+            case 'hash_hmac':
254
+            default:
255
+                $hash = \hash_hmac($algorithm, $msg, $key, true);
256
+                if (\function_exists('hash_equals')) {
257
+                    return \hash_equals($signature, $hash);
258
+                }
259
+                $len = \min(static::safeStrlen($signature), static::safeStrlen($hash));
260
+
261
+                $status = 0;
262
+                for ($i = 0; $i < $len; $i++) {
263
+                    $status |= (\ord($signature[$i]) ^ \ord($hash[$i]));
264
+                }
265
+                $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));
266
+
267
+                return ($status === 0);
268
+        }
269
+    }
270
+
271
+    /**
272
+     * Decode a JSON string into a PHP object.
273
+     *
274
+     * @param string $input JSON string
275
+     *
276
+     * @return object Object representation of JSON string
277
+     *
278
+     * @throws DomainException Provided string was invalid JSON
279
+     */
280
+    public static function jsonDecode($input)
281
+    {
282
+        if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
283
+            /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
284
+             * to specify that large ints (like Steam Transaction IDs) should be treated as
285
+             * strings, rather than the PHP default behaviour of converting them to floats.
286
+             */
287
+            $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
288
+        } else {
289
+            /** Not all servers will support that, however, so for older versions we must
290
+             * manually detect large ints in the JSON string and quote them (thus converting
291
+             *them to strings) before decoding, hence the preg_replace() call.
292
+             */
293
+            $max_int_length = \strlen((string) PHP_INT_MAX) - 1;
294
+            $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
295
+            $obj = \json_decode($json_without_bigints);
296
+        }
297
+
298
+        if ($errno = \json_last_error()) {
299
+            static::handleJsonError($errno);
300
+        } elseif ($obj === null && $input !== 'null') {
301
+            throw new DomainException('Null result with non-null input');
302
+        }
303
+        return $obj;
304
+    }
305
+
306
+    /**
307
+     * Encode a PHP object into a JSON string.
308
+     *
309
+     * @param object|array $input A PHP object or array
310
+     *
311
+     * @return string JSON representation of the PHP object or array
312
+     *
313
+     * @throws DomainException Provided object could not be encoded to valid JSON
314
+     */
315
+    public static function jsonEncode($input)
316
+    {
317
+        $json = \json_encode($input);
318
+        if ($errno = \json_last_error()) {
319
+            static::handleJsonError($errno);
320
+        } elseif ($json === 'null' && $input !== null) {
321
+            throw new DomainException('Null result with non-null input');
322
+        }
323
+        return $json;
324
+    }
325
+
326
+    /**
327
+     * Decode a string with URL-safe Base64.
328
+     *
329
+     * @param string $input A Base64 encoded string
330
+     *
331
+     * @return string A decoded string
332
+     */
333
+    public static function urlsafeB64Decode($input)
334
+    {
335
+        $remainder = \strlen($input) % 4;
336
+        if ($remainder) {
337
+            $padlen = 4 - $remainder;
338
+            $input .= \str_repeat('=', $padlen);
339
+        }
340
+        return \base64_decode(\strtr($input, '-_', '+/'));
341
+    }
342
+
343
+    /**
344
+     * Encode a string with URL-safe Base64.
345
+     *
346
+     * @param string $input The string you want encoded
347
+     *
348
+     * @return string The base64 encode of what you passed in
349
+     */
350
+    public static function urlsafeB64Encode($input)
351
+    {
352
+        return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
353
+    }
354
+
355
+    /**
356
+     * Helper method to create a JSON error.
357
+     *
358
+     * @param int $errno An error number from json_last_error()
359
+     *
360
+     * @return void
361
+     */
362
+    private static function handleJsonError($errno)
363
+    {
364
+        $messages = array(
365
+            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
366
+            JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
367
+            JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
368
+            JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
369
+            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
370
+        );
371
+        throw new DomainException(
372
+            isset($messages[$errno])
373
+            ? $messages[$errno]
374
+            : 'Unknown JSON error: ' . $errno
375
+        );
376
+    }
377
+
378
+    /**
379
+     * Get the number of bytes in cryptographic strings.
380
+     *
381
+     * @param string $str
382
+     *
383
+     * @return int
384
+     */
385
+    private static function safeStrlen($str)
386
+    {
387
+        if (\function_exists('mb_strlen')) {
388
+            return \mb_strlen($str, '8bit');
389
+        }
390
+        return \strlen($str);
391
+    }
392
+
393
+    /**
394
+     * Convert an ECDSA signature to an ASN.1 DER sequence
395
+     *
396
+     * @param   string $sig The ECDSA signature to convert
397
+     * @return  string The encoded DER object
398
+     */
399
+    private static function signatureToDER($sig)
400
+    {
401
+        // Separate the signature into r-value and s-value
402
+        list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2));
403
+
404
+        // Trim leading zeros
405
+        $r = \ltrim($r, "\x00");
406
+        $s = \ltrim($s, "\x00");
407
+
408
+        // Convert r-value and s-value from unsigned big-endian integers to
409
+        // signed two's complement
410
+        if (\ord($r[0]) > 0x7f) {
411
+            $r = "\x00" . $r;
412
+        }
413
+        if (\ord($s[0]) > 0x7f) {
414
+            $s = "\x00" . $s;
415
+        }
416
+
417
+        return self::encodeDER(
418
+            self::ASN1_SEQUENCE,
419
+            self::encodeDER(self::ASN1_INTEGER, $r) .
420
+            self::encodeDER(self::ASN1_INTEGER, $s)
421
+        );
422
+    }
423
+
424
+    /**
425
+     * Encodes a value into a DER object.
426
+     *
427
+     * @param   int     $type DER tag
428
+     * @param   string  $value the value to encode
429
+     * @return  string  the encoded object
430
+     */
431
+    private static function encodeDER($type, $value)
432
+    {
433
+        $tag_header = 0;
434
+        if ($type === self::ASN1_SEQUENCE) {
435
+            $tag_header |= 0x20;
436
+        }
437
+
438
+        // Type
439
+        $der = \chr($tag_header | $type);
440
+
441
+        // Length
442
+        $der .= \chr(\strlen($value));
443
+
444
+        return $der . $value;
445
+    }
446
+
447
+    /**
448
+     * Encodes signature from a DER object.
449
+     *
450
+     * @param   string  $der binary signature in DER format
451
+     * @param   int     $keySize the number of bits in the key
452
+     * @return  string  the signature
453
+     */
454
+    private static function signatureFromDER($der, $keySize)
455
+    {
456
+        // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
457
+        list($offset, $_) = self::readDER($der);
458
+        list($offset, $r) = self::readDER($der, $offset);
459
+        list($offset, $s) = self::readDER($der, $offset);
460
+
461
+        // Convert r-value and s-value from signed two's compliment to unsigned
462
+        // big-endian integers
463
+        $r = \ltrim($r, "\x00");
464
+        $s = \ltrim($s, "\x00");
465
+
466
+        // Pad out r and s so that they are $keySize bits long
467
+        $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
468
+        $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
469
+
470
+        return $r . $s;
471
+    }
472
+
473
+    /**
474
+     * Reads binary DER-encoded data and decodes into a single object
475
+     *
476
+     * @param string $der the binary data in DER format
477
+     * @param int $offset the offset of the data stream containing the object
478
+     * to decode
479
+     * @return array [$offset, $data] the new offset and the decoded object
480
+     */
481
+    private static function readDER($der, $offset = 0)
482
+    {
483
+        $pos = $offset;
484
+        $size = \strlen($der);
485
+        $constructed = (\ord($der[$pos]) >> 5) & 0x01;
486
+        $type = \ord($der[$pos++]) & 0x1f;
487
+
488
+        // Length
489
+        $len = \ord($der[$pos++]);
490
+        if ($len & 0x80) {
491
+            $n = $len & 0x1f;
492
+            $len = 0;
493
+            while ($n-- && $pos < $size) {
494
+                $len = ($len << 8) | \ord($der[$pos++]);
495
+            }
496
+        }
497
+
498
+        // Value
499
+        if ($type == self::ASN1_BIT_STRING) {
500
+            $pos++; // Skip the first contents octet (padding indicator)
501
+            $data = \substr($der, $pos, $len - 1);
502
+            $pos += $len - 1;
503
+        } elseif (!$constructed) {
504
+            $data = \substr($der, $pos, $len);
505
+            $pos += $len;
506
+        } else {
507
+            $data = null;
508
+        }
509
+
510
+        return array($pos, $data);
511
+    }
512
+}