Skip to content
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) {

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

View workflow job for this annotation

GitHub Actions / 3️⃣ Static Analysis

Result of || is always true.

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

View workflow job for this annotation

GitHub Actions / 3️⃣ Static Analysis

Ignored error pattern #^Result of \|\| is always true\.$# (booleanOr.alwaysTrue) in path /home/runner/work/jwt-framework/jwt-framework/src/Experimental/KeyEncryption/Chacha20Poly1305.php is expected to occur 1 time, but occurred 2 times.
throw new RuntimeException('Unable to encrypt the CEK');
}

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

return $result;
}

Expand All @@ -72,8 +72,14 @@
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.');
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
56 changes: 43 additions & 13 deletions src/Library/Core/Util/Base64UrlSafe.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@

use InvalidArgumentException;
use RangeException;
use SensitiveParameter;
use SodiumException;
use function extension_loaded;
use function pack;
use function rtrim;
use function sodium_base642bin;
use function sodium_bin2base64;
use function strlen;
use function substr;
use function unpack;
use const SODIUM_BASE64_VARIANT_URLSAFE;
use const SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING;

/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
Expand All @@ -33,17 +44,31 @@

final readonly class Base64UrlSafe
{
public static function encode(string $binString): string
public static function encode(#[SensitiveParameter] string $binString): string
{
if (extension_loaded('sodium')) {
try {
return sodium_bin2base64($binString, SODIUM_BASE64_VARIANT_URLSAFE);
} catch (SodiumException $ex) {
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
}
}
return static::doEncode($binString, true);
}

public static function encodeUnpadded(string $src): string
public static function encodeUnpadded(#[SensitiveParameter] string $src): string
{
if (extension_loaded('sodium')) {
try {
return sodium_bin2base64($src, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
} catch (SodiumException $ex) {
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
}
}
return static::doEncode($src, false);
}

public static function decode(string $encodedString, bool $strictPadding = false): string
public static function decode(#[SensitiveParameter] string $encodedString, bool $strictPadding = false): string
{
$srcLen = self::safeStrlen($encodedString);
if ($srcLen === 0) {
Expand All @@ -65,6 +90,16 @@ public static function decode(string $encodedString, bool $strictPadding = false
if ($encodedString[$srcLen - 1] === '=') {
throw new RangeException('Incorrect padding');
}
if (extension_loaded('sodium')) {
try {
return sodium_base642bin(
self::safeSubstr($encodedString, 0, $srcLen),
SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING
);
} catch (SodiumException $ex) {
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
}
}
} else {
$encodedString = rtrim($encodedString, '=');
$srcLen = self::safeStrlen($encodedString);
Expand Down Expand Up @@ -120,26 +155,21 @@ public static function decode(string $encodedString, bool $strictPadding = false
return $dest;
}

public static function decodeNoPadding(string $encodedString): string
public static function decodeNoPadding(#[SensitiveParameter] string $encodedString): string
{
$srcLen = self::safeStrlen($encodedString);
if ($srcLen === 0) {
return '';
}
if (($srcLen & 3) === 0) {
if ($encodedString[$srcLen - 1] === '=') {
if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') {
throw new InvalidArgumentException("decodeNoPadding() doesn't tolerate padding");
}
if (($srcLen & 3) > 1) {
if ($encodedString[$srcLen - 2] === '=') {
throw new InvalidArgumentException("decodeNoPadding() doesn't tolerate padding");
}
}
}
return static::decode($encodedString, true);
}

private static function doEncode(string $src, bool $pad = true): string
private static function doEncode(#[SensitiveParameter] string $src, bool $pad = true): string
{
$dest = '';
$srcLen = self::safeStrlen($src);
Expand Down Expand Up @@ -204,12 +234,12 @@ private static function encode6Bits(int $src): string
return pack('C', $src + $diff);
}

private static function safeStrlen(string $str): int
private static function safeStrlen(#[SensitiveParameter] string $str): int
{
return strlen($str);
}

private static function safeSubstr(string $str, int $start = 0, $length = null): string
private static function safeSubstr(#[SensitiveParameter] string $str, int $start = 0, $length = null): string
{
if ($length === 0) {
return '';
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, null);
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
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
Loading
Loading