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