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                this.alg = alg;
122                this.typ = typ;
123                this.cty = cty;
124
125                if (crit != null) {
126                        // Copy and make unmodifiable
127                        this.crit = Collections.unmodifiableSet(new HashSet<>(crit));
128                } else {
129                        this.crit = null;
130                }
131
132                if (customParams != null) {
133                        // Copy and make unmodifiable
134                        this.customParams = Collections.unmodifiableMap(new HashMap<>(customParams));
135                } else {
136                        this.customParams = EMPTY_CUSTOM_PARAMS;
137                }
138
139                this.parsedBase64URL = parsedBase64URL;
140        }
141
142
143        /**
144         * Deep copy constructor.
145         *
146         * @param header The header to copy. Must not be {@code null}.
147         */
148        protected Header(final Header header) {
149
150                this(
151                        header.getAlgorithm(),
152                        header.getType(),
153                        header.getContentType(),
154                        header.getCriticalParams(),
155                        header.getCustomParams(),
156                        header.getParsedBase64URL());
157        }
158
159
160        /**
161         * Gets the algorithm ({@code alg}) parameter.
162         *
163         * @return The algorithm parameter.
164         */
165        public Algorithm getAlgorithm() {
166
167                return alg;
168        }
169
170
171        /**
172         * Gets the type ({@code typ}) parameter.
173         *
174         * @return The type parameter, {@code null} if not specified.
175         */
176        public JOSEObjectType getType() {
177
178                return typ;
179        }
180
181
182        /**
183         * Gets the content type ({@code cty}) parameter.
184         *
185         * @return The content type parameter, {@code null} if not specified.
186         */
187        public String getContentType() {
188
189                return cty;
190        }
191
192
193        /**
194         * Gets the critical header parameters ({@code crit}) parameter.
195         *
196         * @return The names of the critical header parameters, as a
197         *         unmodifiable set, {@code null} if not specified.
198         */
199        public Set<String> getCriticalParams() {
200
201                return crit;
202        }
203
204
205        /**
206         * Gets a custom (non-registered) parameter.
207         *
208         * @param name The name of the custom parameter. Must not be
209         *             {@code null}.
210         *
211         * @return The custom parameter, {@code null} if not specified.
212         */
213        public Object getCustomParam(final String name) {
214
215                return customParams.get(name);
216        }
217
218
219        /**
220         * Gets the custom (non-registered) parameters.
221         *
222         * @return The custom parameters, as a unmodifiable map, empty map if
223         *         none.
224         */
225        public Map<String,Object> getCustomParams() {
226
227                return customParams;
228        }
229
230
231        /**
232         * Gets the original Base64URL used to create this header.
233         *
234         * @return The parsed Base64URL, {@code null} if the header was created
235         *         from scratch.
236         */
237        public Base64URL getParsedBase64URL() {
238
239                return parsedBase64URL;
240        }
241
242
243        /**
244         * Gets the names of all included parameters (registered and custom) in
245         * the header instance.
246         *
247         * @return The included parameters.
248         */
249        public Set<String> getIncludedParams() {
250
251                Set<String> includedParameters =
252                        new HashSet<>(getCustomParams().keySet());
253
254                if (getAlgorithm() != null) {
255                        includedParameters.add(HeaderParameterNames.ALGORITHM);
256                }
257
258                if (getType() != null) {
259                        includedParameters.add(HeaderParameterNames.TYPE);
260                }
261
262                if (getContentType() != null) {
263                        includedParameters.add(HeaderParameterNames.CONTENT_TYPE);
264                }
265
266                if (getCriticalParams() != null && ! getCriticalParams().isEmpty()) {
267                        includedParameters.add(HeaderParameterNames.CRITICAL);
268                }
269
270                return includedParameters;
271        }
272
273
274        /**
275         * Returns a JSON object representation of the header. All custom
276         * parameters are included if they serialise to a JSON entity and
277         * their names don't conflict with the registered ones.
278         *
279         * @return The JSON object representation of the header.
280         */
281        public Map<String, Object> toJSONObject() {
282
283                // Include custom parameters, they will be overwritten if their
284                // names match specified registered ones
285                Map<String, Object> o = JSONObjectUtils.newJSONObject();
286                o.putAll(customParams);
287
288                if (alg != null) {
289                        o.put(HeaderParameterNames.ALGORITHM, alg.toString());
290                }
291
292                if (typ != null) {
293                        o.put(HeaderParameterNames.TYPE, typ.toString());
294                }
295
296                if (cty != null) {
297                        o.put(HeaderParameterNames.CONTENT_TYPE, cty);
298                }
299
300                if (crit != null && ! crit.isEmpty()) {
301                        o.put(HeaderParameterNames.CRITICAL, new ArrayList<>(crit));
302                }
303
304                return o;
305        }
306
307
308        /**
309         * Returns a JSON string representation of the header. All custom
310         * parameters will be included if they serialise to a JSON entity and
311         * their names don't conflict with the registered ones.
312         *
313         * @return The JSON string representation of the header.
314         */
315        public String toString() {
316
317                return JSONObjectUtils.toJSONString(toJSONObject());
318        }
319
320
321        /**
322         * Returns a Base64URL representation of the header. If the header was
323         * parsed always returns the original Base64URL (required for JWS
324         * validation and authenticated JWE decryption).
325         *
326         * @return The original parsed Base64URL representation of the header,
327         *         or a new Base64URL representation if the header was created
328         *         from scratch.
329         */
330        public Base64URL toBase64URL() {
331
332                if (parsedBase64URL == null) {
333
334                        // Header was created from scratch, return new Base64URL
335                        return Base64URL.encode(toString());
336
337                } else {
338
339                        // Header was parsed, return original Base64URL
340                        return parsedBase64URL;
341                }
342        }
343
344
345        /**
346         * Parses an algorithm ({@code alg}) parameter from the specified 
347         * header JSON object. Intended for initial parsing of unsecured
348         * (plain), JWS and JWE headers.
349         *
350         * <p>The algorithm type (none, JWS or JWE) is determined by inspecting
351         * the algorithm name for "none" and the presence of an "enc"
352         * parameter.
353         *
354         * @param json The JSON object to parse. Must not be {@code null}.
355         *
356         * @return The algorithm, an instance of {@link Algorithm#NONE},
357         *         {@link JWSAlgorithm} or {@link JWEAlgorithm}. {@code null}
358         *         if not found.
359         *
360         * @throws ParseException If the {@code alg} parameter couldn't be 
361         *                        parsed.
362         */
363        public static Algorithm parseAlgorithm(final Map<String, Object> json)
364                throws ParseException {
365
366                String algName = JSONObjectUtils.getString(json, HeaderParameterNames.ALGORITHM);
367                
368                if (algName == null) {
369                        throw new ParseException("Missing \"alg\" in header JSON object", 0);
370                }
371
372                // Infer algorithm type
373                if (algName.equals(Algorithm.NONE.getName())) {
374                        // Plain
375                        return Algorithm.NONE;
376                } else if (json.containsKey(HeaderParameterNames.ENCRYPTION_ALGORITHM)) {
377                        // JWE
378                        return JWEAlgorithm.parse(algName);
379                } else {
380                        // JWS
381                        return JWSAlgorithm.parse(algName);
382                }
383        }
384
385
386        /**
387         * Join a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
388         * with an Unprotected header.
389         *
390         * @param unprotected     The Unprotected header. {@code null}
391         *                        if not applicable.
392         *
393         * @return The header.
394         *
395         * @throws ParseException If the specified Unprotected header can not be
396         *                        merged to protected header.
397         */
398        public Header join(final UnprotectedHeader unprotected)
399                throws ParseException {
400
401                Map<String, Object> jsonObject = toJSONObject();
402                try {
403                        HeaderValidation.ensureDisjoint(this, unprotected);
404                } catch (IllegalHeaderException e) {
405                        throw new ParseException(e.getMessage(), 0);
406                }
407                if (unprotected != null) {
408                        jsonObject.putAll(unprotected.toJSONObject());
409                }
410                return parse(jsonObject, null);
411        }
412
413
414        /**
415         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
416         * from the specified JSON object.
417         *
418         * @param jsonObject      The JSON object to parse. Must not be
419         *                        {@code null}.
420         *
421         * @return The header.
422         *
423         * @throws ParseException If the specified JSON object doesn't
424         *                        represent a valid header.
425         */
426        public static Header parse(final Map<String, Object> jsonObject)
427                throws ParseException {
428
429                return parse(jsonObject, null);
430        }
431
432
433        /**
434         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader} 
435         * from the specified JSON object.
436         *
437         * @param jsonObject      The JSON object to parse. Must not be
438         *                        {@code null}.
439         * @param parsedBase64URL The original parsed Base64URL, {@code null}
440         *                        if not applicable.
441         *
442         * @return The header.
443         *
444         * @throws ParseException If the specified JSON object doesn't 
445         *                        represent a valid header.
446         */
447        public static Header parse(final Map<String, Object> jsonObject,
448                                   final Base64URL parsedBase64URL)
449                throws ParseException {
450
451
452                String algName = JSONObjectUtils.getString(jsonObject, HeaderParameterNames.ALGORITHM);
453
454                if (jsonObject.containsKey(HeaderParameterNames.ENCRYPTION_ALGORITHM)) {
455                        // JWE
456                        return JWEHeader.parse(jsonObject, parsedBase64URL);
457                } else if (Algorithm.NONE.getName().equals(algName)) {
458                        // Plain
459                        return PlainHeader.parse(jsonObject, parsedBase64URL);
460                } else if (jsonObject.containsKey(HeaderParameterNames.ALGORITHM)) {
461                        // JWS
462                        return JWSHeader.parse(jsonObject, parsedBase64URL);
463                } else {
464                        throw new ParseException("Missing \"alg\" in header JSON object", 0);
465                }
466        }
467
468
469        /**
470         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
471         * from the specified JSON object string.
472         *
473         * @param jsonString      The JSON object string to parse. Must not be
474         *                        {@code null}.
475         *
476         * @return The header.
477         *
478         * @throws ParseException If the specified JSON object string doesn't
479         *                        represent a valid header.
480         */
481        public static Header parse(final String jsonString)
482                throws ParseException {
483
484                return parse(jsonString, null);
485        }
486
487
488        /**
489         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
490         * from the specified JSON object string.
491         *
492         * @param jsonString      The JSON object string to parse. Must not be
493         *                        {@code null}.
494         * @param parsedBase64URL The original parsed Base64URL, {@code null}
495         *                        if not applicable.
496         *
497         * @return The header.
498         *
499         * @throws ParseException If the specified JSON object string doesn't
500         *                        represent a valid header.
501         */
502        public static Header parse(final String jsonString,
503                                   final Base64URL parsedBase64URL)
504                throws ParseException {
505
506                Map<String, Object> jsonObject = JSONObjectUtils.parse(jsonString, MAX_HEADER_STRING_LENGTH);
507
508                return parse(jsonObject, parsedBase64URL);
509        }
510
511
512        /**
513         * Parses a {@link PlainHeader}, {@link JWSHeader} or {@link JWEHeader}
514         * from the specified Base64URL.
515         *
516         * @param base64URL The Base64URL to parse. Must not be {@code null}.
517         *
518         * @return The header.
519         *
520         * @throws ParseException If the specified Base64URL doesn't represent
521         *                        a valid header.
522         */
523        public static Header parse(final Base64URL base64URL)
524                throws ParseException {
525
526                return parse(base64URL.decodeToString(), base64URL);
527        }
528}