12
12
namespace Symfony \Component \Encryption ;
13
13
14
14
use Lcobucci \JWT \Encoding \CannotDecodeContent ;
15
+ use Symfony \Component \Encryption \Exception \DecryptionException ;
15
16
use Symfony \Component \Encryption \Exception \MalformedCipherException ;
16
17
17
18
/**
18
- * A JSON Web Encryption representation of the encrypted message.
19
+ * A JSON Web Encryption (RFC 7516) representation of the encrypted message.
19
20
*
20
21
* This class is responsible over the payload API.
21
22
*
27
28
*/
28
29
class JWE
29
30
{
31
+ /**
32
+ * @var string algorithm for the asymmetric algorithm. Ie, for the symmetric nonce.
33
+ */
30
34
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
+ */
31
49
private $ ciphertext ;
32
- private $ nonce ;
33
50
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
35
80
{
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 ;
39
90
}
40
91
41
92
/**
@@ -50,22 +101,38 @@ public static function parse(string $input): self
50
101
throw new MalformedCipherException ();
51
102
}
52
103
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
+
56
106
$ initializationVector = self ::base64UrlDecode ($ initializationVector );
57
107
$ ciphertext = self ::base64UrlDecode ($ ciphertext );
58
108
$ authenticationTag = self ::base64UrlDecode ($ authenticationTag );
59
109
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 )) {
61
114
throw new MalformedCipherException ();
62
115
}
63
116
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 )) {
65
121
throw new MalformedCipherException ();
66
122
}
67
123
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 ;
69
136
}
70
137
71
138
/**
@@ -78,15 +145,45 @@ public function __toString()
78
145
79
146
public function getString (): string
80
147
{
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.
84
151
'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
+ }
88
180
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 ;
90
187
}
91
188
92
189
public function getAlgorithm (): string
@@ -99,11 +196,32 @@ public function getCiphertext(): string
99
196
return $ this ->ciphertext ;
100
197
}
101
198
102
- public function getNonce ( ): string
199
+ public function getHeader ( string $ name ): string
103
200
{
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 ));
105
206
}
106
207
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
+
107
225
private static function base64UrlEncode (string $ data ): string
108
226
{
109
227
return rtrim (strtr (base64_encode ($ data ), '+/ ' , '-_ ' ), '= ' );
0 commit comments