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 java.io.Serializable;
022import java.text.ParseException;
023import java.util.*;
024
025import net.minidev.json.JSONObject;
026
027import com.nimbusds.jose.util.Base64URL;
028import com.nimbusds.jose.util.JSONObjectUtils;
029
030
031/**
032 * The base abstract class for unsecured ({@code alg=none}), JSON Web Signature
033 * (JWS) and JSON Web Encryption (JWE) headers.
034 *
035 * <p>The header may also include {@link #getCustomParams custom
036 * parameters}; these will be serialised and parsed along the registered ones.
037 *
038 * @author Vladimir Dzhuvinov
039 * @version 2019-10-04
040 */
041public abstract class Header implements Serializable {
042
043
044        private static final long serialVersionUID = 1L;
045
046
047        /**
048         * The algorithm ({@code alg}) parameter.
049         */
050        private final Algorithm alg;
051
052
053        /**
054         * The JOSE object type ({@code typ}) parameter.
055         */
056        private final JOSEObjectType typ;
057
058
059        /**
060         * The content type ({@code cty}) parameter.
061         */
062        private final String cty;
063
064
065        /**
066         * The critical headers ({@code crit}) parameter.
067         */
068        private final Set<String> crit;
069
070
071        /**
072         * Custom header parameters.
073         */
074        private final Map<String,Object> customParams;
075
076
077        /**
078         * Empty custom parameters constant.
079         */
080        private static final Map<String,Object> EMPTY_CUSTOM_PARAMS =
081                Collections.unmodifiableMap(new HashMap<String,Object>());
082
083
084        /**
085         * The original parsed Base64URL, {@code null} if the header was 
086         * created from scratch.
087         */
088        private final Base64URL parsedBase64URL;
089
090
091        /**
092         * Creates a new abstract header.
093         *
094         * @param alg             The algorithm ({@code alg}) parameter. Must
095         *                        not be {@code null}.
096         * @param typ             The type ({@code typ}) parameter,
097         *                        {@code null} if not specified.
098         * @param cty             The content type ({@code cty}) parameter,
099         *                        {@code null} if not specified.
100         * @param crit            The names of the critical header
101         *                        ({@code crit}) parameters, empty set or
102         *                        {@code null} if none.
103         * @param customParams    The custom parameters, empty map or
104         *                        {@code null} if none.
105         * @param parsedBase64URL The parsed Base64URL, {@code null} if the
106         *                        header is created from scratch.
107         */
108        protected Header(final Algorithm alg,
109                         final JOSEObjectType typ,
110                         final String cty, Set<String> crit,
111                         final Map<String,Object> customParams,
112                         final Base64URL parsedBase64URL) {
113
114                if (alg == null) {
115                        throw new IllegalArgumentException("The algorithm \"alg\" header parameter must not be null");
116                }
117
118                this.alg = alg;
119
120                this.typ = typ;
121                this.cty = cty;
122
123                if (crit != null) {
124                        // Copy and make unmodifiable
125                        this.crit = Collections.unmodifiableSet(new HashSet<>(crit));
126                } else {
127                        this.crit = null;
128                }
129
130                if (customParams != null) {
131                        // Copy and make unmodifiable
132                        this.customParams = Collections.unmodifiableMap(new HashMap<>(customParams));
133                } else {
134                        this.customParams = EMPTY_CUSTOM_PARAMS;
135                }
136
137                this.parsedBase64URL = parsedBase64URL;
138        }
139
140
141        /**
142         * Deep copy constructor.
143         *
144         * @param header The header to copy. Must not be {@code null}.
145         */
146        protected Header(final Header header) {
147
148                this(
149                        header.getAlgorithm(),
150                        header.getType(),
151                        header.getContentType(),
152                        header.getCriticalParams(),
153                        header.getCustomParams(),
154                        header.getParsedBase64URL());
155        }
156
157
158        /**
159         * Gets the algorithm ({@code alg}) parameter.
160         *
161         * @return The algorithm parameter.
162         */
163        public Algorithm getAlgorithm() {
164
165                return alg;
166        }
167
168
169        /**
170         * Gets the type ({@code typ}) parameter.
171         *
172         * @return The type parameter, {@code null} if not specified.
173         */
174        public JOSEObjectType getType() {
175
176                return typ;
177        }
178
179
180        /**
181         * Gets the content type ({@code cty}) parameter.
182         *
183         * @return The content type parameter, {@code null} if not specified.
184         */
185        public String getContentType() {
186
187                return cty;
188        }
189
190
191        /**
192         * Gets the critical header parameters ({@code crit}) parameter.
193         *
194         * @return The names of the critical header parameters, as a
195         *         unmodifiable set, {@code null} if not specified.
196         */
197        public Set<String> getCriticalParams() {
198
199                return crit;
200        }
201
202
203        /**
204         * Gets a custom (non-registered) parameter.
205         *
206         * @param name The name of the custom parameter. Must not be
207         *             {@code null}.
208         *
209         * @return The custom parameter, {@code null} if not specified.
210         */
211        public Object getCustomParam(final String name) {
212
213                return customParams.get(name);
214        }
215
216
217        /**
218         * Gets the custom (non-registered) parameters.
219         *
220         * @return The custom parameters, as a unmodifiable map, empty map if
221         *         none.
222         */
223        public Map<String,Object> getCustomParams() {
224
225                return customParams;
226        }
227
228
229        /**
230         * Gets the original Base64URL used to create this header.
231         *
232         * @return The parsed Base64URL, {@code null} if the header was created
233         *         from scratch.
234         */
235        public Base64URL getParsedBase64URL() {
236
237                return parsedBase64URL;
238        }
239
240
241        /**
242         * Gets the names of all included parameters (registered and custom) in
243         * the header instance.
244         *
245         * @return The included parameters.
246         */
247        public Set<String> getIncludedParams() {
248
249                Set<String> includedParameters =
250                        new HashSet<>(getCustomParams().keySet());
251
252                includedParameters.add("alg");
253
254                if (getType() != null) {
255                        includedParameters.add("typ");
256                }
257
258                if (getContentType() != null) {
259                        includedParameters.add("cty");
260                }
261
262                if (getCriticalParams() != null && ! getCriticalParams().isEmpty()) {
263                        includedParameters.add("crit");
264                }
265
266                return includedParameters;
267        }
268
269
270        /**
271         * Returns a JSON object representation of the header. All custom
272         * parameters are included if they serialise to a JSON entity and
273         * their names don't conflict with the registered ones.
274         *
275         * @return The JSON object representation of the header.
276         */
277        public JSONObject toJSONObject() {
278
279                // Include custom parameters, they will be overwritten if their
280                // names match specified registered ones
281                JSONObject o = new JSONObject(customParams);
282
283                // Alg is always defined
284                o.put("alg", alg.toString());
285
286                if (typ != null) {
287                        o.put("typ", typ.toString());
288                }
289
290                if (cty != null) {
291                        o.put("cty", cty);
292                }
293
294                if (crit != null && ! crit.isEmpty()) {
295                        o.put("crit", new ArrayList<>(crit));
296                }
297
298                return o;
299        }
300
301
302        /**
303         * Returns a JSON string representation of the header. All custom
304         * parameters will be included if they serialise to a JSON entity and
305         * their names don't conflict with the registered ones.
306         *
307         * @return The JSON string representation of the header.
308         */
309        public String toString() {
310
311                return toJSONObject().toString();
312        }
313
314
315        /**
316         * Returns a Base64URL representation of the header. If the header was
317         * parsed always returns the original Base64URL (required for JWS
318         * validation and authenticated JWE decryption).
319         *
320         * @return The original parsed Base64URL representation of the header,
321         *         or a new Base64URL representation if the header was created
322         *         from scratch.
323         */
324        public Base64URL toBase64URL() {
325
326                if (parsedBase64URL == null) {
327
328                        // Header was created from scratch, return new Base64URL
329                        return Base64URL.encode(toString());
330
331                } else {
332
333                        // Header was parsed, return original Base64URL
334                        return parsedBase64URL;
335                }
336        }
337
338
339        /**
340         * Parses an algorithm ({@code alg}) parameter from the specified 
341         * header JSON object. Intended for initial parsing of unsecured
342         * (plain), JWS and JWE headers.
343         *
344         * <p>The algorithm type (none, JWS or JWE) is determined by inspecting
345         * the algorithm name for "none" and the presence of an "enc"
346         * parameter.
347         *
348         * @param json The JSON object to parse. Must not be {@code null}.
349         *
350         * @return The algorithm, an instance of {@link Algorithm#NONE},
351         *         {@link JWSAlgorithm} or {@link JWEAlgorithm}. {@code null}
352         *         if not found.
353         *
354         * @throws ParseException If the {@code alg} parameter couldn't be 
355         *                        parsed.
356         */
357        public static Algorithm parseAlgorithm(final JSONObject json)
358                throws ParseException {
359
360                String algName = JSONObjectUtils.getString(json, "alg");
361                
362                if (algName == null) {
363                        throw new ParseException("Missing \"alg\" in header JSON object", 0);
364                }
365
366                // Infer algorithm type
367                if (algName.equals(Algorithm.NONE.getName())) {
368                        // Plain
369                        return Algorithm.NONE;
370                } else if (json.containsKey("enc")) {
371                        // JWE
372                        return JWEAlgorithm.parse(algName);
373                } else {
374                        // JWS
375                        return JWSAlgorithm.parse(algName);
376                }
377        }
378
379
380        /**
381         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
382         * from the specified JSON object.
383         *
384         * @param jsonObject      The JSON object to parse. Must not be
385         *                        {@code null}.
386         *
387         * @return The header.
388         *
389         * @throws ParseException If the specified JSON object doesn't
390         *                        represent a valid header.
391         */
392        public static Header parse(final JSONObject jsonObject)
393                throws ParseException {
394
395                return parse(jsonObject, null);
396        }
397
398
399        /**
400         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader} 
401         * from the specified JSON object.
402         *
403         * @param jsonObject      The JSON object to parse. Must not be
404         *                        {@code null}.
405         * @param parsedBase64URL The original parsed Base64URL, {@code null}
406         *                        if not applicable.
407         *
408         * @return The header.
409         *
410         * @throws ParseException If the specified JSON object doesn't 
411         *                        represent a valid header.
412         */
413        public static Header parse(final JSONObject jsonObject,
414                                   final Base64URL parsedBase64URL)
415                throws ParseException {
416
417                Algorithm alg = parseAlgorithm(jsonObject);
418
419                if (alg.equals(Algorithm.NONE)) {
420
421                        return PlainHeader.parse(jsonObject, parsedBase64URL);
422
423                } else if (alg instanceof JWSAlgorithm) {
424
425                        return JWSHeader.parse(jsonObject, parsedBase64URL);
426
427                } else if (alg instanceof JWEAlgorithm) {
428
429                        return JWEHeader.parse(jsonObject, parsedBase64URL);
430
431                } else {
432
433                        throw new AssertionError("Unexpected algorithm type: " + alg);
434                }
435        }
436
437
438        /**
439         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
440         * from the specified JSON object string.
441         *
442         * @param jsonString      The JSON object string to parse. Must not be
443         *                        {@code null}.
444         *
445         * @return The header.
446         *
447         * @throws ParseException If the specified JSON object string doesn't
448         *                        represent a valid header.
449         */
450        public static Header parse(final String jsonString)
451                throws ParseException {
452
453                return parse(jsonString, null);
454        }
455
456
457        /**
458         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
459         * from the specified JSON object string.
460         *
461         * @param jsonString      The JSON object string to parse. Must not be
462         *                        {@code null}.
463         * @param parsedBase64URL The original parsed Base64URL, {@code null}
464         *                        if not applicable.
465         *
466         * @return The header.
467         *
468         * @throws ParseException If the specified JSON object string doesn't
469         *                        represent a valid header.
470         */
471        public static Header parse(final String jsonString,
472                                   final Base64URL parsedBase64URL)
473                throws ParseException {
474
475                JSONObject jsonObject = JSONObjectUtils.parse(jsonString);
476
477                return parse(jsonObject, parsedBase64URL);
478        }
479
480
481        /**
482         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
483         * from the specified Base64URL.
484         *
485         * @param base64URL The Base64URL to parse. Must not be {@code null}.
486         *
487         * @return The header.
488         *
489         * @throws ParseException If the specified Base64URL doesn't represent
490         *                        a valid header.
491         */
492        public static Header parse(final Base64URL base64URL)
493                throws ParseException {
494
495                return parse(base64URL.decodeToString(), base64URL);
496        }
497}