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