Skip to content

Commit 92aa12d

Browse files
authored
chore: update library to phpstan level 7 (#407)
1 parent 13082f8 commit 92aa12d

File tree

5 files changed

+96
-73
lines changed

5 files changed

+96
-73
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ jobs:
5454
- name: Run Script
5555
run: |
5656
composer global require phpstan/phpstan
57-
~/.composer/vendor/bin/phpstan analyse src
57+
~/.composer/vendor/bin/phpstan analyse

phpstan.neon.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
level: 7
3+
paths:
4+
- src
5+
treatPhpDocTypesAsCertain: false

src/JWK.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class JWK
2323
/**
2424
* Parse a set of JWK keys
2525
*
26-
* @param array $jwks The JSON Web Key Set as an associative array
26+
* @param array<mixed> $jwks The JSON Web Key Set as an associative array
2727
*
2828
* @return array<string, Key> An associative array of key IDs (kid) to Key objects
2929
*
@@ -48,7 +48,7 @@ public static function parseKeySet(array $jwks): array
4848
foreach ($jwks['keys'] as $k => $v) {
4949
$kid = isset($v['kid']) ? $v['kid'] : $k;
5050
if ($key = self::parseKey($v)) {
51-
$keys[$kid] = $key;
51+
$keys[(string) $kid] = $key;
5252
}
5353
}
5454

@@ -62,7 +62,7 @@ public static function parseKeySet(array $jwks): array
6262
/**
6363
* Parse a JWK key
6464
*
65-
* @param array $jwk An individual JWK
65+
* @param array<mixed> $jwk An individual JWK
6666
*
6767
* @return Key The key object for the JWK
6868
*
@@ -124,10 +124,16 @@ public static function parseKey(array $jwk): ?Key
124124
*
125125
* @uses encodeLength
126126
*/
127-
private static function createPemFromModulusAndExponent($n, $e)
128-
{
129-
$modulus = JWT::urlsafeB64Decode($n);
130-
$publicExponent = JWT::urlsafeB64Decode($e);
127+
private static function createPemFromModulusAndExponent(
128+
string $n,
129+
string $e
130+
): string {
131+
if (false === ($modulus = JWT::urlsafeB64Decode($n))) {
132+
throw new UnexpectedValueException('Invalid JWK encoding');
133+
}
134+
if (false === ($publicExponent = JWT::urlsafeB64Decode($e))) {
135+
throw new UnexpectedValueException('Invalid header encoding');
136+
}
131137

132138
$components = [
133139
'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus),

src/JWT.php

Lines changed: 72 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Exception;
88
use InvalidArgumentException;
99
use OpenSSLAsymmetricKey;
10+
use OpenSSLCertificate;
1011
use TypeError;
1112
use UnexpectedValueException;
1213
use DateTime;
@@ -38,6 +39,9 @@ class JWT
3839
*/
3940
public static int $leeway = 0;
4041

42+
/**
43+
* @var array<string, string[]>
44+
*/
4145
public static array $supported_algs = [
4246
'ES384' => ['openssl', 'SHA384'],
4347
'ES256' => ['openssl', 'SHA256'],
@@ -86,10 +90,16 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
8690
throw new UnexpectedValueException('Wrong number of segments');
8791
}
8892
list($headb64, $bodyb64, $cryptob64) = $tks;
89-
if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
93+
if (false === ($headerRaw = static::urlsafeB64Decode($headb64))) {
94+
throw new UnexpectedValueException('Invalid header encoding');
95+
}
96+
if (null === ($header = static::jsonDecode($headerRaw))) {
9097
throw new UnexpectedValueException('Invalid header encoding');
9198
}
92-
if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
99+
if (false === ($payloadRaw = static::urlsafeB64Decode($bodyb64))) {
100+
throw new UnexpectedValueException('Invalid claims encoding');
101+
}
102+
if (null === ($payload = static::jsonDecode($payloadRaw))) {
93103
throw new UnexpectedValueException('Invalid claims encoding');
94104
}
95105
if (!$payload instanceof stdClass) {
@@ -116,7 +126,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
116126
// OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
117127
$sig = self::signatureToDER($sig);
118128
}
119-
if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) {
129+
if (!self::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) {
120130
throw new SignatureInvalidException('Signature verification failed');
121131
}
122132

@@ -148,11 +158,10 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
148158
/**
149159
* Converts and signs a PHP object or array into a JWT string.
150160
*
151-
* @param array $payload PHP array
152-
* @param string|OpenSSLAsymmetricKey $key The secret key.
153-
* If the algorithm used is asymmetric, this is the private key
154-
* @param string $keyId
155-
* @param array $head An array with header elements to attach
161+
* @param array<mixed> $payload PHP array
162+
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed> $key The secret key.
163+
* @param string $keyId
164+
* @param array<string, string> $head An array with header elements to attach
156165
*
157166
* @return string A signed JWT
158167
*
@@ -161,7 +170,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
161170
*/
162171
public static function encode(
163172
array $payload,
164-
string|OpenSSLAsymmetricKey $key,
173+
string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key,
165174
string $alg,
166175
string $keyId = null,
167176
array $head = null
@@ -174,8 +183,8 @@ public static function encode(
174183
$header = \array_merge($head, $header);
175184
}
176185
$segments = [];
177-
$segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
178-
$segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
186+
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
187+
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
179188
$signing_input = \implode('.', $segments);
180189

181190
$signature = static::sign($signing_input, $key, $alg);
@@ -187,23 +196,29 @@ public static function encode(
187196
/**
188197
* Sign a string with a given key and algorithm.
189198
*
190-
* @param string $msg The message to sign
191-
* @param string|OpenSSLAsymmetricKey $key The secret key.
192-
* @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
193-
* 'HS512', 'RS256', 'RS384', and 'RS512'
199+
* @param string $msg The message to sign
200+
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed> $key The secret key.
201+
* @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
202+
* 'HS512', 'RS256', 'RS384', and 'RS512'
194203
*
195204
* @return string An encrypted message
196205
*
197206
* @throws DomainException Unsupported algorithm or bad key was specified
198207
*/
199-
public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, string $alg): string
200-
{
208+
public static function sign(
209+
string $msg,
210+
string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key,
211+
string $alg
212+
): string {
201213
if (empty(static::$supported_algs[$alg])) {
202214
throw new DomainException('Algorithm not supported');
203215
}
204216
list($function, $algorithm) = static::$supported_algs[$alg];
205217
switch ($function) {
206218
case 'hash_hmac':
219+
if (!is_string($key)) {
220+
throw new InvalidArgumentException('key must be a string when using hmac');
221+
}
207222
return \hash_hmac($algorithm, $msg, $key, true);
208223
case 'openssl':
209224
$signature = '';
@@ -221,10 +236,13 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
221236
if (!function_exists('sodium_crypto_sign_detached')) {
222237
throw new DomainException('libsodium is not available');
223238
}
239+
if (!is_string($key)) {
240+
throw new InvalidArgumentException('key must be a string when using EdDSA');
241+
}
224242
try {
225243
// The last non-empty line is used as the key.
226244
$lines = array_filter(explode("\n", $key));
227-
$key = base64_decode(end($lines));
245+
$key = base64_decode((string) end($lines));
228246
return sodium_crypto_sign_detached($msg, $key);
229247
} catch (Exception $e) {
230248
throw new DomainException($e->getMessage(), 0, $e);
@@ -238,10 +256,10 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
238256
* Verify a signature with the message, key and method. Not all methods
239257
* are symmetric, so we must have a separate verify and sign method.
240258
*
241-
* @param string $msg The original message (header and body)
242-
* @param string $signature The original signature
243-
* @param string|OpenSSLAsymmetricKey $key For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
244-
* @param string $alg The algorithm
259+
* @param string $msg The original message (header and body)
260+
* @param string $signature The original signature
261+
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed> $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
262+
* @param string $alg The algorithm
245263
*
246264
* @return bool
247265
*
@@ -250,7 +268,7 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
250268
private static function verify(
251269
string $msg,
252270
string $signature,
253-
string|OpenSSLAsymmetricKey $keyMaterial,
271+
string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial,
254272
string $alg
255273
): bool {
256274
if (empty(static::$supported_algs[$alg])) {
@@ -274,16 +292,22 @@ private static function verify(
274292
if (!function_exists('sodium_crypto_sign_verify_detached')) {
275293
throw new DomainException('libsodium is not available');
276294
}
295+
if (!is_string($keyMaterial)) {
296+
throw new InvalidArgumentException('key must be a string when using EdDSA');
297+
}
277298
try {
278299
// The last non-empty line is used as the key.
279300
$lines = array_filter(explode("\n", $keyMaterial));
280-
$key = base64_decode(end($lines));
301+
$key = base64_decode((string) end($lines));
281302
return sodium_crypto_sign_verify_detached($signature, $msg, $key);
282303
} catch (Exception $e) {
283304
throw new DomainException($e->getMessage(), 0, $e);
284305
}
285306
case 'hash_hmac':
286307
default:
308+
if (!is_string($keyMaterial)) {
309+
throw new InvalidArgumentException('key must be a string when using hmac');
310+
}
287311
$hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
288312
return self::constantTimeEquals($hash, $signature);
289313
}
@@ -303,7 +327,7 @@ public static function jsonDecode(string $input): mixed
303327
$obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
304328

305329
if ($errno = \json_last_error()) {
306-
static::handleJsonError($errno);
330+
self::handleJsonError($errno);
307331
} elseif ($obj === null && $input !== 'null') {
308332
throw new DomainException('Null result with non-null input');
309333
}
@@ -313,13 +337,13 @@ public static function jsonDecode(string $input): mixed
313337
/**
314338
* Encode a PHP array into a JSON string.
315339
*
316-
* @param array $input A PHP array
340+
* @param array<mixed> $input A PHP array
317341
*
318-
* @return string JSON representation of the PHP array
342+
* @return string|false JSON representation of the PHP array
319343
*
320344
* @throws DomainException Provided object could not be encoded to valid JSON
321345
*/
322-
public static function jsonEncode(array $input): string
346+
public static function jsonEncode(array $input): string|false
323347
{
324348
if (PHP_VERSION_ID >= 50400) {
325349
$json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
@@ -328,7 +352,7 @@ public static function jsonEncode(array $input): string
328352
$json = \json_encode($input);
329353
}
330354
if ($errno = \json_last_error()) {
331-
static::handleJsonError($errno);
355+
self::handleJsonError($errno);
332356
} elseif ($json === 'null' && $input !== null) {
333357
throw new DomainException('Null result with non-null input');
334358
}
@@ -342,7 +366,7 @@ public static function jsonEncode(array $input): string
342366
*
343367
* @return string A decoded string
344368
*/
345-
public static function urlsafeB64Decode(string $input): string
369+
public static function urlsafeB64Decode(string $input): string|false
346370
{
347371
$remainder = \strlen($input) % 4;
348372
if ($remainder) {
@@ -381,29 +405,22 @@ private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $ki
381405
return $keyOrKeyArray;
382406
}
383407

384-
if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) {
385-
foreach ($keyOrKeyArray as $keyId => $key) {
386-
if (!$key instanceof Key) {
387-
throw new TypeError(
388-
'$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
389-
. 'array of Firebase\JWT\Key keys'
390-
);
391-
}
392-
}
393-
if (!isset($kid)) {
394-
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
395-
}
396-
if (!isset($keyOrKeyArray[$kid])) {
397-
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
408+
foreach ($keyOrKeyArray as $keyId => $key) {
409+
if (!$key instanceof Key) {
410+
throw new TypeError(
411+
'$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
412+
. 'array of Firebase\JWT\Key keys'
413+
);
398414
}
399-
400-
return $keyOrKeyArray[$kid];
415+
}
416+
if (!isset($kid)) {
417+
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
418+
}
419+
if (!isset($keyOrKeyArray[$kid])) {
420+
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
401421
}
402422

403-
throw new UnexpectedValueException(
404-
'$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
405-
. 'array of Firebase\JWT\Key keys'
406-
);
423+
return $keyOrKeyArray[$kid];
407424
}
408425

409426
/**
@@ -416,13 +433,13 @@ public static function constantTimeEquals(string $left, string $right): bool
416433
if (\function_exists('hash_equals')) {
417434
return \hash_equals($left, $right);
418435
}
419-
$len = \min(static::safeStrlen($left), static::safeStrlen($right));
436+
$len = \min(self::safeStrlen($left), self::safeStrlen($right));
420437

421438
$status = 0;
422439
for ($i = 0; $i < $len; $i++) {
423440
$status |= (\ord($left[$i]) ^ \ord($right[$i]));
424441
}
425-
$status |= (static::safeStrlen($left) ^ static::safeStrlen($right));
442+
$status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
426443

427444
return ($status === 0);
428445
}
@@ -476,7 +493,8 @@ private static function safeStrlen(string $str): int
476493
private static function signatureToDER(string $sig): string
477494
{
478495
// Separate the signature into r-value and s-value
479-
list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2));
496+
$length = max(1, (int) (\strlen($sig) / 2));
497+
list($r, $s) = \str_split($sig, $length > 0 ? $length : 1);
480498

481499
// Trim leading zeros
482500
$r = \ltrim($r, "\x00");
@@ -556,7 +574,7 @@ private static function signatureFromDER(string $der, int $keySize): string
556574
* @param int $offset the offset of the data stream containing the object
557575
* to decode
558576
*
559-
* @return array [$offset, $data] the new offset and the decoded object
577+
* @return array{int, string|null} the new offset and the decoded object
560578
*/
561579
private static function readDER(string $der, int $offset = 0): array
562580
{

0 commit comments

Comments
 (0)