Skip to content

Commit 4674049

Browse files
committed
Merge branch '3.4.x' into 4.0.x
* 3.4.x: Update signature tests for the protected-header "alg" requirement (web-token#651) Merge commit from fork Merge commit from fork Merge commit from fork Merge commit from fork Add sodium support for Base64 URL safe encoding/decoding (web-token#644) Allow `psr/cache` v2 (web-token#620) Fix call function on null (web-token#596) Add RangeException to Base64UrlSafe (web-token#577) Add Base64UrlSafe utility and refactor code references (web-token#576) # Conflicts: # composer.json # phpstan-baseline.neon # src/Bundle/DataCollector/JWECollector.php # src/Experimental/KeyEncryption/AESCTR.php # src/Experimental/KeyEncryption/Chacha20Poly1305.php # src/Experimental/Signature/Blake2b.php # src/Library/Console/GeneratorCommand.php # src/Library/Core/JWK.php # src/Library/Core/Util/Base64UrlSafe.php # src/Library/Encryption/Algorithm/ContentEncryption/AESCBCHS.php # src/Library/Encryption/Algorithm/ContentEncryption/AESGCM.php # src/Library/Encryption/Algorithm/KeyEncryption/AESGCMKW.php # src/Library/Encryption/Algorithm/KeyEncryption/AESKW.php # src/Library/Encryption/Algorithm/KeyEncryption/AbstractECDH.php # src/Library/Encryption/Algorithm/KeyEncryption/Dir.php # src/Library/Encryption/Algorithm/KeyEncryption/PBES2AESKW.php # src/Library/Encryption/Algorithm/KeyEncryption/RSA15.php # src/Library/Encryption/Algorithm/KeyEncryption/Util/ConcatKDF.php # src/Library/Encryption/Algorithm/KeyEncryption/Util/RSACrypt.php # src/Library/Encryption/Serializer/CompactSerializer.php # src/Library/Encryption/Serializer/JSONFlattenedSerializer.php # src/Library/Encryption/Serializer/JSONGeneralSerializer.php # src/Library/KeyManagement/Analyzer/ESKeyAnalyzer.php # src/Library/KeyManagement/Analyzer/HSKeyAnalyzer.php # src/Library/KeyManagement/Analyzer/OctAnalyzer.php # src/Library/KeyManagement/Analyzer/RsaAnalyzer.php # src/Library/KeyManagement/Analyzer/ZxcvbnKeyAnalyzer.php # src/Library/Signature/Algorithm/EdDSA.php # src/Library/Signature/Algorithm/HMAC.php # src/Library/Signature/Serializer/CompactSerializer.php # src/Library/Signature/Serializer/JSONFlattenedSerializer.php # src/Library/Signature/Serializer/JSONGeneralSerializer.php # src/Library/composer.json # tests/Bundle/JoseFramework/Functional/Encryption/JWECollectorTest.php # tests/Bundle/JoseFramework/Functional/KeyManagement/JWKLoaderTest.php # tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionWithCompressionTest.php # tests/Component/KeyManagement/JWKFactoryTest.php
2 parents 1f39614 + fb6d524 commit 4674049

12 files changed

Lines changed: 512 additions & 41 deletions

File tree

src/Experimental/KeyEncryption/Chacha20Poly1305.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ public function encryptKey(JWK $key, string $cek, array $completeHeader, array &
4747
$k = $this->getKey($key);
4848
$nonce = random_bytes(12);
4949

50-
// We set header parameters
51-
$additionalHeader['nonce'] = Base64UrlSafe::encodeUnpadded($nonce);
52-
5350
$tag = null;
5451
$result = openssl_encrypt($cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce, $tag);
55-
if ($result === false || ! is_string($tag)) {
52+
if ($result === false || ! is_string($tag) || strlen($tag) !== 16) {
5653
throw new RuntimeException('Unable to encrypt the CEK');
5754
}
5855

56+
$additionalHeader['nonce'] = Base64UrlSafe::encodeUnpadded($nonce);
57+
$additionalHeader['tag'] = Base64UrlSafe::encodeUnpadded($tag);
58+
5959
return $result;
6060
}
6161

@@ -72,8 +72,14 @@ public function decryptKey(JWK $key, string $encrypted_cek, array $header): stri
7272
if (strlen($nonce) !== 12) {
7373
throw new InvalidArgumentException('The header parameter "nonce" is not valid.');
7474
}
75+
isset($header['tag']) || throw new InvalidArgumentException('The header parameter "tag" is missing.');
76+
is_string($header['tag']) || throw new InvalidArgumentException('The header parameter "tag" is not valid.');
77+
$tag = Base64UrlSafe::decodeNoPadding($header['tag']);
78+
if (strlen($tag) !== 16) {
79+
throw new InvalidArgumentException('The header parameter "tag" is not valid.');
80+
}
7581

76-
$result = openssl_decrypt($encrypted_cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce);
82+
$result = openssl_decrypt($encrypted_cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce, $tag);
7783
if ($result === false) {
7884
throw new RuntimeException('Unable to decrypt the CEK');
7985
}

src/Library/Core/Util/Base64UrlSafe.php

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@
66

77
use InvalidArgumentException;
88
use RangeException;
9+
use SensitiveParameter;
10+
use SodiumException;
11+
use function extension_loaded;
12+
use function pack;
13+
use function rtrim;
14+
use function sodium_base642bin;
15+
use function sodium_bin2base64;
916
use function strlen;
17+
use function substr;
18+
use function unpack;
19+
use const SODIUM_BASE64_VARIANT_URLSAFE;
20+
use const SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING;
1021

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

3445
final readonly class Base64UrlSafe
3546
{
36-
public static function encode(string $binString): string
47+
public static function encode(#[SensitiveParameter] string $binString): string
3748
{
49+
if (extension_loaded('sodium')) {
50+
try {
51+
return sodium_bin2base64($binString, SODIUM_BASE64_VARIANT_URLSAFE);
52+
} catch (SodiumException $ex) {
53+
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
54+
}
55+
}
3856
return static::doEncode($binString, true);
3957
}
4058

41-
public static function encodeUnpadded(string $src): string
59+
public static function encodeUnpadded(#[SensitiveParameter] string $src): string
4260
{
61+
if (extension_loaded('sodium')) {
62+
try {
63+
return sodium_bin2base64($src, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
64+
} catch (SodiumException $ex) {
65+
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
66+
}
67+
}
4368
return static::doEncode($src, false);
4469
}
4570

46-
public static function decode(string $encodedString, bool $strictPadding = false): string
71+
public static function decode(#[SensitiveParameter] string $encodedString, bool $strictPadding = false): string
4772
{
4873
$srcLen = self::safeStrlen($encodedString);
4974
if ($srcLen === 0) {
@@ -65,6 +90,16 @@ public static function decode(string $encodedString, bool $strictPadding = false
6590
if ($encodedString[$srcLen - 1] === '=') {
6691
throw new RangeException('Incorrect padding');
6792
}
93+
if (extension_loaded('sodium')) {
94+
try {
95+
return sodium_base642bin(
96+
self::safeSubstr($encodedString, 0, $srcLen),
97+
SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING
98+
);
99+
} catch (SodiumException $ex) {
100+
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
101+
}
102+
}
68103
} else {
69104
$encodedString = rtrim($encodedString, '=');
70105
$srcLen = self::safeStrlen($encodedString);
@@ -120,26 +155,21 @@ public static function decode(string $encodedString, bool $strictPadding = false
120155
return $dest;
121156
}
122157

123-
public static function decodeNoPadding(string $encodedString): string
158+
public static function decodeNoPadding(#[SensitiveParameter] string $encodedString): string
124159
{
125160
$srcLen = self::safeStrlen($encodedString);
126161
if ($srcLen === 0) {
127162
return '';
128163
}
129164
if (($srcLen & 3) === 0) {
130-
if ($encodedString[$srcLen - 1] === '=') {
165+
if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') {
131166
throw new InvalidArgumentException("decodeNoPadding() doesn't tolerate padding");
132167
}
133-
if (($srcLen & 3) > 1) {
134-
if ($encodedString[$srcLen - 2] === '=') {
135-
throw new InvalidArgumentException("decodeNoPadding() doesn't tolerate padding");
136-
}
137-
}
138168
}
139169
return static::decode($encodedString, true);
140170
}
141171

142-
private static function doEncode(string $src, bool $pad = true): string
172+
private static function doEncode(#[SensitiveParameter] string $src, bool $pad = true): string
143173
{
144174
$dest = '';
145175
$srcLen = self::safeStrlen($src);
@@ -204,12 +234,12 @@ private static function encode6Bits(int $src): string
204234
return pack('C', $src + $diff);
205235
}
206236

207-
private static function safeStrlen(string $str): int
237+
private static function safeStrlen(#[SensitiveParameter] string $str): int
208238
{
209239
return strlen($str);
210240
}
211241

212-
private static function safeSubstr(string $str, int $start = 0, $length = null): string
242+
private static function safeSubstr(#[SensitiveParameter] string $str, int $start = 0, $length = null): string
213243
{
214244
if ($length === 0) {
215245
return '';

src/Library/Encryption/Algorithm/KeyEncryption/PBES2AESKW.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
use function in_array;
1717
use function is_int;
1818
use function is_string;
19+
use function sprintf;
1920

2021
abstract readonly class PBES2AESKW implements KeyWrapping
2122
{
23+
public const DEFAULT_MAX_COUNT = 1_000_000;
24+
2225
public function __construct(
2326
private readonly int $salt_size = 64,
24-
private readonly int $nb_count = 4096
27+
private readonly int $nb_count = 4096,
28+
private readonly int $max_count = self::DEFAULT_MAX_COUNT
2529
) {
2630
if (! interface_exists(WrapperInterface::class)) {
2731
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
@@ -139,6 +143,12 @@ protected function checkHeaderAdditionalParameters(array $header): void
139143
if (! is_int($header['p2c']) || $header['p2c'] <= 0) {
140144
throw new InvalidArgumentException('The header parameter "p2c" is not valid.');
141145
}
146+
if ($header['p2c'] > $this->max_count) {
147+
throw new InvalidArgumentException(sprintf(
148+
'The header parameter "p2c" is too large. The maximum allowed value is %d.',
149+
$this->max_count
150+
));
151+
}
142152
}
143153

144154
abstract protected function getWrapper(): A256KW|A128KW|A192KW;

src/Library/Encryption/Algorithm/KeyEncryption/RSA15.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,54 @@
44

55
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
66

7+
use InvalidArgumentException;
8+
use Jose\Component\Core\JWK;
9+
use Jose\Component\Core\Util\RSAKey;
710
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\RSACrypt;
811
use Override;
12+
use function is_string;
913

1014
final readonly class RSA15 extends RSA
1115
{
16+
/**
17+
* @var array<string, int>
18+
*/
19+
private const CEK_LENGTHS = [
20+
'A128GCM' => 16,
21+
'A192GCM' => 24,
22+
'A256GCM' => 32,
23+
'A128CBC-HS256' => 32,
24+
'A192CBC-HS384' => 48,
25+
'A256CBC-HS512' => 64,
26+
];
27+
1228
#[Override]
1329
public function name(): string
1430
{
1531
return 'RSA1_5';
1632
}
1733

34+
/**
35+
* @param array<string, mixed> $header
36+
*/
37+
#[Override]
38+
public function decryptKey(JWK $key, string $encrypted_cek, array $header): string
39+
{
40+
$this->checkKey($key);
41+
if (! $key->has('d')) {
42+
throw new InvalidArgumentException('The key is not a private key');
43+
}
44+
$priv = RSAKey::createFromJWK($key);
45+
46+
return RSACrypt::decrypt(
47+
$priv,
48+
$encrypted_cek,
49+
RSACrypt::ENCRYPTION_PKCS1,
50+
null,
51+
$this->getExpectedCekLength($header)
52+
);
53+
}
54+
1855
#[Override]
1956
protected function getEncryptionMode(): int
2057
{
@@ -26,4 +63,17 @@ protected function getHashAlgorithm(): ?string
2663
{
2764
return null;
2865
}
66+
67+
/**
68+
* @param array<string, mixed> $header
69+
*/
70+
private function getExpectedCekLength(array $header): ?int
71+
{
72+
$enc = $header['enc'] ?? null;
73+
if (! is_string($enc)) {
74+
return null;
75+
}
76+
77+
return self::CEK_LENGTHS[$enc] ?? null;
78+
}
2979
}

src/Library/Encryption/Algorithm/KeyEncryption/Util/RSACrypt.php

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@ public static function encrypt(RSAKey $key, string $data, int $mode, ?string $ha
4747
}
4848
}
4949

50-
public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?string $hash = null): string
51-
{
50+
public static function decrypt(
51+
RSAKey $key,
52+
string $plaintext,
53+
int $mode,
54+
?string $hash = null,
55+
?int $expectedKeyLength = null
56+
): string {
5257
switch ($mode) {
5358
case self::ENCRYPTION_OAEP:
5459
if ($hash === null) {
@@ -57,7 +62,7 @@ public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?strin
5762

5863
return self::decryptWithRSAOAEP($key, $plaintext, $hash);
5964
case self::ENCRYPTION_PKCS1:
60-
return self::decryptWithRSA15($key, $plaintext);
65+
return self::decryptWithRSA15($key, $plaintext, $expectedKeyLength);
6166
default:
6267
throw new InvalidArgumentException('Unsupported mode.');
6368
}
@@ -86,24 +91,79 @@ public static function encryptWithRSA15(RSAKey $key, string $data): string
8691
return self::convertIntegerToOctetString($c, $key->getModulusLength());
8792
}
8893

89-
public static function decryptWithRSA15(RSAKey $key, string $c): string
94+
public static function decryptWithRSA15(RSAKey $key, string $c, ?int $expectedKeyLength = null): string
9095
{
9196
if (strlen($c) !== $key->getModulusLength()) {
9297
throw new InvalidArgumentException('Unable to decrypt');
9398
}
9499
$c = BigInteger::createFromBinaryString($c);
95100
$m = self::getRSADP($key, $c);
96101
$em = self::convertIntegerToOctetString($m, $key->getModulusLength());
97-
if (ord($em[0]) !== 0 || ord($em[1]) > 2) {
98-
throw new InvalidArgumentException('Unable to decrypt');
102+
if ($expectedKeyLength === null) {
103+
if (ord($em[0]) !== 0 || ord($em[1]) > 2) {
104+
throw new InvalidArgumentException('Unable to decrypt');
105+
}
106+
$ps = substr($em, 2, (int) strpos($em, chr(0), 2) - 2);
107+
$m = substr($em, strlen($ps) + 3);
108+
if (strlen($ps) < 8) {
109+
throw new InvalidArgumentException('Unable to decrypt');
110+
}
111+
112+
return $m;
99113
}
100-
$ps = substr($em, 2, (int) strpos($em, chr(0), 2) - 2);
101-
$m = substr($em, strlen($ps) + 3, null);
102-
if (strlen($ps) < 8) {
103-
throw new InvalidArgumentException('Unable to decrypt');
114+
115+
return self::extractRSA15KeyOrRandom($em, $expectedKeyLength);
116+
}
117+
118+
private static function extractRSA15KeyOrRandom(string $em, int $expectedKeyLength): string
119+
{
120+
$k = strlen($em);
121+
$random = random_bytes($expectedKeyLength);
122+
123+
if ($k < $expectedKeyLength + 11) {
124+
return $random;
125+
}
126+
$candidate = substr($em, $k - $expectedKeyLength);
127+
128+
$valid = self::ctEq(ord($em[0]), 0x00) & self::ctEq(ord($em[1]), 0x02);
129+
130+
$seenSeparator = 0;
131+
$separatorIndex = 0;
132+
$psLength = 0;
133+
for ($i = 2; $i < $k; ++$i) {
134+
$isZero = self::ctEq(ord($em[$i]), 0x00);
135+
$firstZero = $isZero & (1 - $seenSeparator);
136+
$separatorIndex |= $firstZero * $i;
137+
$psLength += (1 - $seenSeparator) & (1 - $isZero);
138+
$seenSeparator |= $isZero;
104139
}
105140

106-
return $m;
141+
$valid &= $seenSeparator;
142+
$valid &= self::ctGe($psLength, 8);
143+
144+
$messageLength = $k - $separatorIndex - 1;
145+
$valid &= self::ctEq($messageLength, $expectedKeyLength);
146+
147+
return self::ctSelect($valid, $candidate, $random);
148+
}
149+
150+
private static function ctEq(int $a, int $b): int
151+
{
152+
$diff = $a ^ $b;
153+
154+
return (($diff - 1) >> 63) & 1;
155+
}
156+
157+
private static function ctGe(int $a, int $b): int
158+
{
159+
return (($b - $a - 1) >> 63) & 1;
160+
}
161+
162+
private static function ctSelect(int $condition, string $a, string $b): string
163+
{
164+
$mask = str_repeat(chr(($condition * 0xFF) & 0xFF), strlen($a));
165+
166+
return ($a & $mask) | ($b & ~$mask);
107167
}
108168

109169
/**

src/Library/Signature/JWSVerifier.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,17 @@ private function checkPayload(JWS $jws, ?string $detachedPayload = null): void
141141
*/
142142
private function getAlgorithm(Signature $signature): Algorithm
143143
{
144-
$completeHeader = [...$signature->getProtectedHeader(), ...$signature->getHeader()];
145-
if (! isset($completeHeader['alg'])) {
146-
throw new InvalidArgumentException('No "alg" parameter set in the header.');
144+
$protectedHeader = $signature->getProtectedHeader();
145+
if (! isset($protectedHeader['alg'])) {
146+
throw new InvalidArgumentException('No "alg" parameter set in the protected header.');
147147
}
148+
$alg = $protectedHeader['alg'];
148149

149-
$algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']);
150+
$algorithm = $this->signatureAlgorithmManager->get($alg);
150151
if (! $algorithm instanceof SignatureAlgorithm && ! $algorithm instanceof MacAlgorithm) {
151152
throw new InvalidArgumentException(sprintf(
152153
'The algorithm "%s" is not supported or is not a signature or MAC algorithm.',
153-
$completeHeader['alg']
154+
$alg
154155
));
155156
}
156157

0 commit comments

Comments
 (0)