001package com.nimbusds.jose.jwk;
002
003
004import java.io.File;
005import java.io.IOException;
006import java.net.URL;
007import java.text.ParseException;
008import java.util.HashMap;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.Map;
012
013import com.nimbusds.jose.util.JSONObjectUtils;
014import com.nimbusds.jose.util.DefaultResourceRetriever;
015import com.nimbusds.jose.util.Resource;
016import com.nimbusds.jose.util.RestrictedResourceRetriever;
017import net.minidev.json.JSONArray;
018import net.minidev.json.JSONObject;
019import org.apache.commons.io.FileUtils;
020
021
022/**
023 * JSON Web Key (JWK) set. Represented by a JSON object that contains an array
024 * of {@link JWK JSON Web Keys} (JWKs) as the value of its "keys" member.
025 * Additional (custom) members of the JWK Set JSON object are also supported.
026 *
027 * <p>Example JSON Web Key (JWK) set:
028 *
029 * <pre>
030 * {
031 *   "keys" : [ { "kty" : "EC",
032 *                "crv" : "P-256",
033 *                "x"   : "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
034 *                "y"   : "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
035 *                "use" : "enc",
036 *                "kid" : "1" },
037 *
038 *              { "kty" : "RSA",
039 *                "n"   : "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx
040 *                         4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs
041 *                         tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2
042 *                         QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI
043 *                         SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb
044 *                         w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
045 *                "e"   : "AQAB",
046 *                "alg" : "RS256",
047 *                "kid" : "2011-04-29" } ]
048 * }
049 * </pre>
050 *
051 * @author Vladimir Dzhuvinov
052 * @version 2016-04-10
053 */
054public class JWKSet {
055
056
057        /**
058         * The MIME type of JWK set objects: 
059         * {@code application/jwk-set+json; charset=UTF-8}
060         */
061        public static final String MIME_TYPE = "application/jwk-set+json; charset=UTF-8";
062
063
064        /**
065         * The JWK list.
066         */
067        private final List<JWK> keys = new LinkedList<>();
068
069
070        /**
071         * Additional custom members.
072         */
073        private final Map<String,Object> customMembers = new HashMap<>();
074
075
076        /**
077         * Creates a new empty JSON Web Key (JWK) set.
078         */
079        public JWKSet() {
080
081                // Nothing to do
082        }
083
084
085        /**
086         * Creates a new JSON Web Key (JWK) set with a single key.
087         *
088         * @param key The JWK. Must not be {@code null}.
089         */
090        public JWKSet(final JWK key) {
091
092                if (key == null) {
093                        throw new IllegalArgumentException("The JWK must not be null");
094                }
095
096                keys.add(key);
097        }
098
099
100        /**
101         * Creates a new JSON Web Key (JWK) set with the specified keys.
102         *
103         * @param keys The JWK list. Must not be {@code null}.
104         */
105        public JWKSet(final List<JWK> keys) {
106
107                if (keys == null) {
108                        throw new IllegalArgumentException("The JWK list must not be null");
109                }
110
111                this.keys.addAll(keys);
112        }
113
114
115        /**
116         * Creates a new JSON Web Key (JWK) set with the specified keys and
117         * additional custom members.
118         *
119         * @param keys          The JWK list. Must not be {@code null}.
120         * @param customMembers The additional custom members. Must not be
121         *                      {@code null}.
122         */
123        public JWKSet(final List<JWK> keys, final Map<String,Object> customMembers) {
124
125                if (keys == null) {
126                        throw new IllegalArgumentException("The JWK list must not be null");
127                }
128
129                this.keys.addAll(keys);
130
131                this.customMembers.putAll(customMembers);
132        }
133
134
135        /**
136         * Gets the keys (ordered) of this JSON Web Key (JWK) set.
137         *
138         * @return The keys, empty list if none.
139         */
140        public List<JWK> getKeys() {
141
142                return keys;
143        }
144
145        
146        /**
147         * Gets the key from this JSON Web Key (JWK) set as identified by its 
148         * Key ID (kid) member.
149         * 
150         * <p>If more than one key exists in the JWK Set with the same 
151         * identifier, this function returns only the first one in the set.
152         *
153         * @param kid They key identifier.
154         *
155         * @return The key identified by {@code kid} or {@code null} if no key 
156         *         exists.
157         */
158        public JWK getKeyByKeyId(String kid) {
159                
160                for (JWK key : getKeys()) {
161                
162                        if (key.getKeyID() != null && key.getKeyID().equals(kid)) {
163                                return key;
164                        }
165                }
166                
167                // no key found
168                return null;
169        }
170
171
172        /**
173         * Gets the additional custom members of this JSON Web Key (JWK) set.
174         *
175         * @return The additional custom members, empty map if none.
176         */
177        public Map<String,Object> getAdditionalMembers() {
178
179                return customMembers;
180        }
181
182
183        /**
184         * Returns a copy of this JSON Web Key (JWK) set with all private keys
185         * and parameters removed.
186         *
187         * @return A copy of this JWK set with all private keys and parameters
188         *         removed.
189         */
190        public JWKSet toPublicJWKSet() {
191
192                List<JWK> publicKeyList = new LinkedList<>();
193
194                for (JWK key: keys) {
195
196                        JWK publicKey = key.toPublicJWK();
197
198                        if (publicKey != null) {
199                                publicKeyList.add(publicKey);
200                        }
201                }
202
203                return new JWKSet(publicKeyList, customMembers);
204        }
205
206
207        /**
208         * Returns the JSON object representation of this JSON Web Key (JWK) 
209         * set. Private keys and parameters will be omitted from the output.
210         * Use the alternative {@link #toJSONObject(boolean)} method if you
211         * wish to include them.
212         *
213         * @return The JSON object representation.
214         */
215        public JSONObject toJSONObject() {
216
217                return toJSONObject(true);
218        }
219
220
221        /**
222         * Returns the JSON object representation of this JSON Web Key (JWK) 
223         * set.
224         *
225         * @param publicKeysOnly Controls the inclusion of private keys and
226         *                       parameters into the output JWK members. If
227         *                       {@code true} private keys and parameters will
228         *                       be omitted. If {@code false} all available key
229         *                       parameters will be included.
230         *
231         * @return The JSON object representation.
232         */
233        public JSONObject toJSONObject(final boolean publicKeysOnly) {
234
235                JSONObject o = new JSONObject(customMembers);
236
237                JSONArray a = new JSONArray();
238
239                for (JWK key: keys) {
240
241                        if (publicKeysOnly) {
242
243                                // Try to get public key, then serialise
244                                JWK publicKey = key.toPublicJWK();
245
246                                if (publicKey != null) {
247                                        a.add(publicKey.toJSONObject());
248                                }
249                        } else {
250
251                                a.add(key.toJSONObject());
252                        }
253                }
254
255                o.put("keys", a);
256
257                return o;
258        }
259
260
261        /**
262         * Returns the JSON object string representation of this JSON Web Key
263         * (JWK) set.
264         *
265         * @return The JSON object string representation.
266         */
267        @Override
268        public String toString() {
269
270                return toJSONObject().toString();
271        }
272
273
274        /**
275         * Parses the specified string representing a JSON Web Key (JWK) set.
276         *
277         * @param s The string to parse. Must not be {@code null}.
278         *
279         * @return The JSON Web Key (JWK) set.
280         *
281         * @throws ParseException If the string couldn't be parsed to a valid
282         *                        JSON Web Key (JWK) set.
283         */
284        public static JWKSet parse(final String s)
285                throws ParseException {
286
287                return parse(JSONObjectUtils.parse(s));
288        }
289
290
291        /**
292         * Parses the specified JSON object representing a JSON Web Key (JWK) 
293         * set.
294         *
295         * @param json The JSON object to parse. Must not be {@code null}.
296         *
297         * @return The JSON Web Key (JWK) set.
298         *
299         * @throws ParseException If the string couldn't be parsed to a valid
300         *                        JSON Web Key (JWK) set.
301         */
302        public static JWKSet parse(final JSONObject json)
303                throws ParseException {
304
305                JSONArray keyArray = JSONObjectUtils.getJSONArray(json, "keys");
306
307                List<JWK> keys = new LinkedList<>();
308
309                for (int i=0; i < keyArray.size(); i++) {
310
311                        if (! (keyArray.get(i) instanceof JSONObject)) {
312                                throw new ParseException("The \"keys\" JSON array must contain JSON objects only", 0);
313                        }
314
315                        JSONObject keyJSON = (JSONObject)keyArray.get(i);
316
317                        try {
318                                keys.add(JWK.parse(keyJSON));
319
320                        } catch (ParseException e) {
321
322                                throw new ParseException("Invalid JWK at position " + i + ": " + e.getMessage(), 0);
323                        }
324                }
325
326                // Parse additional custom members
327                JWKSet jwkSet = new JWKSet(keys);
328
329                for (Map.Entry<String,Object> entry: json.entrySet()) {
330
331                        if (entry.getKey() == null || entry.getKey().equals("keys")) {
332                                continue;
333                        }
334
335                        jwkSet.getAdditionalMembers().put(entry.getKey(), entry.getValue());
336                }
337
338                return jwkSet;
339        }
340
341
342        /**
343         * Loads a JSON Web Key (JWK) set from the specified file.
344         *
345         * @param file The JWK set file. Must not be {@code null}.
346         *
347         * @return The JSON Web Key (JWK) set.
348         *
349         * @throws IOException    If the file couldn't be read.
350         * @throws ParseException If the file couldn't be parsed to a valid
351         *                        JSON Web Key (JWK) set.
352         */
353        public static JWKSet load(final File file)
354                throws IOException, ParseException {
355
356                return parse(FileUtils.readFileToString(file));
357        }
358
359
360        /**
361         * Loads a JSON Web Key (JWK) set from the specified URL.
362         *
363         * @param url            The JWK set URL. Must not be {@code null}.
364         * @param connectTimeout The URL connection timeout, in milliseconds.
365         *                       If zero no (infinite) timeout.
366         * @param readTimeout    The URL read timeout, in milliseconds. If zero
367         *                       no (infinite) timeout.
368         * @param sizeLimit      The read size limit, in bytes. If zero no
369         *                       limit.
370         *
371         * @return The JSON Web Key (JWK) set.
372         *
373         * @throws IOException    If the file couldn't be read.
374         * @throws ParseException If the file couldn't be parsed to a valid
375         *                        JSON Web Key (JWK) set.
376         */
377        public static JWKSet load(final URL url,
378                                  final int connectTimeout,
379                                  final int readTimeout,
380                                  final int sizeLimit)
381                throws IOException, ParseException {
382
383                RestrictedResourceRetriever resourceRetriever = new DefaultResourceRetriever(
384                        connectTimeout,
385                        readTimeout,
386                        sizeLimit);
387                Resource resource = resourceRetriever.retrieveResource(url);
388                return parse(resource.getContent());
389        }
390
391
392        /**
393         * Loads a JSON Web Key (JWK) set from the specified URL.
394         *
395         * @param url The JWK set URL. Must not be {@code null}.
396         *
397         * @return The JSON Web Key (JWK) set.
398         *
399         * @throws IOException    If the file couldn't be read.
400         * @throws ParseException If the file couldn't be parsed to a valid
401         *                        JSON Web Key (JWK) set.
402         */
403        public static JWKSet load(final URL url)
404                throws IOException, ParseException {
405
406                return load(url, 0, 0, 0);
407        }
408}