001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2023, Connect2id Ltd and contributors.
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.JWEObject.State;
022import com.nimbusds.jose.util.Base64URL;
023import com.nimbusds.jose.util.JSONArrayUtils;
024import com.nimbusds.jose.util.JSONObjectUtils;
025import net.jcip.annotations.Immutable;
026import net.jcip.annotations.ThreadSafe;
027
028import java.nio.charset.StandardCharsets;
029import java.text.ParseException;
030import java.util.*;
031
032
033/**
034 * JSON Web Encryption (JWE) secured object with
035 * <a href="https://datatracker.ietf.org/doc/html/rfc7516#section-7.2">JSON
036 * serialisation</a>.
037 *
038 * <p>This class is thread-safe.
039 *
040 * @author Egor Puzanov
041 * @author Vladimir Dzhuvinov
042 * @version 2024-04-20
043 */
044@ThreadSafe
045public class JWEObjectJSON extends JOSEObjectJSON {
046
047
048        private static final long serialVersionUID = 1L;
049
050
051        /**
052         * Individual recipient in a JWE object serialisable to JSON.
053         */
054        @Immutable
055        public static final class Recipient {
056
057
058                /**
059                 * The per-recipient unprotected header.
060                 */
061                private final UnprotectedHeader unprotectedHeader;
062
063
064                /**
065                 * The encrypted key, {@code null} if none.
066                 */
067                private final Base64URL encryptedKey;
068
069
070                /**
071                 * Creates a new parsed recipient.
072                 *
073                 * @param unprotectedHeader The per-recipient unprotected
074                 *                          header, {@code null} if none.
075                 * @param encryptedKey      The encrypted key, {@code null} if
076                 *                          none.
077                 */
078                public Recipient(final UnprotectedHeader unprotectedHeader,
079                                 final Base64URL encryptedKey) {
080                        this.unprotectedHeader = unprotectedHeader;
081                        this.encryptedKey = encryptedKey;
082                }
083
084
085                /**
086                 * Returns the per-recipient unprotected header.
087                 *
088                 * @return The per-recipient unprotected header, {@code null}
089                 *         if none.
090                 */
091                public UnprotectedHeader getUnprotectedHeader() {
092                        return unprotectedHeader;
093                }
094
095
096                /**
097                 * Returns the encrypted key.
098                 *
099                 * @return The encryptedKey.
100                 */
101                public Base64URL getEncryptedKey() {
102                        return encryptedKey;
103                }
104
105
106                /**
107                 * Returns a JSON object representation for use in the general
108                 * and flattened serialisations.
109                 *
110                 * @return The JSON object.
111                 */
112                public Map<String, Object> toJSONObject() {
113                        Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject();
114                        
115                        if (unprotectedHeader != null && ! unprotectedHeader.getIncludedParams().isEmpty()) {
116                                jsonObject.put("header", unprotectedHeader.toJSONObject());
117                        }
118                        if (encryptedKey != null) {
119                                jsonObject.put("encrypted_key", encryptedKey.toString());
120                        }
121                        return jsonObject;
122                }
123
124
125                /**
126                 * Parses a recipients object from the specified JSON object.
127                 *
128                 * @param jsonObject The JSON object to parse. Must not be
129                 *             {@code null}.
130                 *
131                 * @return The recipient object.
132                 *
133                 * @throws ParseException If the string couldn't be parsed to a
134                 *                        JWE object.
135                 */
136                public static Recipient parse(final Map<String, Object> jsonObject)
137                        throws ParseException {
138
139                        final UnprotectedHeader header = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(jsonObject, "header"));
140                        final Base64URL encryptedKey = JSONObjectUtils.getBase64URL(jsonObject, "encrypted_key");
141
142                        return new Recipient(header, encryptedKey);
143                }
144        }
145
146
147        /**
148         * The JWE protected header.
149         */
150        private final JWEHeader header;
151
152
153        /**
154         * The shared unprotected header.
155         */
156        private UnprotectedHeader unprotectedHeader;
157
158
159        /**
160         * The recipients list.
161         */
162        private final List<Recipient> recipients = new LinkedList<>();
163
164
165        /**
166         * The initialisation vector, {@code null} if not generated or
167         * applicable.
168         */
169        private Base64URL iv;
170
171
172        /**
173         * The cipher text, {@code null} if not computed.
174         */
175        private Base64URL cipherText;
176
177
178        /**
179         * The authentication tag, {@code null} if not computed or applicable.
180         */
181        private Base64URL authTag;
182
183
184        /**
185         * The additional authenticated data, {@code null} if not computed or
186         * applicable.
187         */
188        private final byte[] aad;
189
190
191        /**
192         * The JWE object state.
193         */
194        private JWEObject.State state;
195
196
197        /**
198         * Creates a new JWE JSON object from the specified JWE object with
199         * compact serialisation. The initial state is copied from the JWE
200         * object.
201         *
202         * @param jweObject  The JWE object. Must not be {@code null}.
203         */
204        public JWEObjectJSON(final JWEObject jweObject) {
205
206                super(jweObject.getPayload());
207
208                this.header = jweObject.getHeader();
209                this.aad = null;
210                this.iv = jweObject.getIV();
211                this.cipherText = jweObject.getCipherText();
212                this.authTag = jweObject.getAuthTag();
213                if (jweObject.getState() == JWEObject.State.ENCRYPTED) {
214                        this.recipients.add(new Recipient(null, jweObject.getEncryptedKey()));
215                        this.state = State.ENCRYPTED;
216                } else if (jweObject.getState() == JWEObject.State.DECRYPTED) {
217                        this.recipients.add(new Recipient(null, jweObject.getEncryptedKey()));
218                        this.state = State.DECRYPTED;
219                } else {
220                        this.state = State.UNENCRYPTED;
221                }
222        }
223
224
225        /**
226         * Creates a new to-be-encrypted JSON Web Encryption (JWE) object with
227         * the specified JWE protected header and payload. The initial state
228         * will be {@link State#UNENCRYPTED unencrypted}.
229         *
230         * @param header  The JWE protected header. Must not be {@code null}.
231         * @param payload The payload. Must not be {@code null}.
232         */
233        public JWEObjectJSON(final JWEHeader header, final Payload payload) {
234
235            this(header, payload, null, null);
236        }
237
238
239        /**
240         * Creates a new to-be-encrypted JSON Web Encryption (JWE) object with
241         * the specified JWE protected header, payload and Additional
242         * Authenticated Data (AAD). The initial state will be
243         * {@link State#UNENCRYPTED unencrypted}.
244         *
245         * @param header            The JWE protected header. Must not be
246         *                          {@code null}.
247         * @param payload           The payload. Must not be {@code null}.
248         * @param unprotectedHeader The shared unprotected header, empty or
249         *                          {@code null} if none.
250         * @param aad               The additional authenticated data (AAD),
251         *                          {@code null} if none.
252         */
253        public JWEObjectJSON(final JWEHeader header,
254                             final Payload payload,
255                             final UnprotectedHeader unprotectedHeader,
256                             final byte[] aad) {
257
258                super(payload);
259                this.header = Objects.requireNonNull(header);
260                setPayload(Objects.requireNonNull(payload));
261                this.unprotectedHeader = unprotectedHeader;
262                this.aad = aad;
263                this.cipherText = null;
264                this.state = State.UNENCRYPTED;
265        }
266
267
268        /**
269         * Creates a new encrypted JSON Web Encryption (JWE) object. The state
270         * will be {@link State#ENCRYPTED encrypted}.
271         *
272         * @param header            The JWE protected header. Must not be
273         *                          {@code null}.
274         * @param cipherText        The cipher text. Must not be {@code null}.
275         * @param iv                The initialisation vector, empty or
276         *                          {@code null} if none.
277         * @param authTag           The authentication tag, empty or
278         *                          {@code null} if none.
279         * @param recipients        The recipients list. Must not be
280         *                          {@code null}.
281         * @param unprotectedHeader The shared unprotected header, empty or
282         *                          {@code null} if none.
283         * @param aad               The additional authenticated data. Must not
284         *                          be {@code null}.
285         *
286         */
287        public JWEObjectJSON(final JWEHeader header,
288                             final Base64URL cipherText,
289                             final Base64URL iv,
290                             final Base64URL authTag,
291                             final List<Recipient> recipients,
292                             final UnprotectedHeader unprotectedHeader,
293                             final byte[] aad) {
294
295                super(null); // Payload not decrypted yet, must be null
296
297                this.header = Objects.requireNonNull(header);
298                this.recipients.addAll(recipients);
299                this.unprotectedHeader = unprotectedHeader;
300                this.aad = aad;
301                this.iv = iv;
302                this.cipherText = Objects.requireNonNull(cipherText);
303                this.authTag = authTag;
304
305                state = State.ENCRYPTED; // but not decrypted yet!
306        }
307
308
309        /**
310         * Returns the JWE protected header of this JWE object.
311         *
312         * @return The JWE protected header.
313         */
314        public JWEHeader getHeader() {
315                return header;
316        }
317
318
319        /**
320         * Returns the shared unprotected header of this JWE object.
321         *
322         * @return The shared unprotected header, empty or {@code null} if
323         *         none.
324         */
325        public UnprotectedHeader getUnprotectedHeader() {
326                return unprotectedHeader;
327        }
328
329
330        /**
331         * Returns the encrypted key of this JWE object.
332         *
333         * @return The encrypted key, {@code null} not applicable or the JWE
334         *         object has not been encrypted yet.
335         */
336        public Base64URL getEncryptedKey() {
337                if (recipients.isEmpty()) {
338                        return null;
339                } else if (recipients.size() == 1) {
340                        return recipients.get(0).getEncryptedKey();
341                }
342                List<Object> recipientsList = JSONArrayUtils.newJSONArray();
343                for (Recipient recipient : recipients) {
344                        recipientsList.add(recipient.toJSONObject());
345                }
346                Map<String, Object> recipientsMap = JSONObjectUtils.newJSONObject();
347                recipientsMap.put("recipients", recipientsList);
348                return Base64URL.encode(JSONObjectUtils.toJSONString(recipientsMap));
349        }
350
351
352        /**
353         * Returns the initialisation vector (IV) of this JWE object.
354         *
355         * @return The initialisation vector (IV), {@code null} if not
356         *         applicable or the JWE object has not been encrypted yet.
357         */
358        public Base64URL getIV() {
359                return iv;
360        }
361
362
363        /**
364         * Returns the cipher text of this JWE object.
365         *
366         * @return The cipher text, {@code null} if the JWE object has not been
367         *         encrypted yet.
368         */
369        public Base64URL getCipherText() {
370                return cipherText;
371        }
372
373
374        /**
375         * Returns the authentication tag of this JWE object.
376         *
377         * @return The authentication tag, {@code null} if not applicable or
378         *         the JWE object has not been encrypted yet.
379         */
380        public Base64URL getAuthTag() {
381                return authTag;
382        }
383
384
385        /**
386         * Returns the Additional Authenticated Data (AAD) of this JWE object.
387         *
388         * @return The Additional Authenticated Data (AAD).
389         */
390        public byte[] getAAD() {
391                StringBuilder aadSB = new StringBuilder(header.toBase64URL().toString());
392                if (aad != null && aad.length > 0) {
393                        aadSB.append(".").append(new String(aad, StandardCharsets.US_ASCII));
394                }
395                return aadSB.toString().getBytes(StandardCharsets.US_ASCII);
396        }
397
398
399        /**
400         * Returns the recipients list of the JWE object.
401         *
402         * @return The recipients list.
403         */
404        public List<Recipient> getRecipients() {
405                return Collections.unmodifiableList(recipients);
406        }
407
408
409        /**
410         * Returns the state of this JWE object.
411         *
412         * @return The state.
413         */
414        public State getState() {
415                return state;
416        }
417
418
419        /**
420         * Ensures the current state is {@link State#UNENCRYPTED unencrypted}.
421         *
422         * @throws IllegalStateException If the current state is not 
423         *                               unencrypted.
424         */
425        private void ensureUnencryptedState() {
426                if (state != State.UNENCRYPTED) {
427                        throw new IllegalStateException("The JWE object must be in an unencrypted state");
428                }
429        }
430
431
432        /**
433         * Ensures the current state is {@link State#ENCRYPTED encrypted}.
434         *
435         * @throws IllegalStateException If the current state is not encrypted.
436         */
437        private void ensureEncryptedState() {
438                if (state != State.ENCRYPTED) {
439                        throw new IllegalStateException("The JWE object must be in an encrypted state");
440                }
441        }
442
443
444        /**
445         * Ensures the current state is {@link State#ENCRYPTED encrypted} or
446         * {@link State#DECRYPTED decrypted}.
447         *
448         * @throws IllegalStateException If the current state is not encrypted
449         *                               or decrypted.
450         */
451        private void ensureEncryptedOrDecryptedState() {
452                if (state != State.ENCRYPTED && state != State.DECRYPTED) {
453                        throw new IllegalStateException("The JWE object must be in an encrypted or decrypted state");
454                }
455        }
456
457
458        /**
459         * Ensures the specified JWE encrypter supports the algorithms of this
460         * JWE object.
461         *
462         * @throws JOSEException If the JWE algorithms are not supported.
463         */
464        private void ensureJWEEncrypterSupport(final JWEEncrypter encrypter)
465                throws JOSEException {
466
467                if (! encrypter.supportedJWEAlgorithms().contains(getHeader().getAlgorithm())) {
468                        throw new JOSEException("The " + getHeader().getAlgorithm() +
469                                                " algorithm is not supported by the JWE encrypter: Supported algorithms: " + encrypter.supportedJWEAlgorithms());
470                }
471
472                if (! encrypter.supportedEncryptionMethods().contains(getHeader().getEncryptionMethod())) {
473                        throw new JOSEException("The " + getHeader().getEncryptionMethod() +
474                                                " encryption method or key size is not supported by the JWE encrypter: Supported methods: " + encrypter.supportedEncryptionMethods());
475                }
476        }
477
478
479        /**
480         * Encrypts this JWE object with the specified encrypter. The JWE
481         * object must be in an {@link State#UNENCRYPTED unencrypted} state.
482         *
483         * @param encrypter The JWE encrypter. Must not be {@code null}.
484         *
485         * @throws IllegalStateException If the JWE object is not in an
486         *                               {@link State#UNENCRYPTED unencrypted
487         *                               state}.
488         * @throws JOSEException         If the JWE object couldn't be
489         *                               encrypted.
490         */
491        public synchronized void encrypt(final JWEEncrypter encrypter)
492                throws JOSEException {
493
494                ensureUnencryptedState();
495
496                ensureJWEEncrypterSupport(encrypter);
497
498                JWECryptoParts parts;
499
500                JWEHeader jweJoinedHeader = getHeader();
501                try {
502                        jweJoinedHeader = (JWEHeader) getHeader().join(unprotectedHeader);
503                        parts = encrypter.encrypt(jweJoinedHeader, getPayload().toBytes(), getAAD());
504                } catch (JOSEException e) {
505                        throw e;
506                } catch (Exception e) {
507                        // Prevent throwing unchecked exceptions at this point,
508                        // see issue #20
509                        throw new JOSEException(e.getMessage(), e);
510                }
511
512                Base64URL encryptedKey = parts.getEncryptedKey();
513                try {
514                        for (Map<String, Object> recipientMap : JSONObjectUtils.getJSONObjectArray((JSONObjectUtils.parse(encryptedKey.decodeToString())), "recipients")) {
515                                recipients.add(Recipient.parse(recipientMap));
516                        }
517                } catch (Exception e) {
518                        Map<String, Object> recipientHeader = parts.getHeader().toJSONObject();
519                        for (String param : jweJoinedHeader.getIncludedParams()) {
520                                if (recipientHeader.containsKey(param)) {
521                                        recipientHeader.remove(param);
522                                }
523                        }
524                        try {
525                                recipients.add(new Recipient(UnprotectedHeader.parse(recipientHeader), encryptedKey));
526                        } catch (Exception ex) {
527                                throw new JOSEException(ex.getMessage(), ex);
528                        }
529                }
530                iv = parts.getInitializationVector();
531                cipherText = parts.getCipherText();
532                authTag = parts.getAuthenticationTag();
533
534                state = State.ENCRYPTED;
535        }
536
537
538        /**
539         * Decrypts this JWE object with the specified decrypter. The JWE
540         * object must be in a {@link State#ENCRYPTED encrypted} state.
541         *
542         * @param decrypter The JWE decrypter. Must not be {@code null}.
543         *
544         * @throws IllegalStateException If the JWE object is not in an
545         *                               {@link State#ENCRYPTED encrypted
546         *                               state}.
547         * @throws JOSEException         If the JWE object couldn't be
548         *                               decrypted.
549         */
550        public synchronized void decrypt(final JWEDecrypter decrypter)
551                throws JOSEException {
552
553                ensureEncryptedState();
554
555                try {
556                        setPayload(new Payload(decrypter.decrypt(getHeader(),
557                                               getEncryptedKey(),
558                                               getIV(),
559                                               getCipherText(),
560                                               getAuthTag(),
561                                               getAAD())));
562                } catch (JOSEException e) {
563                        throw e;
564                } catch (Exception e) {
565                        // Prevent throwing unchecked exceptions at this point,
566                        // see issue #20
567                        throw new JOSEException(e.getMessage(), e);
568                }
569
570                state = State.DECRYPTED;
571        }
572
573
574        /**
575         * Returns the JSON object with the common members in general and
576         * flattened JWE JSON serialisation.
577         */
578        private Map<String,Object> toBaseJSONObject() {
579                Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject();
580                jsonObject.put("protected", header.toBase64URL().toString());
581                if (aad != null) {
582                        jsonObject.put("aad", new String(aad, StandardCharsets.US_ASCII));
583                }
584                jsonObject.put("ciphertext", cipherText.toString());
585                jsonObject.put("iv", iv.toString());
586                jsonObject.put("tag", authTag.toString());
587                return jsonObject;
588        }
589
590
591        @Override
592        public Map<String, Object> toGeneralJSONObject() {
593
594                ensureEncryptedOrDecryptedState();
595
596                if (recipients.isEmpty() || (recipients.get(0).getUnprotectedHeader() == null && recipients.get(0).getEncryptedKey() == null)) {
597                        throw new IllegalStateException("The general JWE JSON serialization requires at least one recipient");
598                }
599
600                Map<String, Object> jsonObject = toBaseJSONObject();
601
602                if (unprotectedHeader != null) {
603                        jsonObject.put("unprotected", unprotectedHeader.toJSONObject());
604                }
605
606                List<Object> recipientsJSONArray = JSONArrayUtils.newJSONArray();
607
608                for (Recipient recipient: recipients) {
609                        Map<String, Object> recipientJSONObject = recipient.toJSONObject();
610                        recipientsJSONArray.add(recipientJSONObject);
611                }
612
613                jsonObject.put("recipients", recipientsJSONArray);
614                return jsonObject;
615        }
616
617
618        @Override
619        public Map<String, Object> toFlattenedJSONObject() {
620
621                ensureEncryptedOrDecryptedState();
622
623                if (recipients.size() != 1) {
624                        throw new IllegalStateException("The flattened JWE JSON serialization requires exactly one recipient");
625                }
626
627                Map<String, Object> jsonObject = toBaseJSONObject();
628
629                Map<String, Object> recipientHeader = JSONObjectUtils.newJSONObject();
630                if (recipients.get(0).getUnprotectedHeader() != null) {
631                        recipientHeader.putAll(recipients.get(0).getUnprotectedHeader().toJSONObject());
632                }
633                if (unprotectedHeader != null) {
634                        recipientHeader.putAll(unprotectedHeader.toJSONObject());
635                }
636                if (recipientHeader.size() > 0) {
637                        jsonObject.put("unprotected", recipientHeader);
638                }
639                if (recipients.get(0).getEncryptedKey() != null) {
640                        jsonObject.put("encrypted_key", recipients.get(0).getEncryptedKey().toString());
641                }
642                return jsonObject;
643        }
644
645
646        @Override
647        public String serializeGeneral() {
648                return JSONObjectUtils.toJSONString(toGeneralJSONObject());
649        }
650
651
652        @Override
653        public String serializeFlattened() {
654                return JSONObjectUtils.toJSONString(toFlattenedJSONObject());
655        }
656
657
658        /**
659         * Parses a JWE object from the specified JSON object representation.
660         *
661         * @param jsonObject The JSON object to parse. Must not be
662         *                   {@code null}.
663         *
664         * @return The JWE secured object.
665         *
666         * @throws ParseException If the JSON object couldn't be parsed to a
667         *                        JWE secured object.
668         */
669        public static JWEObjectJSON parse(final Map<String, Object> jsonObject)
670                throws ParseException {
671
672                if (!jsonObject.containsKey("protected")) {
673                        throw new ParseException("The JWE protected header mast be present", 0);
674                }
675
676                List<Recipient> recipientList = new LinkedList<>();
677                final JWEHeader jweHeader = JWEHeader.parse(JSONObjectUtils.getBase64URL(jsonObject, "protected"));
678                final UnprotectedHeader unprotected = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(jsonObject, "unprotected"));
679                final Base64URL cipherText = JSONObjectUtils.getBase64URL(jsonObject, "ciphertext");
680                final Base64URL iv = JSONObjectUtils.getBase64URL(jsonObject, "iv");
681                final Base64URL authTag = JSONObjectUtils.getBase64URL(jsonObject, "tag");
682                final Base64URL aad = JSONObjectUtils.getBase64URL(jsonObject, "aad");
683                final JWEHeader jweJoinedHeader = (JWEHeader) jweHeader.join(unprotected);
684
685                if (jsonObject.containsKey("recipients")) {
686                        Map<String, Object>[] recipients = JSONObjectUtils.getJSONObjectArray(jsonObject, "recipients");
687                        if (recipients == null || recipients.length == 0) {
688                                throw new ParseException("The \"recipients\" member must be present in general JSON Serialization", 0);
689                        }
690                        for (Map<String, Object> recipientJSONObject: recipients) {
691                                Recipient recipient = Recipient.parse(recipientJSONObject);
692                                try {
693                                        HeaderValidation.ensureDisjoint(jweJoinedHeader, recipient.getUnprotectedHeader());
694                                } catch (IllegalHeaderException e) {
695                                        throw new ParseException(e.getMessage(), 0);
696                                }
697                                recipientList.add(recipient);
698                        }
699                } else {
700                        Base64URL encryptedKey = JSONObjectUtils.getBase64URL(jsonObject, "encrypted_key");
701                        recipientList.add(new Recipient(null, encryptedKey));
702                }
703
704                return new JWEObjectJSON(jweHeader, cipherText, iv, authTag, recipientList, unprotected, aad == null ? null : aad.toString().getBytes(StandardCharsets.US_ASCII));
705        }
706
707
708        /**
709         * Parses a JWE object from the specified JSON object string.
710         *
711         * @param json The JSON object string to parse. Must not be
712         *             {@code null}.
713         *
714         * @return The JWE object.
715         *
716         * @throws ParseException If the string couldn't be parsed to a JWE
717         *                        object.
718         */
719        public static JWEObjectJSON parse(final String json)
720                throws ParseException {
721
722                return parse(JSONObjectUtils.parse(Objects.requireNonNull(json)));
723        }
724}