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 net.minidev.json.JSONArray;
014import net.minidev.json.JSONObject;
015
016import org.apache.commons.io.FileUtils;
017
018import com.nimbusds.jose.util.JSONObjectUtils;
019import com.nimbusds.jose.util.URLUtils;
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 2014-12-14
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         * @return The key identified by {@code kid} or {@code null} if no key 
154         *         exists.
155         */
156        public JWK getKeyByKeyId(String kid) {
157                
158                for (JWK key : getKeys()) {
159                
160                        if (key.getKeyID() != null && key.getKeyID().equals(kid)) {
161                                return key;
162                        }
163                }
164                
165                // no key found
166                return null;
167        }
168
169
170        /**
171         * Gets the additional custom members of this JSON Web Key (JWK) set.
172         *
173         * @return The additional custom members, empty map if none.
174         */
175        public Map<String,Object> getAdditionalMembers() {
176
177                return customMembers;
178        }
179
180
181        /**
182         * Returns a copy of this JSON Web Key (JWK) set with all private keys
183         * and parameters removed.
184         *
185         * @return A copy of this JWK set with all private keys and parameters
186         *         removed.
187         */
188        public JWKSet toPublicJWKSet() {
189
190                List<JWK> publicKeyList = new LinkedList<>();
191
192                for (JWK key: keys) {
193
194                        JWK publicKey = key.toPublicJWK();
195
196                        if (publicKey != null) {
197                                publicKeyList.add(publicKey);
198                        }
199                }
200
201                return new JWKSet(publicKeyList, customMembers);
202        }
203
204
205        /**
206         * Returns the JSON object representation of this JSON Web Key (JWK) 
207         * set. Private keys and parameters will be omitted from the output.
208         * Use the alternative {@link #toJSONObject(boolean)} method if you
209         * wish to include them.
210         *
211         * @return The JSON object representation.
212         */
213        public JSONObject toJSONObject() {
214
215                return toJSONObject(true);
216        }
217
218
219        /**
220         * Returns the JSON object representation of this JSON Web Key (JWK) 
221         * set.
222         *
223         * @param publicKeysOnly Controls the inclusion of private keys and
224         *                       parameters into the output JWK members. If
225         *                       {@code true} private keys and parameters will
226         *                       be omitted. If {@code false} all available key
227         *                       parameters will be included.
228         *
229         * @return The JSON object representation.
230         */
231        public JSONObject toJSONObject(final boolean publicKeysOnly) {
232
233                JSONObject o = new JSONObject(customMembers);
234
235                JSONArray a = new JSONArray();
236
237                for (JWK key: keys) {
238
239                        if (publicKeysOnly) {
240
241                                // Try to get public key, then serialise
242                                JWK publicKey = key.toPublicJWK();
243
244                                if (publicKey != null) {
245                                        a.add(publicKey.toJSONObject());
246                                }
247                        } else {
248
249                                a.add(key.toJSONObject());
250                        }
251                }
252
253                o.put("keys", a);
254
255                return o;
256        }
257
258
259        /**
260         * Returns the JSON object string representation of this JSON Web Key
261         * (JWK) set.
262         *
263         * @return The JSON object string representation.
264         */
265        @Override
266        public String toString() {
267
268                return toJSONObject().toString();
269        }
270
271
272        /**
273         * Parses the specified string representing a JSON Web Key (JWK) set.
274         *
275         * @param s The string to parse. Must not be {@code null}.
276         *
277         * @return The JSON Web Key (JWK) set.
278         *
279         * @throws ParseException If the string couldn't be parsed to a valid
280         *                        JSON Web Key (JWK) set.
281         */
282        public static JWKSet parse(final String s)
283                throws ParseException {
284
285                return parse(JSONObjectUtils.parseJSONObject(s));
286        }
287
288
289        /**
290         * Parses the specified JSON object representing a JSON Web Key (JWK) 
291         * set.
292         *
293         * @param json The JSON object to parse. Must not be {@code null}.
294         *
295         * @return The JSON Web Key (JWK) set.
296         *
297         * @throws ParseException If the string couldn't be parsed to a valid
298         *                        JSON Web Key (JWK) set.
299         */
300        public static JWKSet parse(final JSONObject json)
301                throws ParseException {
302
303                JSONArray keyArray = JSONObjectUtils.getJSONArray(json, "keys");
304
305                List<JWK> keys = new LinkedList<>();
306
307                for (int i=0; i < keyArray.size(); i++) {
308
309                        if (! (keyArray.get(i) instanceof JSONObject)) {
310                                throw new ParseException("The \"keys\" JSON array must contain JSON objects only", 0);
311                        }
312
313                        JSONObject keyJSON = (JSONObject)keyArray.get(i);
314
315                        try {
316                                keys.add(JWK.parse(keyJSON));
317
318                        } catch (ParseException e) {
319
320                                throw new ParseException("Invalid JWK at position " + i + ": " + e.getMessage(), 0);
321                        }
322                }
323
324                // Parse additional custom members
325                JWKSet jwkSet = new JWKSet(keys);
326
327                for (Map.Entry<String,Object> entry: json.entrySet()) {
328
329                        if (entry.getKey() == null || entry.getKey().equals("keys")) {
330                                continue;
331                        }
332
333                        jwkSet.getAdditionalMembers().put(entry.getKey(), entry.getValue());
334                }
335
336                return jwkSet;
337        }
338
339
340        /**
341         * Loads a JSON Web Key (JWK) set from the specified file.
342         *
343         * @param file The JWK set file. Must not be {@code null}.
344         *
345         * @return The JSON Web Key (JWK) set.
346         *
347         * @throws IOException    If the file couldn't be read.
348         * @throws ParseException If the file couldn't be parsed to a valid
349         *                        JSON Web Key (JWK) set.
350         */
351        public static JWKSet load(final File file)
352                throws IOException, ParseException {
353
354                return parse(FileUtils.readFileToString(file));
355        }
356
357
358        /**
359         * Loads a JSON Web Key (JWK) set from the specified URL.
360         *
361         * @param url            The JWK set URL. Must not be {@code null}.
362         * @param connectTimeout The URL connection timeout, in milliseconds.
363         *                       If zero no (infinite) timeout.
364         * @param readTimeout    The URL read timeout, in milliseconds. If zero
365         *                       no (infinite) timeout.
366         * @param sizeLimit      The read size limit, in bytes. If negative no
367         *                       limit.
368         *
369         * @return The JSON Web Key (JWK) set.
370         *
371         * @throws IOException    If the file couldn't be read.
372         * @throws ParseException If the file couldn't be parsed to a valid
373         *                        JSON Web Key (JWK) set.
374         */
375        public static JWKSet load(final URL url,
376                                  final int connectTimeout,
377                                  final int readTimeout,
378                                  final int sizeLimit)
379                throws IOException, ParseException {
380
381                return parse(URLUtils.read(url, connectTimeout, readTimeout, sizeLimit));
382        }
383
384
385        /**
386         * Loads a JSON Web Key (JWK) set from the specified URL.
387         *
388         * @param url The JWK set URL. Must not be {@code null}.
389         *
390         * @return The JSON Web Key (JWK) set.
391         *
392         * @throws IOException    If the file couldn't be read.
393         * @throws ParseException If the file couldn't be parsed to a valid
394         *                        JSON Web Key (JWK) set.
395         */
396        public static JWKSet load(final URL url)
397                throws IOException, ParseException {
398
399                return parse(URLUtils.read(url, 0, 0, -1));
400        }
401}