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