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.util.Base64URL;
022import com.nimbusds.jose.util.JSONObjectUtils;
023import com.nimbusds.jose.util.StandardCharset;
024import com.nimbusds.jwt.SignedJWT;
025import net.jcip.annotations.Immutable;
026
027import java.io.Serializable;
028import java.text.ParseException;
029import java.util.Map;
030import java.util.Objects;
031
032
033/**
034 * Payload of an unsecured (plain), JSON Web Signature (JWS) or JSON Web
035 * Encryption (JWE) object. Supports JSON object, string, byte array,
036 * Base64URL, JWS object and signed JWT payload representations. This class is
037 * immutable.
038 *
039 * <p>UTF-8 is the character set for all conversions between strings and byte
040 * arrays.
041 *
042 * <p>Conversion relations:
043 *
044 * <pre>
045 * JSON object &lt;=&gt; String &lt;=&gt; Base64URL
046 *                        &lt;=&gt; byte[]
047 *                        &lt;=&gt; JWSObject
048 *                        &lt;=&gt; SignedJWT
049 * </pre>
050 *
051 * @author Vladimir Dzhuvinov
052 * @version 2024-04-20
053 */
054@Immutable
055public final class Payload implements Serializable {
056
057
058        /**
059         * Enumeration of the original data types used to create a 
060         * {@link Payload}.
061         */
062        public enum Origin {
063
064
065                /**
066                 * The payload was created from a JSON object.
067                 */
068                JSON,
069
070
071                /**
072                 * The payload was created from a string.
073                 */
074                STRING,
075
076
077                /**
078                 * The payload was created from a byte array.
079                 */
080                BYTE_ARRAY,
081
082
083                /**
084                 * The payload was created from a Base64URL-encoded object.
085                 */
086                BASE64URL,
087
088
089                /**
090                 * The payload was created from a JWS object.
091                 */
092                JWS_OBJECT,
093
094
095                /**
096                 * The payload was created from a signed JSON Web Token (JWT).
097                 */
098                SIGNED_JWT
099        }
100
101
102        private static final long serialVersionUID = 1L;
103
104
105        /**
106         * The original payload data type.
107         */
108        private final Origin origin;
109
110
111        /**
112         * The JSON object representation.
113         */
114        private final Map<String, Object> jsonObject;
115
116
117        /**
118         * The string representation.
119         */
120        private final String string;
121
122
123        /**
124         * The byte array representation.
125         */
126        private final byte[] bytes;
127
128
129        /**
130         * The Base64URL representation.
131         */
132        private final Base64URL base64URL;
133
134
135        /**
136         * The JWS object representation.
137         */
138        private final JWSObject jwsObject;
139
140
141        /**
142         * The signed JWT representation.
143         */
144        private final SignedJWT signedJWT;
145
146
147        /**
148         * Converts a byte array to a string using {@code UTF-8}.
149         *
150         * @param bytes The byte array to convert. May be {@code null}.
151         *
152         * @return The resulting string, {@code null} if conversion failed.
153         */
154        private static String byteArrayToString(final byte[] bytes) {
155
156                return bytes != null ? new String(bytes, StandardCharset.UTF_8) : null;
157        }
158
159
160        /**
161         * Converts a string to a byte array using {@code UTF-8}.
162         *
163         * @param string The string to convert. May be {@code null}.
164         *
165         * @return The resulting byte array, {@code null} if conversion failed.
166         */
167        private static byte[] stringToByteArray(final String string) {
168
169                return string != null ? string.getBytes(StandardCharset.UTF_8) : null;
170        }
171
172
173        /**
174         * Creates a new payload from the specified JSON object.
175         *
176         * @param jsonObject The JSON object representing the payload. Must not
177         *                   be {@code null}.
178         */
179        public Payload(final Map<String, Object> jsonObject) {
180
181                this.jsonObject = JSONObjectUtils.newJSONObject();
182                this.jsonObject.putAll(Objects.requireNonNull(jsonObject, "The JSON object must not be null"));
183                string = null;
184                bytes = null;
185                base64URL = null;
186                jwsObject = null;
187                signedJWT = null;
188
189                origin = Origin.JSON;
190        }
191
192
193        /**
194         * Creates a new payload from the specified string.
195         *
196         * @param string The string representing the payload. Must not be 
197         *               {@code null}.
198         */
199        public Payload(final String string) {
200
201                jsonObject = null;
202                this.string = Objects.requireNonNull(string, "The string must not be null");
203                bytes = null;
204                base64URL = null;
205                jwsObject = null;
206                signedJWT = null;
207
208                origin = Origin.STRING;
209        }
210
211
212        /**
213         * Creates a new payload from the specified byte array.
214         *
215         * @param bytes The byte array representing the payload. Must not be 
216         *              {@code null}.
217         */
218        public Payload(final byte[] bytes) {
219
220                jsonObject = null;
221                string = null;
222                this.bytes = Objects.requireNonNull(bytes, "The byte array must not be null");
223                base64URL = null;
224                jwsObject = null;
225                signedJWT = null;
226
227                origin = Origin.BYTE_ARRAY;
228        }
229
230
231        /**
232         * Creates a new payload from the specified Base64URL-encoded object.
233         *
234         * @param base64URL The Base64URL-encoded object representing the 
235         *                  payload. Must not be {@code null}.
236         */
237        public Payload(final Base64URL base64URL) {
238
239                jsonObject = null;
240                string = null;
241                bytes = null;
242                this.base64URL = Objects.requireNonNull(base64URL, "The Base64URL-encoded object must not be null");
243                jwsObject = null;
244                signedJWT = null;
245
246                origin = Origin.BASE64URL;
247        }
248
249
250        /**
251         * Creates a new payload from the specified JWS object. Intended for
252         * signed then encrypted JOSE objects.
253         *
254         * @param jwsObject The JWS object representing the payload. Must be in
255         *                  a signed state and not {@code null}.
256         */
257        public Payload(final JWSObject jwsObject) {
258
259                if (jwsObject.getState() == JWSObject.State.UNSIGNED) {
260                        throw new IllegalArgumentException("The JWS object must be signed");
261                }
262
263                jsonObject = null;
264                string = null;
265                bytes = null;
266                base64URL = null;
267                this.jwsObject = jwsObject;
268                signedJWT = null;
269
270                origin = Origin.JWS_OBJECT;
271        }
272
273
274        /**
275         * Creates a new payload from the specified signed JSON Web Token
276         * (JWT). Intended for signed then encrypted JWTs.
277         *
278         * @param signedJWT The signed JWT representing the payload. Must be in
279         *                  a signed state and not {@code null}.
280         */
281        public Payload(final SignedJWT signedJWT) {
282
283                if (signedJWT.getState() == JWSObject.State.UNSIGNED) {
284                        throw new IllegalArgumentException("The JWT must be signed");
285                }
286
287                jsonObject = null;
288                string = null;
289                bytes = null;
290                base64URL = null;
291                this.signedJWT = signedJWT;
292                jwsObject = signedJWT; // The signed JWT is also a JWS
293
294                origin = Origin.SIGNED_JWT;
295        }
296
297
298        /**
299         * Gets the original data type used to create this payload.
300         *
301         * @return The payload origin.
302         */
303        public Origin getOrigin() {
304
305                return origin;
306        }
307
308
309        /**
310         * Returns a JSON object representation of this payload.
311         *
312         * @return The JSON object representation, {@code null} if the payload
313         *         couldn't be converted to a JSON object.
314         */
315        public Map<String, Object> toJSONObject() {
316
317                if (jsonObject != null) {
318                        return jsonObject;
319                }
320
321                // Convert
322
323                String s = toString();
324
325                if (s == null) {
326                        // to string conversion failed
327                        return null;
328                }
329
330                try {
331                        return JSONObjectUtils.parse(s);
332
333                } catch (ParseException e) {
334                        // Payload not a JSON object
335                        return null;
336                }
337        }
338
339        /**
340         * Returns a string representation of this payload.
341         *
342         * @return The string representation.
343         */
344        @Override
345        public String toString() {
346
347                if (string != null) {
348
349                        return string;
350                }
351
352                // Convert
353                if (jwsObject != null) {
354
355                        if (jwsObject.getParsedString() != null) {
356                                return jwsObject.getParsedString();
357                        } else {
358                                return jwsObject.serialize();
359                        }
360
361                } else if (jsonObject != null) {
362
363                        return JSONObjectUtils.toJSONString(jsonObject);
364
365                } else if (bytes != null) {
366
367                        return byteArrayToString(bytes);
368
369                } else if (base64URL != null) {
370
371                        return base64URL.decodeToString();
372                } else {
373                        return null; // should never happen
374                }
375        }
376
377
378        /**
379         * Returns a byte array representation of this payload.
380         *
381         * @return The byte array representation.
382         */
383        public byte[] toBytes() {
384
385                if (bytes != null) {
386                        return bytes;
387                }
388
389                // Convert
390                if (base64URL != null) {
391                        return base64URL.decode();
392
393                }
394
395                return stringToByteArray(toString());
396        }
397
398
399        /**
400         * Returns a Base64URL representation of this payload, as required for
401         * JOSE serialisation (see RFC 7515, section 7).
402         *
403         * @return The Base64URL representation.
404         */
405        public Base64URL toBase64URL() {
406
407                if (base64URL != null) {
408                        return base64URL;
409                }
410
411                // Convert
412                return Base64URL.encode(toBytes());
413        }
414
415
416        /**
417         * Returns a JWS object representation of this payload. Intended for
418         * signed then encrypted JOSE objects.
419         *
420         * @return The JWS object representation, {@code null} if the payload
421         *         couldn't be converted to a JWS object.
422         */
423        public JWSObject toJWSObject() {
424
425                if (jwsObject != null) {
426                        return jwsObject;
427                }
428
429                try {
430                        return JWSObject.parse(toString());
431
432                } catch (ParseException e) {
433
434                        return null;
435                }
436        }
437
438
439        /**
440         * Returns a signed JSON Web Token (JWT) representation of this
441         * payload. Intended for signed then encrypted JWTs.
442         *
443         * @return The signed JWT representation, {@code null} if the payload
444         *         couldn't be converted to a signed JWT.
445         */
446        public SignedJWT toSignedJWT() {
447
448                if (signedJWT != null) {
449                        return signedJWT;
450                }
451
452                try {
453                        return SignedJWT.parse(toString());
454
455                } catch (ParseException e) {
456
457                        return null;
458                }
459        }
460
461
462        /**
463         * Returns a transformation of this payload.
464         *
465         * @param <T> Type of the result.
466         * @param transformer The payload transformer. Must not be
467         *                    {@code null}.
468         *
469         * @return The transformed payload.
470         */
471        public <T> T toType(final PayloadTransformer<T> transformer) {
472
473                return transformer.transform(this);
474        }
475}