Skip to content

Commit 89dc4ff

Browse files
Update Jwe decryption service and add tests (#6)
* Update Jwe decryption service and add test * Add test for payload is null check * Add disable phpcs linter for multiline constructor
1 parent cbb4e9f commit 89dc4ff

File tree

4 files changed

+254
-27
lines changed

4 files changed

+254
-27
lines changed

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"vimeo/psalm": "^5.8",
3636
"phpstan/phpstan": "^1.10",
3737
"squizlabs/php_codesniffer": "^3.7",
38-
"slevomat/coding-standard": "^8.8"
38+
"slevomat/coding-standard": "^8.8",
39+
"ext-openssl": "*"
3940
},
4041
"scripts": {
4142
"test": [

src/OpenIDConnectServiceProvider.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Foundation\Application;
88
use Illuminate\Support\Facades\Route;
99
use Illuminate\Support\ServiceProvider;
10+
use Jose\Component\KeyManagement\JWKFactory;
1011
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponse;
1112
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponseInterface;
1213
use MinVWS\OpenIDConnectLaravel\OpenIDConfiguration\OpenIDConfigurationLoader;
@@ -94,8 +95,16 @@ protected function registerClient(): void
9495

9596
protected function registerJweDecryptInterface(): void
9697
{
98+
if (empty(config('oidc.decryption_key_path'))) {
99+
$this->app->singleton(JweDecryptInterface::class, function () {
100+
return null;
101+
});
102+
return;
103+
}
104+
97105
$this->app->singleton(JweDecryptInterface::class, function (Application $app) {
98-
return new JweDecryptService(decryptionKeyPath: $app['config']->get('oidc.decryption_key_path'));
106+
$jwk = JWKFactory::createFromKeyFile($app['config']->get('oidc.decryption_key_path'));
107+
return new JweDecryptService(decryptionKey: $jwk);
99108
});
100109
}
101110

src/Services/JWE/JweDecryptService.php

+16-25
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,36 @@
1313
use Jose\Component\Encryption\JWEDecrypter;
1414
use Jose\Component\Encryption\Serializer\CompactSerializer;
1515
use Jose\Component\Encryption\Serializer\JWESerializerManager;
16-
use Jose\Component\KeyManagement\JWKFactory;
1716

1817
class JweDecryptService implements JweDecryptInterface
1918
{
19+
/**
20+
* @param JWK $decryptionKey
21+
* @param JWESerializerManager $serializerManager
22+
* @param JWEDecrypter $jweDecrypter
23+
* phpcs:disable Squiz.Functions.MultiLineFunctionDeclaration.Indent -- waiting for phpcs 3.8.0
24+
*/
2025
public function __construct(
21-
protected string $decryptionKeyPath,
26+
protected JWK $decryptionKey,
27+
protected JWESerializerManager $serializerManager = new JWESerializerManager([new CompactSerializer()]),
28+
protected JWEDecrypter $jweDecrypter = new JWEDecrypter(
29+
new AlgorithmManager([new RSAOAEP()]),
30+
new AlgorithmManager([new A128CBCHS256()]),
31+
new CompressionMethodManager([new Deflate()])
32+
),
2233
) {
2334
}
2435

2536
/**
37+
* phpcs:enable
2638
* @throws JweDecryptException
2739
*/
2840
public function decrypt(string $jweString): string
2941
{
30-
$jweDecrypter = $this->getDecrypter();
31-
32-
$serializerManager = new JWESerializerManager([new CompactSerializer()]);
33-
$jwe = $serializerManager->unserialize($jweString);
42+
$jwe = $this->serializerManager->unserialize($jweString);
3443

3544
// Success of decryption, $jwe is now decrypted
36-
$success = $jweDecrypter->decryptUsingKey($jwe, $this->getDecryptionKey(), 0);
45+
$success = $this->jweDecrypter->decryptUsingKey($jwe, $this->decryptionKey, 0);
3746
if (!$success) {
3847
throw new JweDecryptException('Failed to decrypt JWE');
3948
}
@@ -45,22 +54,4 @@ public function decrypt(string $jweString): string
4554

4655
return $payload;
4756
}
48-
49-
protected function getDecrypter(): JWEDecrypter
50-
{
51-
$keyEncryptionAlgorithmManager = new AlgorithmManager([new RSAOAEP()]);
52-
$contentEncryptionAlgorithmManager = new AlgorithmManager([new A128CBCHS256()]);
53-
$compressionMethodManager = new CompressionMethodManager([new Deflate()]);
54-
55-
return new JWEDecrypter(
56-
$keyEncryptionAlgorithmManager,
57-
$contentEncryptionAlgorithmManager,
58-
$compressionMethodManager
59-
);
60-
}
61-
62-
protected function getDecryptionKey(): JWK
63-
{
64-
return JWKFactory::createFromKeyFile($this->decryptionKeyPath);
65-
}
6657
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MinVWS\OpenIDConnectLaravel\Tests\Unit\Services\JWE;
6+
7+
use Jose\Component\Core\AlgorithmManager;
8+
use Jose\Component\Core\JWK;
9+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
10+
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP;
11+
use Jose\Component\Encryption\Compression\CompressionMethodManager;
12+
use Jose\Component\Encryption\Compression\Deflate;
13+
use Jose\Component\Encryption\JWE;
14+
use Jose\Component\Encryption\JWEBuilder;
15+
use Jose\Component\Encryption\JWEDecrypter;
16+
use Jose\Component\Encryption\Serializer\CompactSerializer;
17+
use Jose\Component\Encryption\Serializer\JWESerializerManager;
18+
use Jose\Component\KeyManagement\JWKFactory;
19+
use JsonException;
20+
use MinVWS\OpenIDConnectLaravel\Services\JWE\JweDecryptException;
21+
use MinVWS\OpenIDConnectLaravel\Services\JWE\JweDecryptService;
22+
use Mockery;
23+
use OpenSSLAsymmetricKey;
24+
use OpenSSLCertificate;
25+
use OpenSSLCertificateSigningRequest;
26+
use PHPUnit\Framework\TestCase;
27+
use RuntimeException;
28+
29+
class JweDecryptServiceTest extends TestCase
30+
{
31+
/**
32+
* @var resource
33+
*/
34+
protected $decryptionKeyResource;
35+
protected JWK $decryptionKey;
36+
protected OpenSSLCertificate $x509Certificate;
37+
38+
protected function setUp(): void
39+
{
40+
parent::setUp();
41+
42+
[$key, $keyResource] = $this->generateOpenSSLKey();
43+
$this->decryptionKeyResource = $keyResource;
44+
$this->decryptionKey = JWKFactory::createFromKeyFile(stream_get_meta_data($keyResource)['uri']);
45+
$this->x509Certificate = $this->generateX509Certificate($key);
46+
}
47+
48+
protected function tearDown(): void
49+
{
50+
if (is_resource($this->decryptionKeyResource)) {
51+
fclose($this->decryptionKeyResource);
52+
}
53+
54+
parent::tearDown();
55+
}
56+
57+
public function testServiceCanBeCreated(): void
58+
{
59+
$jweDecryptService = new JweDecryptService($this->decryptionKey);
60+
61+
$this->assertInstanceOf(JweDecryptService::class, $jweDecryptService);
62+
}
63+
64+
/**
65+
* @throws JweDecryptException
66+
* @throws JsonException
67+
*/
68+
public function testJweDecryption(): void
69+
{
70+
$payload = $this->buildExamplePayload();
71+
72+
$jwe = $this->buildJweString(
73+
payload: $payload,
74+
recipient: JWKFactory::createFromX509Resource($this->x509Certificate)
75+
);
76+
77+
$jweDecryptService = new JweDecryptService($this->decryptionKey);
78+
$decryptedPayload = $jweDecryptService->decrypt($jwe);
79+
80+
$this->assertEquals($payload, $decryptedPayload);
81+
}
82+
83+
/**
84+
* @throws JweDecryptException
85+
* @throws JsonException
86+
*/
87+
public function testJweDecryptionThrowsExceptionWhenKeyIsNotCorrect(): void
88+
{
89+
$this->expectException(JweDecryptException::class);
90+
$this->expectExceptionMessage('Failed to decrypt JWE');
91+
92+
// Create different key
93+
[$key, $keyResource] = $this->generateOpenSSLKey();
94+
$jwk = JWKFactory::createFromKeyFile(stream_get_meta_data($keyResource)['uri']);
95+
96+
// Build JWE for default certificate
97+
$payload = $this->buildExamplePayload();
98+
$jwe = $this->buildJweString(
99+
payload: $payload,
100+
recipient: JWKFactory::createFromX509Resource($this->x509Certificate)
101+
);
102+
103+
// Try to decrypt with different key
104+
$jweDecryptService = new JweDecryptService($jwk);
105+
$jweDecryptService->decrypt($jwe);
106+
}
107+
108+
/**
109+
* @throws JweDecryptException
110+
* @throws JsonException
111+
*/
112+
public function testJweDecryptionThrowsExceptionWhenPayloadIsNull(): void
113+
{
114+
$this->expectException(JweDecryptException::class);
115+
$this->expectExceptionMessage('Payload of JWE is null');
116+
117+
$jweMock = Mockery::mock(JWE::class);
118+
$jweMock
119+
->shouldReceive('getPayload')
120+
->andReturn(null);
121+
122+
$decryptionKey = Mockery::mock(JWK::class);
123+
$serializerManager = Mockery::mock(JWESerializerManager::class);
124+
$serializerManager
125+
->shouldReceive('unserialize')
126+
->with('something')
127+
->andReturn($jweMock);
128+
129+
$jweDecrypter = Mockery::mock(JWEDecrypter::class);
130+
$jweDecrypter
131+
->shouldReceive('decryptUsingKey')
132+
->andReturn(true);
133+
134+
$decryptService = new JweDecryptService(
135+
$decryptionKey,
136+
$serializerManager,
137+
$jweDecrypter,
138+
);
139+
140+
$decryptService->decrypt('something');
141+
}
142+
143+
protected function buildJweString(string $payload, JWK $recipient): string
144+
{
145+
// Create the JWE builder object
146+
$jweBuilder = new JWEBuilder(
147+
new AlgorithmManager([new RSAOAEP()]),
148+
new AlgorithmManager([new A128CBCHS256()]),
149+
new CompressionMethodManager([new Deflate()])
150+
);
151+
152+
// Build the JWE
153+
$jwe = $jweBuilder
154+
->create()
155+
->withPayload($payload)
156+
->withSharedProtectedHeader([
157+
'alg' => 'RSA-OAEP',
158+
'enc' => 'A128CBC-HS256',
159+
'zip' => 'DEF',
160+
])
161+
->addRecipient($recipient)
162+
->build();
163+
164+
// Get the compact serialization of the JWE
165+
return (new CompactSerializer())->serialize($jwe, 0);
166+
}
167+
168+
/**
169+
* @throws JsonException
170+
*/
171+
protected function buildExamplePayload(): string
172+
{
173+
return json_encode([
174+
'iat' => time(),
175+
'nbf' => time(),
176+
'exp' => time() + 3600,
177+
'iss' => 'My service',
178+
'aud' => 'Your application',
179+
], JSON_THROW_ON_ERROR);
180+
}
181+
182+
/**
183+
* Generate OpenSSL Key and return the tempfile resource
184+
* @return array{OpenSSLAsymmetricKey, resource}
185+
*/
186+
protected function generateOpenSSLKey(): array
187+
{
188+
$file = tmpfile();
189+
if (!is_resource($file)) {
190+
throw new RuntimeException('Could not create temporary file');
191+
}
192+
193+
$key = openssl_pkey_new([
194+
'private_key_bits' => 512,
195+
'private_key_type' => OPENSSL_KEYTYPE_RSA,
196+
]);
197+
if (!$key instanceof OpenSSLAsymmetricKey) {
198+
throw new RuntimeException('Could not generate private key');
199+
}
200+
201+
openssl_pkey_export($key, $privateKey);
202+
fwrite($file, $privateKey);
203+
204+
return [$key, $file];
205+
}
206+
207+
/**
208+
* Generate X509 certificate
209+
* @param OpenSSLAsymmetricKey $key
210+
* @return OpenSSLCertificate
211+
*/
212+
protected function generateX509Certificate(OpenSSLAsymmetricKey $key): OpenSSLCertificate
213+
{
214+
$csr = openssl_csr_new([], $key);
215+
if (!$csr instanceof OpenSSLCertificateSigningRequest) {
216+
throw new RuntimeException('Could not generate CSR');
217+
}
218+
219+
$certificate = openssl_csr_sign($csr, null, $key, 365);
220+
if (!$certificate instanceof OpenSSLCertificate) {
221+
throw new RuntimeException('Could not generate X509 certificate');
222+
}
223+
224+
return $certificate;
225+
}
226+
}

0 commit comments

Comments
 (0)