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