Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 0 additions & 222 deletions .ci-tools/phpstan-baseline.neon

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"require": {
"php": ">=8.2",
"ext-openssl": "*",
"brick/math": "^0.12|^0.13|^0.14",
"brick/math": "^0.12|^0.13|^0.14|^0.15|^0.16|^0.17",
"psr/clock": "^1.0",
"psr/event-dispatcher": "^1.0",
"spomky-labs/pki-framework": "^1.2.1",
Expand Down Expand Up @@ -92,6 +92,5 @@
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}
}
16 changes: 11 additions & 5 deletions src/Experimental/KeyEncryption/Chacha20Poly1305.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@
$k = $this->getKey($key);
$nonce = random_bytes(12);

// We set header parameters
$additionalHeader['nonce'] = Base64UrlSafe::encodeUnpadded($nonce);

$tag = null;
$result = openssl_encrypt($cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce, $tag);
if ($result === false || ! is_string($tag)) {
if ($result === false || ! is_string($tag) || strlen($tag) !== 16) {
throw new RuntimeException('Unable to encrypt the CEK');
}

$additionalHeader['nonce'] = Base64UrlSafe::encodeUnpadded($nonce);
$additionalHeader['tag'] = Base64UrlSafe::encodeUnpadded($tag);

return $result;
}

Expand All @@ -66,14 +66,20 @@
public function decryptKey(JWK $key, string $encrypted_cek, array $header): string
{
$k = $this->getKey($key);
isset($header['nonce']) || throw new InvalidArgumentException('The header parameter "nonce" is missing.');

Check failure on line 69 in src/Experimental/KeyEncryption/Chacha20Poly1305.php

View workflow job for this annotation

GitHub Actions / 1️⃣ Static Analysis (PHPStan)

Ignored error "Language construct isset() should not be used." (ergebnis.noIsset) in path /__w/jwt-framework/jwt-framework/src/Experimental/KeyEncryption/Chacha20Poly1305.php is expected to occur 1 time, but occurred 2 times.

Check failure on line 69 in src/Experimental/KeyEncryption/Chacha20Poly1305.php

View workflow job for this annotation

GitHub Actions / 1️⃣ Static Analysis (PHPStan)

Ignored error "Language construct isset() should not be used." (ergebnis.noIsset) in path /__w/jwt-framework/jwt-framework/src/Experimental/KeyEncryption/Chacha20Poly1305.php is expected to occur 1 time, but occurred 2 times.
is_string($header['nonce']) || throw new InvalidArgumentException('The header parameter "nonce" is not valid.');
$nonce = Base64UrlSafe::decodeNoPadding($header['nonce']);
if (strlen($nonce) !== 12) {
throw new InvalidArgumentException('The header parameter "nonce" is not valid.');
}
isset($header['tag']) || throw new InvalidArgumentException('The header parameter "tag" is missing.');

Check failure on line 75 in src/Experimental/KeyEncryption/Chacha20Poly1305.php

View workflow job for this annotation

GitHub Actions / 1️⃣ Static Analysis (PHPStan)

Language construct isset() should not be used.

Check failure on line 75 in src/Experimental/KeyEncryption/Chacha20Poly1305.php

View workflow job for this annotation

GitHub Actions / 1️⃣ Static Analysis (PHPStan)

Language construct isset() should not be used.
is_string($header['tag']) || throw new InvalidArgumentException('The header parameter "tag" is not valid.');
$tag = Base64UrlSafe::decodeNoPadding($header['tag']);
if (strlen($tag) !== 16) {
throw new InvalidArgumentException('The header parameter "tag" is not valid.');
}

$result = openssl_decrypt($encrypted_cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce);
$result = openssl_decrypt($encrypted_cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce, $tag);
if ($result === false) {
throw new RuntimeException('Unable to decrypt the CEK');
}
Expand Down
12 changes: 11 additions & 1 deletion src/Library/Encryption/Algorithm/KeyEncryption/PBES2AESKW.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
use function in_array;
use function is_int;
use function is_string;
use function sprintf;

abstract readonly class PBES2AESKW implements KeyWrapping
{
public const DEFAULT_MAX_COUNT = 1_000_000;

public function __construct(
private readonly int $salt_size = 64,
private readonly int $nb_count = 4096
private readonly int $nb_count = 4096,
private readonly int $max_count = self::DEFAULT_MAX_COUNT
) {
if (! interface_exists(WrapperInterface::class)) {
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
Expand Down Expand Up @@ -139,6 +143,12 @@ protected function checkHeaderAdditionalParameters(array $header): void
if (! is_int($header['p2c']) || $header['p2c'] <= 0) {
throw new InvalidArgumentException('The header parameter "p2c" is not valid.');
}
if ($header['p2c'] > $this->max_count) {
throw new InvalidArgumentException(sprintf(
'The header parameter "p2c" is too large. The maximum allowed value is %d.',
$this->max_count
));
}
}

abstract protected function getWrapper(): A256KW|A128KW|A192KW;
Expand Down
50 changes: 50 additions & 0 deletions src/Library/Encryption/Algorithm/KeyEncryption/RSA15.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,54 @@

namespace Jose\Component\Encryption\Algorithm\KeyEncryption;

use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\RSAKey;
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\RSACrypt;
use Override;
use function is_string;

final readonly class RSA15 extends RSA
{
/**
* @var array<string, int>
*/
private const CEK_LENGTHS = [
'A128GCM' => 16,
'A192GCM' => 24,
'A256GCM' => 32,
'A128CBC-HS256' => 32,
'A192CBC-HS384' => 48,
'A256CBC-HS512' => 64,
];

#[Override]
public function name(): string
{
return 'RSA1_5';
}

/**
* @param array<string, mixed> $header
*/
#[Override]
public function decryptKey(JWK $key, string $encrypted_cek, array $header): string
{
$this->checkKey($key);
if (! $key->has('d')) {
throw new InvalidArgumentException('The key is not a private key');
}
$priv = RSAKey::createFromJWK($key);

return RSACrypt::decrypt(
$priv,
$encrypted_cek,
RSACrypt::ENCRYPTION_PKCS1,
null,
$this->getExpectedCekLength($header)
);
}

#[Override]
protected function getEncryptionMode(): int
{
Expand All @@ -26,4 +63,17 @@ protected function getHashAlgorithm(): ?string
{
return null;
}

/**
* @param array<string, mixed> $header
*/
private function getExpectedCekLength(array $header): ?int
{
$enc = $header['enc'] ?? null;
if (! is_string($enc)) {
return null;
}

return self::CEK_LENGTHS[$enc] ?? null;
}
}
82 changes: 71 additions & 11 deletions src/Library/Encryption/Algorithm/KeyEncryption/Util/RSACrypt.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ public static function encrypt(RSAKey $key, string $data, int $mode, ?string $ha
}
}

public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?string $hash = null): string
{
public static function decrypt(
RSAKey $key,
string $plaintext,
int $mode,
?string $hash = null,
?int $expectedKeyLength = null
): string {
switch ($mode) {
case self::ENCRYPTION_OAEP:
if ($hash === null) {
Expand All @@ -57,7 +62,7 @@ public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?strin

return self::decryptWithRSAOAEP($key, $plaintext, $hash);
case self::ENCRYPTION_PKCS1:
return self::decryptWithRSA15($key, $plaintext);
return self::decryptWithRSA15($key, $plaintext, $expectedKeyLength);
default:
throw new InvalidArgumentException('Unsupported mode.');
}
Expand Down Expand Up @@ -86,24 +91,79 @@ public static function encryptWithRSA15(RSAKey $key, string $data): string
return self::convertIntegerToOctetString($c, $key->getModulusLength());
}

public static function decryptWithRSA15(RSAKey $key, string $c): string
public static function decryptWithRSA15(RSAKey $key, string $c, ?int $expectedKeyLength = null): string
{
if (strlen($c) !== $key->getModulusLength()) {
throw new InvalidArgumentException('Unable to decrypt');
}
$c = BigInteger::createFromBinaryString($c);
$m = self::getRSADP($key, $c);
$em = self::convertIntegerToOctetString($m, $key->getModulusLength());
if (ord($em[0]) !== 0 || ord($em[1]) > 2) {
throw new InvalidArgumentException('Unable to decrypt');
if ($expectedKeyLength === null) {
if (ord($em[0]) !== 0 || ord($em[1]) > 2) {
throw new InvalidArgumentException('Unable to decrypt');
}
$ps = substr($em, 2, (int) strpos($em, chr(0), 2) - 2);
$m = substr($em, strlen($ps) + 3);
if (strlen($ps) < 8) {
throw new InvalidArgumentException('Unable to decrypt');
}

return $m;
}
$ps = substr($em, 2, (int) strpos($em, chr(0), 2) - 2);
$m = substr($em, strlen($ps) + 3);
if (strlen($ps) < 8) {
throw new InvalidArgumentException('Unable to decrypt');

return self::extractRSA15KeyOrRandom($em, $expectedKeyLength);
}

private static function extractRSA15KeyOrRandom(string $em, int $expectedKeyLength): string
{
$k = strlen($em);
$random = random_bytes($expectedKeyLength);

if ($k < $expectedKeyLength + 11) {
return $random;
}
$candidate = substr($em, $k - $expectedKeyLength);

$valid = self::ctEq(ord($em[0]), 0x00) & self::ctEq(ord($em[1]), 0x02);

$seenSeparator = 0;
$separatorIndex = 0;
$psLength = 0;
for ($i = 2; $i < $k; ++$i) {
$isZero = self::ctEq(ord($em[$i]), 0x00);
$firstZero = $isZero & (1 - $seenSeparator);
$separatorIndex |= $firstZero * $i;
$psLength += (1 - $seenSeparator) & (1 - $isZero);
$seenSeparator |= $isZero;
}

return $m;
$valid &= $seenSeparator;
$valid &= self::ctGe($psLength, 8);

$messageLength = $k - $separatorIndex - 1;
$valid &= self::ctEq($messageLength, $expectedKeyLength);

return self::ctSelect($valid, $candidate, $random);
}

private static function ctEq(int $a, int $b): int
{
$diff = $a ^ $b;

return (($diff - 1) >> 63) & 1;
}

private static function ctGe(int $a, int $b): int
{
return (($b - $a - 1) >> 63) & 1;
}

private static function ctSelect(int $condition, string $a, string $b): string
{
$mask = str_repeat(chr(($condition * 0xFF) & 0xFF), strlen($a));

return ($a & $mask) | ($b & ~$mask);
}

/**
Expand Down
10 changes: 7 additions & 3 deletions src/Library/Signature/Algorithm/Util/RSA.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ private static function encodeEMSAPSS(string $message, int $modulusLength, Hash
$db = $ps . chr(1) . $salt;
$dbMask = self::getMGF1($h, $emLen - $hash->getLength() - 1, $hash);
$maskedDB = $db ^ $dbMask;
$maskedDB[0] = ~chr(0xFF << ($modulusLength & 7)) & $maskedDB[0];
// PHP 8.5 Compatibility: Constrain value to 0-255 before passing to chr()
$shiftBits = $modulusLength & 7;
$maskedDB[0] = ~chr((0xFF << $shiftBits) & 0xFF) & $maskedDB[0];

return $maskedDB . $h . chr(0xBC);
}
Expand All @@ -168,13 +170,15 @@ private static function verifyEMSAPSS(string $m, string $em, int $emBits, Hash $
}
$maskedDB = substr($em, 0, -$hash->getLength() - 1);
$h = substr($em, -$hash->getLength() - 1, $hash->getLength());
$temp = chr(0xFF << ($emBits & 7));
// PHP 8.5 Compatibility: Constrain value to 0-255 before passing to chr()
$shiftBits = $emBits & 7;
$temp = chr((0xFF << $shiftBits) & 0xFF);
if ((~$maskedDB[0] & $temp) !== $temp) {
throw new InvalidArgumentException();
}
$dbMask = self::getMGF1($h, $emLen - $hash->getLength() - 1, $hash/*MGF*/);
$db = $maskedDB ^ $dbMask;
$db[0] = ~chr(0xFF << ($emBits & 7)) & $db[0];
$db[0] = ~chr((0xFF << $shiftBits) & 0xFF) & $db[0];
$temp = $emLen - $hash->getLength() - $sLen - 2;
if (substr($db, 0, $temp) !== str_repeat(chr(0), $temp)) {
throw new InvalidArgumentException();
Expand Down
11 changes: 6 additions & 5 deletions src/Library/Signature/JWSVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,17 @@ private function checkPayload(JWS $jws, ?string $detachedPayload = null): void
*/
private function getAlgorithm(Signature $signature): Algorithm
{
$completeHeader = [...$signature->getProtectedHeader(), ...$signature->getHeader()];
if (! isset($completeHeader['alg'])) {
throw new InvalidArgumentException('No "alg" parameter set in the header.');
$protectedHeader = $signature->getProtectedHeader();
if (! isset($protectedHeader['alg'])) {
throw new InvalidArgumentException('No "alg" parameter set in the protected header.');
}
$alg = $protectedHeader['alg'];

$algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']);
$algorithm = $this->signatureAlgorithmManager->get($alg);
if (! $algorithm instanceof SignatureAlgorithm && ! $algorithm instanceof MacAlgorithm) {
throw new InvalidArgumentException(sprintf(
'The algorithm "%s" is not supported or is not a signature or MAC algorithm.',
$completeHeader['alg']
$alg
));
}

Expand Down
2 changes: 1 addition & 1 deletion src/Library/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
"require": {
"php": ">=8.2",
"brick/math": "^0.12|^0.13|^0.14",
"brick/math": "^0.12|^0.13|^0.14|^0.15|^0.16|^0.17",
"psr/clock": "^1.0",
"spomky-labs/pki-framework": "^1.2.1"
},
Expand Down
Loading
Loading