001package com.nimbusds.jose;
002
003
004import java.text.ParseException;
005
006import net.jcip.annotations.ThreadSafe;
007
008import com.nimbusds.jose.util.Base64URL;
009
010
011/**
012 * JSON Web Encryption (JWE) secured object. This class is thread-safe.
013 *
014 * @author Vladimir Dzhuvinov
015 * @version 2016-04-13
016 */
017@ThreadSafe
018public class JWEObject extends JOSEObject {
019
020
021        private static final long serialVersionUID = 1L;
022
023
024        /**
025         * Enumeration of the states of a JSON Web Encryption (JWE) object.
026         */
027        public enum State {
028
029
030                /**
031                 * The JWE object is created but not encrypted yet.
032                 */
033                UNENCRYPTED,
034
035
036                /**
037                 * The JWE object is encrypted.
038                 */
039                ENCRYPTED,
040
041
042                /**
043                 * The JWE object is decrypted.
044                 */
045                DECRYPTED
046        }
047
048
049        /**
050         * The header.
051         */
052        private JWEHeader header;
053
054
055        /** 
056         * The encrypted key, {@code null} if not computed or applicable.
057         */
058        private Base64URL encryptedKey;
059
060
061        /**
062         * The initialisation vector, {@code null} if not generated or 
063         * applicable.
064         */
065        private Base64URL iv;
066
067
068        /**
069         * The cipher text, {@code null} if not computed.
070         */
071        private Base64URL cipherText;
072
073
074        /**
075         * The authentication tag, {@code null} if not computed or applicable.
076         */
077        private Base64URL authTag;
078
079
080        /**
081         * The JWE object state.
082         */
083        private State state;
084
085
086        /**
087         * Creates a new to-be-encrypted JSON Web Encryption (JWE) object with 
088         * the specified header and payload. The initial state will be 
089         * {@link State#UNENCRYPTED unencrypted}.
090         *
091         * @param header  The JWE header. Must not be {@code null}.
092         * @param payload The payload. Must not be {@code null}.
093         */
094        public JWEObject(final JWEHeader header, final Payload payload) {
095
096                if (header == null) {
097
098                        throw new IllegalArgumentException("The JWE header must not be null");
099                }
100
101                this.header = header;
102
103                if (payload == null) {
104
105                        throw new IllegalArgumentException("The payload must not be null");
106                }
107
108                setPayload(payload);
109
110                encryptedKey = null;
111
112                cipherText = null;
113
114                state = State.UNENCRYPTED;
115        }
116
117
118        /**
119         * Creates a new encrypted JSON Web Encryption (JWE) object with the 
120         * specified serialised parts. The state will be {@link State#ENCRYPTED 
121         * encrypted}.
122         *
123         * @param firstPart  The first part, corresponding to the JWE header. 
124         *                   Must not be {@code null}.
125         * @param secondPart The second part, corresponding to the encrypted 
126         *                   key. Empty or {@code null} if none.
127         * @param thirdPart  The third part, corresponding to the 
128         *                   initialisation vector. Empty or {@code null} if 
129         *                   none.
130         * @param fourthPart The fourth part, corresponding to the cipher text.
131         *                   Must not be {@code null}.
132         * @param fifthPart  The fifth part, corresponding to the 
133         *                   authentication tag. Empty of {@code null} if none.
134         *
135         * @throws ParseException If parsing of the serialised parts failed.
136         */
137        public JWEObject(final Base64URL firstPart, 
138                         final Base64URL secondPart, 
139                         final Base64URL thirdPart,
140                         final Base64URL fourthPart,
141                         final Base64URL fifthPart)
142                throws ParseException {
143
144                if (firstPart == null) {
145
146                        throw new IllegalArgumentException("The first part must not be null");
147                }
148
149                try {
150                        this.header = JWEHeader.parse(firstPart);
151
152                } catch (ParseException e) {
153
154                        throw new ParseException("Invalid JWE header: " + e.getMessage(), 0);
155                }
156
157                if (secondPart == null || secondPart.toString().isEmpty()) {
158
159                        encryptedKey = null;
160
161                } else {
162
163                        encryptedKey = secondPart;
164                }
165
166                if (thirdPart == null || thirdPart.toString().isEmpty()) {
167
168                        iv = null;
169
170                } else {
171
172                        iv = thirdPart;
173                }
174
175                if (fourthPart == null) {
176
177                        throw new IllegalArgumentException("The fourth part must not be null");
178                }
179
180                cipherText = fourthPart;
181
182                if (fifthPart == null || fifthPart.toString().isEmpty()) {
183
184                        authTag = null;
185
186                } else {
187
188                        authTag = fifthPart;
189                }
190
191                state = State.ENCRYPTED; // but not decrypted yet!
192
193                setParsedParts(firstPart, secondPart, thirdPart, fourthPart, fifthPart);
194        }
195
196
197        @Override
198        public JWEHeader getHeader() {
199
200                return header;
201        }
202
203
204        /**
205         * Returns the encrypted key of this JWE object.
206         *
207         * @return The encrypted key, {@code null} not applicable or the JWE
208         *         object has not been encrypted yet.
209         */
210        public Base64URL getEncryptedKey() {
211
212                return encryptedKey;
213        }
214
215
216        /**
217         * Returns the initialisation vector (IV) of this JWE object.
218         *
219         * @return The initialisation vector (IV), {@code null} if not 
220         *         applicable or the JWE object has not been encrypted yet.
221         */
222        public Base64URL getIV() {
223
224                return iv;
225        }
226
227
228        /**
229         * Returns the cipher text of this JWE object.
230         *
231         * @return The cipher text, {@code null} if the JWE object has not been
232         *         encrypted yet.
233         */
234        public Base64URL getCipherText() {
235
236                return cipherText;
237        }
238
239
240        /**
241         * Returns the authentication tag of this JWE object.
242         *
243         * @return The authentication tag, {@code null} if not applicable or
244         *         the JWE object has not been encrypted yet.
245         */
246        public Base64URL getAuthTag() {
247
248                return authTag;
249        }
250
251
252        /**
253         * Returns the state of this JWE object.
254         *
255         * @return The state.
256         */
257        public State getState() {
258
259                return state;
260        }
261
262
263        /**
264         * Ensures the current state is {@link State#UNENCRYPTED unencrypted}.
265         *
266         * @throws IllegalStateException If the current state is not 
267         *                               unencrypted.
268         */
269        private void ensureUnencryptedState() {
270
271                if (state != State.UNENCRYPTED) {
272
273                        throw new IllegalStateException("The JWE object must be in an unencrypted state");
274                }
275        }
276
277
278        /**
279         * Ensures the current state is {@link State#ENCRYPTED encrypted}.
280         *
281         * @throws IllegalStateException If the current state is not encrypted.
282         */
283        private void ensureEncryptedState() {
284
285                if (state != State.ENCRYPTED) {
286
287                        throw new IllegalStateException("The JWE object must be in an encrypted state");
288                }
289        }
290
291
292        /**
293         * Ensures the current state is {@link State#ENCRYPTED encrypted} or
294         * {@link State#DECRYPTED decrypted}.
295         *
296         * @throws IllegalStateException If the current state is not encrypted 
297         *                               or decrypted.
298         */
299        private void ensureEncryptedOrDecryptedState() {
300
301                if (state != State.ENCRYPTED && state != State.DECRYPTED) {
302
303                        throw new IllegalStateException("The JWE object must be in an encrypted or decrypted state");
304                }
305        }
306
307
308        /**
309         * Ensures the specified JWE encrypter supports the algorithms of this 
310         * JWE object.
311         *
312         * @throws JOSEException If the JWE algorithms are not supported.
313         */
314        private void ensureJWEEncrypterSupport(final JWEEncrypter encrypter)
315                throws JOSEException {
316
317                if (! encrypter.supportedJWEAlgorithms().contains(getHeader().getAlgorithm())) {
318
319                        throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
320                                                "\" algorithm is not supported by the JWE encrypter: Supported algorithms: " + encrypter.supportedJWEAlgorithms());
321                }
322
323                if (! encrypter.supportedEncryptionMethods().contains(getHeader().getEncryptionMethod())) {
324
325                        throw new JOSEException("The \"" + getHeader().getEncryptionMethod() + 
326                                                "\" encryption method is not supported by the JWE encrypter: Supported methods: " + encrypter.supportedEncryptionMethods());
327                }
328        }
329
330
331        /**
332         * Encrypts this JWE object with the specified encrypter. The JWE 
333         * object must be in an {@link State#UNENCRYPTED unencrypted} state.
334         *
335         * @param encrypter The JWE encrypter. Must not be {@code null}.
336         *
337         * @throws IllegalStateException If the JWE object is not in an 
338         *                               {@link State#UNENCRYPTED unencrypted
339         *                               state}.
340         * @throws JOSEException         If the JWE object couldn't be 
341         *                               encrypted.
342         */
343        public synchronized void encrypt(final JWEEncrypter encrypter)
344                throws JOSEException {
345
346                ensureUnencryptedState();
347
348                ensureJWEEncrypterSupport(encrypter);
349
350                JWECryptoParts parts;
351
352                try {
353                        parts = encrypter.encrypt(getHeader(), getPayload().toBytes());
354
355                } catch (JOSEException e) {
356
357                        throw e;
358                
359                } catch (Exception e) {
360
361                        // Prevent throwing unchecked exceptions at this point,
362                        // see issue #20
363                        throw new JOSEException(e.getMessage(), e);
364                }
365
366                // Check if the header has been modified
367                if (parts.getHeader() != null) {
368                        header = parts.getHeader();
369                }
370
371                encryptedKey = parts.getEncryptedKey();
372                iv = parts.getInitializationVector();
373                cipherText = parts.getCipherText();
374                authTag = parts.getAuthenticationTag();
375
376                state = State.ENCRYPTED;
377        }
378
379
380        /**
381         * Decrypts this JWE object with the specified decrypter. The JWE 
382         * object must be in a {@link State#ENCRYPTED encrypted} state.
383         *
384         * @param decrypter The JWE decrypter. Must not be {@code null}.
385         *
386         * @throws IllegalStateException If the JWE object is not in an 
387         *                               {@link State#ENCRYPTED encrypted
388         *                               state}.
389         * @throws JOSEException         If the JWE object couldn't be 
390         *                               decrypted.
391         */
392        public synchronized void decrypt(final JWEDecrypter decrypter)
393                throws JOSEException {
394
395                ensureEncryptedState();
396
397                try {
398                        setPayload(new Payload(decrypter.decrypt(getHeader(), 
399                                               getEncryptedKey(), 
400                                               getIV(),
401                                               getCipherText(), 
402                                               getAuthTag())));
403
404                } catch (JOSEException e) {
405
406                        throw e;
407
408                } catch (Exception e) {
409
410                        // Prevent throwing unchecked exceptions at this point,
411                        // see issue #20
412                        throw new JOSEException(e.getMessage(), e);
413                }
414
415                state = State.DECRYPTED;
416        }
417
418
419        /**
420         * Serialises this JWE object to its compact format consisting of 
421         * Base64URL-encoded parts delimited by period ('.') characters. It 
422         * must be in a {@link State#ENCRYPTED encrypted} or 
423         * {@link State#DECRYPTED decrypted} state.
424         *
425         * <pre>
426         * [header-base64url].[encryptedKey-base64url].[iv-base64url].[cipherText-base64url].[authTag-base64url]
427         * </pre>
428         *
429         * @return The serialised JWE object.
430         *
431         * @throws IllegalStateException If the JWE object is not in a 
432         *                               {@link State#ENCRYPTED encrypted} or
433         *                               {@link State#DECRYPTED decrypted 
434         *                               state}.
435         */
436        @Override
437        public String serialize() {
438
439                ensureEncryptedOrDecryptedState();
440
441                StringBuilder sb = new StringBuilder(header.toBase64URL().toString());
442                sb.append('.');
443
444                if (encryptedKey != null) {
445                        
446                        sb.append(encryptedKey.toString());
447                }
448
449                sb.append('.');
450
451                if (iv != null) {
452
453                        sb.append(iv.toString());
454                }
455
456                sb.append('.');
457
458                sb.append(cipherText.toString());
459
460                sb.append('.');
461
462                if (authTag != null) {
463
464                        sb.append(authTag.toString());
465                }
466
467                return sb.toString();
468        }
469
470
471        /**
472         * Parses a JWE object from the specified string in compact form. The 
473         * parsed JWE object will be given an {@link State#ENCRYPTED} state.
474         *
475         * @param s The string to parse. Must not be {@code null}.
476         *
477         * @return The JWE object.
478         *
479         * @throws ParseException If the string couldn't be parsed to a valid 
480         *                        JWE object.
481         */
482        public static JWEObject parse(final String s)
483                throws ParseException {
484
485                Base64URL[] parts = JOSEObject.split(s);
486
487                if (parts.length != 5) {
488
489                        throw new ParseException("Unexpected number of Base64URL parts, must be five", 0);
490                }
491
492                return new JWEObject(parts[0], parts[1], parts[2], parts[3], parts[4]);
493        }
494}