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