Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3506151

Browse filesBrowse files
committed
Fixing Sodium with JWE
1 parent d0e3303 commit 3506151
Copy full SHA for 3506151

File tree

Expand file treeCollapse file tree

6 files changed

+244
-62
lines changed
Filter options
Expand file treeCollapse file tree

6 files changed

+244
-62
lines changed

‎src/Symfony/Component/Encryption/AsymmetricEncryptionInterface.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Encryption/AsymmetricEncryptionInterface.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public function generateKeypair(): array;
5151
* @param string $publicKey Bob's public key
5252
* @param string|null $privateKey Alice's private key. If a private key is provided, Bob is forced to verify that the message comes from Alice.
5353
*
54+
* @return string The output will be formatted according to JWE (RFC 7516).
55+
*
5456
* @throws EncryptionException
5557
*/
5658
public function encrypt(string $message, string $publicKey, ?string $privateKey = null): string;

‎src/Symfony/Component/Encryption/JWE.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Encryption/JWE.php
+139-21Lines changed: 139 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
namespace Symfony\Component\Encryption;
1313

1414
use Lcobucci\JWT\Encoding\CannotDecodeContent;
15+
use Symfony\Component\Encryption\Exception\DecryptionException;
1516
use Symfony\Component\Encryption\Exception\MalformedCipherException;
1617

1718
/**
18-
* A JSON Web Encryption representation of the encrypted message.
19+
* A JSON Web Encryption (RFC 7516) representation of the encrypted message.
1920
*
2021
* This class is responsible over the payload API.
2122
*
@@ -27,15 +28,65 @@
2728
*/
2829
class JWE
2930
{
31+
/**
32+
* @var string algorithm for the asymmetric algorithm. Ie, for the symmetric nonce.
33+
*/
3034
private $algorithm;
35+
36+
/**
37+
* @var string algorithm for the symmetric algorithm. Ie, for the payload.
38+
*/
39+
private $encryptionAlgorithm;
40+
41+
/**
42+
* @var callable to get the encoded payload. Only available when creating a JWE
43+
*/
44+
private $cipher;
45+
46+
/**
47+
* @var string|null the encoded payload. Only available after parsing
48+
*/
3149
private $ciphertext;
32-
private $nonce;
3350

34-
public function __construct(string $algorithm, string $ciphertext, string $nonce)
51+
/**
52+
* @var string the key that is used for decrypting the cipher text. This key
53+
* must be encrypted with $algorithm.
54+
*/
55+
private $cek;
56+
57+
/**
58+
* @var string Additional authentication data;
59+
*/
60+
private $aad;
61+
62+
/**
63+
* @var string nonce for the symmetric algorithm. Ie, for the payload.
64+
*/
65+
private $initializationVector;
66+
67+
/**
68+
* @var array additional headers
69+
*/
70+
private $headers = [];
71+
72+
private function __construct()
73+
{
74+
}
75+
76+
/**
77+
* @param callable $cipher expects some additional data as first parameter to compute the ciphertext
78+
*/
79+
public static function create(string $algorithm, string $cek, string $encAlgorithm, callable $cipher, string $initializationVector, array $headers = []): self
3580
{
36-
$this->algorithm = $algorithm;
37-
$this->ciphertext = $ciphertext;
38-
$this->nonce = $nonce;
81+
$jwe = new self();
82+
$jwe->algorithm = $algorithm;
83+
$jwe->cek = $cek;
84+
$jwe->encryptionAlgorithm = $encAlgorithm;
85+
$jwe->cipher = $cipher;
86+
$jwe->initializationVector = $initializationVector;
87+
$jwe->headers = $headers;
88+
89+
return $jwe;
3990
}
4091

4192
/**
@@ -50,22 +101,38 @@ public static function parse(string $input): self
50101
throw new MalformedCipherException();
51102
}
52103

53-
[$header, $cek, $initializationVector, $ciphertext, $authenticationTag] = $parts;
54-
$header = json_decode(self::base64UrlDecode($header), true);
55-
$cek = self::base64UrlDecode($cek);
104+
[$headers, $cek, $initializationVector, $ciphertext, $authenticationTag] = $parts;
105+
56106
$initializationVector = self::base64UrlDecode($initializationVector);
57107
$ciphertext = self::base64UrlDecode($ciphertext);
58108
$authenticationTag = self::base64UrlDecode($authenticationTag);
59109

60-
if (md5($ciphertext) !== $authenticationTag) {
110+
// Check if Authentication Tag is valid
111+
$aad = self::computeAdditionalAuthenticationData($headers);
112+
$hash = hash('sha256', $aad.$initializationVector.$ciphertext);
113+
if (!hash_equals($hash, $authenticationTag)) {
61114
throw new MalformedCipherException();
62115
}
63116

64-
if (!is_array($header) || !array_key_exists('enc', $header)) {
117+
$headers= json_decode(self::base64UrlDecode($headers), true);
118+
$cek = self::base64UrlDecode($cek);
119+
120+
if (!is_array($headers) || !array_key_exists('enc', $headers) || !array_key_exists('alg', $headers)) {
65121
throw new MalformedCipherException();
66122
}
67123

68-
return new self($header['enc'], $ciphertext, $initializationVector);
124+
$jwt = new self();
125+
$jwt->algorithm = $headers['alg'];
126+
unset($headers['alg']);
127+
$jwt->encryptionAlgorithm = $headers['enc'];
128+
unset($headers['enc']);
129+
$jwt->headers = $headers;
130+
$jwt->initializationVector = $initializationVector;
131+
$jwt->ciphertext = $ciphertext;
132+
$jwt->cek = $cek;
133+
$jwt->aad = $aad;
134+
135+
return $jwt;
69136
}
70137

71138
/**
@@ -78,15 +145,45 @@ public function __toString()
78145

79146
public function getString(): string
80147
{
81-
$header = self::base64UrlEncode(json_encode([
82-
'alg' => 'none', // he algorithm to encrypt the CEK.
83-
'enc' => $this->algorithm, // The algorithm to encrypt the payload.
148+
$headers = array_merge($this->headers, [
149+
'alg' => $this->algorithm ?? 'none', // he algorithm to encrypt the CEK.
150+
'enc' => $this->encryptionAlgorithm, // The algorithm to encrypt the payload.
84151
'cty' => 'plaintext',
85-
]));
86-
$cek = self::base64UrlEncode(random_bytes(32));
87-
$initializationVector = self::base64UrlEncode($this->nonce);
152+
'com.symfony.authentication_tag' => 'sha256',
153+
]);
154+
155+
$encodedHeader = self::base64UrlEncode(json_encode($headers));
156+
$aad = self::computeAdditionalAuthenticationData($encodedHeader);
157+
$cipher = $this->cipher;
158+
$ciphertext = $cipher($aad);
159+
160+
$hash = hash('sha256', $aad.$this->initializationVector.$ciphertext);
161+
162+
return sprintf('%s.%s.%s.%s.%s',
163+
$encodedHeader,
164+
self::base64UrlEncode($this->cek ?? 'none'),
165+
self::base64UrlEncode($this->initializationVector),
166+
self::base64UrlEncode($ciphertext),
167+
self::base64UrlEncode($hash)
168+
);
169+
}
170+
171+
/**
172+
* This will compute a hash over the encoded headers.
173+
*/
174+
private static function computeAdditionalAuthenticationData(string $input): string
175+
{
176+
$ascii = [];
177+
for ($i = 0; $i < strlen($input); $i++) {
178+
$ascii[] = ord($input[$i]);
179+
}
88180

89-
return sprintf('%s.%s.%s.%s.%s', $header, $cek, $initializationVector, self::base64UrlEncode($this->ciphertext), self::base64UrlEncode(md5($this->ciphertext)));
181+
return json_encode($ascii);
182+
}
183+
184+
public function getAdditionalAuthenticationData(): string
185+
{
186+
return $this->aad;
90187
}
91188

92189
public function getAlgorithm(): string
@@ -99,11 +196,32 @@ public function getCiphertext(): string
99196
return $this->ciphertext;
100197
}
101198

102-
public function getNonce(): string
199+
public function getHeader(string $name): string
103200
{
104-
return $this->nonce;
201+
if (array_key_exists($name, $this->headers)) {
202+
return $this->headers[$name];
203+
}
204+
205+
throw new DecryptionException(sprintf('The expected header "%s" is not found', $name));
105206
}
106207

208+
public function getEncryptedCek(): ?string
209+
{
210+
return $this->cek;
211+
}
212+
213+
public function getEncryptionAlgorithm(): string
214+
{
215+
return $this->encryptionAlgorithm;
216+
}
217+
218+
public function getInitializationVector(): string
219+
{
220+
return $this->initializationVector;
221+
}
222+
223+
224+
107225
private static function base64UrlEncode(string $data): string
108226
{
109227
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');

‎src/Symfony/Component/Encryption/Provider/PhpseclibEncryption.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Encryption/Provider/PhpseclibEncryption.php
+34-22Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Encryption\Provider;
1313

1414
use phpseclib\Crypt\AES;
15+
use phpseclib\Crypt\Random;
1516
use phpseclib\Crypt\RSA;
1617
use Symfony\Component\Encryption\AsymmetricEncryptionInterface;
1718
use Symfony\Component\Encryption\JWE;
@@ -63,39 +64,41 @@ public function generateKeypair(): array
6364

6465
public function encrypt(string $message, ?string $publicKey = null, ?string $privateKey = null): string
6566
{
67+
if (null === $publicKey && null !== $privateKey) {
68+
throw new InvalidArgumentException('Private key cannot have a value when no public key is provided.');
69+
}
70+
6671
set_error_handler(__CLASS__.'::throwError');
67-
$nonce = random_bytes(16);
72+
$nonce = random_bytes(12); // 96 bits
6873
try {
69-
if (null === $publicKey) {
70-
$algorithm = 'aes';
71-
$aes = new AES();
72-
$aes->setKey($this->secret);
73-
$ciphertext = $aes->encrypt($message);
74-
} elseif (null === $privateKey) {
75-
$algorithm = 'rsa';
76-
$rsa = new RSA();
77-
$rsa->loadKey($publicKey);
78-
$ciphertext = $rsa->encrypt($message);
79-
} elseif (null !== $publicKey && null !== $privateKey) {
80-
$algorithm = 'rsa_signature_pss';
81-
$rsa = new RSA();
82-
$rsa->loadKey($publicKey);
83-
$ciphertext = $rsa->encrypt($message);
74+
if (null === $publicKey && null === $privateKey) {
75+
$ciphertext = $this->symmetricEncryption($message, $nonce, $this->secret);
76+
77+
return JWE::createWithOnlySymmetricEncryption('A128CBC-HS256', $ciphertext, $nonce)->getString();
78+
}
79+
80+
// Asymmetric encryption
81+
$cek = Random::string(32);
82+
$rsa = new RSA();
83+
$rsa->loadKey($publicKey);
84+
$rsa->setEncryptionMode(RSA::ENCRYPTION_OAEP);
85+
$encryptedCek = $rsa->encrypt($cek);
86+
$ciphertext = $this->symmetricEncryption($message, $nonce, $cek);
8487

88+
$headers = [];
89+
if ($privateKey !== null) {
8590
// Load private key after encryption
8691
$rsa->loadKey($privateKey);
8792
$rsa->setSignatureMode(RSA::SIGNATURE_PSS);
88-
$nonce = $rsa->sign($ciphertext);
89-
} else {
90-
throw new InvalidArgumentException('Private key cannot have a value when no public key is provided.');
93+
$headers['com.symfony.signature'] = $rsa->sign($ciphertext);
9194
}
95+
96+
return JWE::create('RSA-OAEP', $encryptedCek, 'A128CBC-HS256', $ciphertext, $nonce, $headers)->getString();
9297
} catch (\ErrorException $exception) {
93-
throw new EncryptionException(sprintf('Failed to encrypt message with algorithm "%s".', $algorithm), $exception);
98+
throw new EncryptionException(null, $exception);
9499
} finally {
95100
restore_error_handler();
96101
}
97-
98-
return (new JWE($algorithm, $ciphertext, $nonce))->getString();
99102
}
100103

101104
public function decrypt(string $message, ?string $privateKey = null, ?string $publicKey = null): string
@@ -156,4 +159,13 @@ public static function throwError($type, $message, $file, $line)
156159
{
157160
throw new \ErrorException($message, 0, $type, $file, $line);
158161
}
162+
163+
private function symmetricEncryption(string $message, string $nonce, string $secret): string
164+
{
165+
$aes = new AES();
166+
$aes->setKey($secret);
167+
$aes->setIV($nonce);
168+
169+
return $aes->encrypt($message);
170+
}
159171
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.