61d52a3147e4a1e893e6b1c453e01fd4269888c3
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / hs / provider / CryptoHandleRepository.java
1 /* 
2  * The Shibboleth License, Version 1. 
3  * Copyright (c) 2002 
4  * University Corporation for Advanced Internet Development, Inc. 
5  * All rights reserved
6  * 
7  * 
8  * Redistribution and use in source and binary forms, with or without 
9  * modification, are permitted provided that the following conditions are met:
10  * 
11  * Redistributions of source code must retain the above copyright notice, this 
12  * list of conditions and the following disclaimer.
13  * 
14  * Redistributions in binary form must reproduce the above copyright notice, 
15  * this list of conditions and the following disclaimer in the documentation 
16  * and/or other materials provided with the distribution, if any, must include 
17  * the following acknowledgment: "This product includes software developed by 
18  * the University Corporation for Advanced Internet Development 
19  * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement 
20  * may appear in the software itself, if and wherever such third-party 
21  * acknowledgments normally appear.
22  * 
23  * Neither the name of Shibboleth nor the names of its contributors, nor 
24  * Internet2, nor the University Corporation for Advanced Internet Development, 
25  * Inc., nor UCAID may be used to endorse or promote products derived from this 
26  * software without specific prior written permission. For written permission, 
27  * please contact shibboleth@shibboleth.org
28  * 
29  * Products derived from this software may not be called Shibboleth, Internet2, 
30  * UCAID, or the University Corporation for Advanced Internet Development, nor 
31  * may Shibboleth appear in their name, without prior written permission of the 
32  * University Corporation for Advanced Internet Development.
33  * 
34  * 
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
36  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
37  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
38  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK 
39  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. 
40  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY 
41  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, 
42  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
43  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
44  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
46  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
47  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48  */
49
50 package edu.internet2.middleware.shibboleth.hs.provider;
51
52 import java.io.ByteArrayInputStream;
53 import java.io.ByteArrayOutputStream;
54 import java.io.IOException;
55 import java.io.ObjectInputStream;
56 import java.io.ObjectOutput;
57 import java.io.ObjectOutputStream;
58 import java.io.Serializable;
59 import java.security.GeneralSecurityException;
60 import java.security.InvalidKeyException;
61 import java.security.KeyException;
62 import java.security.KeyStore;
63 import java.security.KeyStoreException;
64 import java.security.NoSuchAlgorithmException;
65 import java.security.SecureRandom;
66 import java.security.UnrecoverableKeyException;
67 import java.security.cert.CertificateException;
68 import java.util.Arrays;
69 import java.util.Properties;
70 import java.util.zip.GZIPInputStream;
71 import java.util.zip.GZIPOutputStream;
72
73 import javax.crypto.Cipher;
74 import javax.crypto.Mac;
75 import javax.crypto.NoSuchPaddingException;
76 import javax.crypto.SecretKey;
77 import javax.crypto.spec.IvParameterSpec;
78
79 import org.apache.log4j.Logger;
80
81 import sun.misc.BASE64Decoder;
82 import sun.misc.BASE64Encoder;
83 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
84 import edu.internet2.middleware.shibboleth.common.Constants;
85 import edu.internet2.middleware.shibboleth.common.ShibResource;
86 import edu.internet2.middleware.shibboleth.hs.HandleRepository;
87 import edu.internet2.middleware.shibboleth.hs.HandleRepositoryException;
88 import edu.internet2.middleware.shibboleth.hs.InvalidHandleException;
89
90 /**
91  * <code>HandleRepository</code> implementation that employs the use of a shard secret
92  * in order to transmit identity information.
93  * 
94  * @author Walter Hoehn (wassa@columbia.edu)
95  */
96 public class CryptoHandleRepository extends BaseHandleRepository implements HandleRepository {
97
98         private static Logger log = Logger.getLogger(CryptoHandleRepository.class.getName());
99         protected SecretKey secret;
100         private SecureRandom random = new SecureRandom();
101         
102         public CryptoHandleRepository(Properties properties) throws HandleRepositoryException {
103                 super(properties);
104                 try {
105
106                         checkRequiredParams(properties);
107                         KeyStore keyStore = KeyStore.getInstance("JCEKS");
108
109                         keyStore.load(
110                                 new ShibResource(
111                                         properties.getProperty(
112                                                 "edu.internet2.middleware.shibboleth.hs.provider.CryptoHandleRepository.keyStorePath"),
113                                         this.getClass())
114                                         .getInputStream(),
115                                 properties
116                                         .getProperty("edu.internet2.middleware.shibboleth.hs.provider.CryptoHandleRepository.keyStorePassword")
117                                         .toCharArray());
118                         secret =
119                                 (SecretKey) keyStore.getKey(
120                                         properties.getProperty(
121                                                 "edu.internet2.middleware.shibboleth.hs.provider.CryptoHandleRepository.keyStoreKeyAlias"),
122                                         properties
123                                                 .getProperty("edu.internet2.middleware.shibboleth.hs.provider.CryptoHandleRepository.keyStoreKeyPassword")
124                                                 .toCharArray());
125
126                         //Before we finish initilization, make sure that things are working
127                         testEncryption();
128
129                         if (usingDefaultSecret()) {
130                                 log.warn(
131                                         "You are running the Crypto Handle Repository with the default secret key.  This is UNSAFE!  Please change "
132                                                 + "this configuration and restart the origin.");
133                         }
134
135                 } catch (KeyStoreException e) {
136                         log.error(
137                                 "An error occurred while loading the java keystore.  Unable to initialize Crypto Handle Repository: "
138                                         + e);
139                         throw new HandleRepositoryException("An error occurred while loading the java keystore.  Unable to initialize Crypto Handle Repository.");
140                 } catch (CertificateException e) {
141                         log.error(
142                                 "The java keystore contained corrupted data.  Unable to initialize Crypto Handle Repository: " + e);
143                         throw new HandleRepositoryException("The java keystore contained corrupted data.  Unable to initialize Crypto Handle Repository.");
144                 } catch (NoSuchAlgorithmException e) {
145                         log.error(
146                                 "Appropriate JCE provider not found in the java environment. Unable to initialize Crypto Handle Repository: "
147                                         + e);
148                         throw new HandleRepositoryException("Appropriate JCE provider not found in the java environment. Unable to initialize Crypto Handle Repository.");
149                 } catch (IOException e) {
150                         log.error(
151                                 "An error accessing while loading the java keystore.  Unable to initialize Crypto Handle Repository: "
152                                         + e);
153                         throw new HandleRepositoryException("An error occurred while accessing the java keystore.  Unable to initialize Crypto Handle Repository.");
154                 } catch (UnrecoverableKeyException e) {
155                         log.error(
156                                 "Secret could not be loaded from the java keystore.  Verify that the alias and password are correct: "
157                                         + e);
158                         throw new HandleRepositoryException("Secret could not be loaded from the java keystore.  Verify that the alias and password are correct. ");
159                 }
160         }
161
162         private boolean usingDefaultSecret() {
163                 byte[] defaultKey =
164                         new byte[] {
165                                 (byte) 0xC7,
166                                 (byte) 0x49,
167                                 (byte) 0x80,
168                                 (byte) 0xD3,
169                                 (byte) 0x02,
170                                 (byte) 0x4A,
171                                 (byte) 0x61,
172                                 (byte) 0xEF,
173                                 (byte) 0x25,
174                                 (byte) 0x5D,
175                                 (byte) 0xE3,
176                                 (byte) 0x2F,
177                                 (byte) 0x57,
178                                 (byte) 0x51,
179                                 (byte) 0x20,
180                                 (byte) 0x15,
181                                 (byte) 0xC7,
182                                 (byte) 0x49,
183                                 (byte) 0x80,
184                                 (byte) 0xD3,
185                                 (byte) 0x02,
186                                 (byte) 0x4A,
187                                 (byte) 0x61,
188                                 (byte) 0xEF };
189                 byte[] encodedKey = secret.getEncoded();
190                 return Arrays.equals(defaultKey, encodedKey);
191         }
192
193         private void checkRequiredParams(Properties params) throws HandleRepositoryException {
194                 StringBuffer missingProperties = new StringBuffer();
195                 String[] requiredProperties =
196                         {
197                                 "edu.internet2.middleware.shibboleth.hs.provider.CryptoHandleRepository.keyStorePath",
198                                 "edu.internet2.middleware.shibboleth.hs.provider.CryptoHandleRepository.keyStorePassword",
199                                 "edu.internet2.middleware.shibboleth.hs.provider.CryptoHandleRepository.keyStoreKeyAlias",
200                                 "edu.internet2.middleware.shibboleth.hs.provider.CryptoHandleRepository.keyStoreKeyPassword" };
201
202                 for (int i = 0; i < requiredProperties.length; i++) {
203                         if (params.getProperty(requiredProperties[i]) == null) {
204                                 missingProperties.append("\"");
205                                 missingProperties.append(requiredProperties[i]);
206                                 missingProperties.append("\" ");
207                         }
208                 }
209                 if (missingProperties.length() > 0) {
210                         log.error(
211                                 "Missing configuration data.  The following configuration properites are required for the Crypto Handle Repository and have not been set: "
212                                         + missingProperties.toString());
213                         throw new HandleRepositoryException("Missing configuration data.");
214                 }
215         }
216
217         /**
218          * @see edu.internet2.middleware.shibboleth.hs.HandleRepository#getHandle(Principal)
219          */
220         public String getHandle(AuthNPrincipal principal, StringBuffer format) throws HandleRepositoryException {
221                 try {
222                         if (principal == null || format == null) {
223                                 log.error("A principal and format buffer must be supplied for Attribute Query Handle creation.");
224                                 throw new IllegalArgumentException("A principal and format buffer must be supplied for Attribute Query Handle creation.");
225                         }
226
227                         HandleEntry handleEntry = createHandleEntry(principal);
228
229                         Mac mac = Mac.getInstance("HmacSHA1");
230                         mac.init(secret);
231                         HMACHandleEntry macHandleEntry = new HMACHandleEntry(handleEntry, mac);
232
233                         ByteArrayOutputStream outStream = new ByteArrayOutputStream();
234                         ByteArrayOutputStream encStream = new ByteArrayOutputStream();
235
236                         Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
237                         byte[] iv = new byte[8];
238                         random.nextBytes(iv);
239                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
240                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
241
242                         //Handle contains 8 byte IV, followed by cipher text
243                         outStream.write(cipher.getIV());
244
245                         ObjectOutput objectStream = new ObjectOutputStream(new GZIPOutputStream(encStream));
246                         objectStream.writeObject(macHandleEntry);
247                         objectStream.close();
248
249                         outStream.write(cipher.doFinal(encStream.toByteArray()));
250                         encStream.close();
251
252                         String handle = new BASE64Encoder().encode(outStream.toByteArray());
253                         outStream.close();
254
255                         format.setLength(0);
256                         format.append(Constants.SHIB_NAMEID_FORMAT_URI);
257
258                         return handle.replaceAll(System.getProperty("line.separator"), "");
259
260                 } catch (KeyException e) {
261                         log.error("Could not use the supplied secret key: " + e);
262                         throw new HandleRepositoryException("Could not use the supplied secret key.");
263                 } catch (GeneralSecurityException e) {
264                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Cipher: " + e);
265                         throw new HandleRepositoryException("Appropriate JCE provider not found in the java environment.  Could not load Cipher.");
266                 } catch (IOException e) {
267                         log.error("Could not serialize Principal for handle creation: " + e);
268                         throw new HandleRepositoryException("Could not serialize Principal for Attribute Query Handle creation.");
269                 }
270         }
271
272         /**
273          * @see edu.internet2.middleware.shibboleth.hs.HandleRepository#getPrincipal(String)
274          */
275         public AuthNPrincipal getPrincipal(String handle, String format)
276                 throws HandleRepositoryException, InvalidHandleException {
277                 if (!Constants.SHIB_NAMEID_FORMAT_URI.equals(format)) {
278                         log.debug(
279                                 "This Repository does not understand handles with a format URI of "
280                                         + (format == null ? "null" : format));
281                         throw new InvalidHandleException(
282                                 "This Repository does not understand handles with a format URI of "
283                                         + (format == null ? "null" : format));
284                 }
285
286                 try {
287                         //Separate the IV and handle
288                         byte[] in = new BASE64Decoder().decodeBuffer(handle);
289                         if (in.length < 9) {
290                                 log.debug("Attribute Query Handle is malformed (not enough bytes).");
291                                 throw new InvalidHandleException("Attribute Query Handle is malformed (not enough bytes).");
292                         }
293                         byte[] iv = new byte[8];
294                         System.arraycopy(in, 0, iv, 0, 8);
295                         byte[] encryptedHandle = new byte[in.length - iv.length];
296                         System.arraycopy(in, 8, encryptedHandle, 0, in.length - iv.length);
297
298                         Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
299                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
300                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
301
302                         byte[] objectArray = cipher.doFinal(encryptedHandle);
303                         GZIPInputStream zipBytesIn = new GZIPInputStream(new ByteArrayInputStream(objectArray));
304
305                         ObjectInputStream objectStream = new ObjectInputStream(zipBytesIn);
306
307                         HMACHandleEntry handleEntry = (HMACHandleEntry) objectStream.readObject();
308                         objectStream.close();
309
310                         if (handleEntry.isExpired()) {
311                                 log.debug("Attribute Query Handle is expired.");
312                                 throw new InvalidHandleException("Attribute Query Handle is expired.");
313                         }
314
315                         Mac mac = Mac.getInstance("HmacSHA1");
316                         mac.init(secret);
317                         if (!handleEntry.isValid(mac)) {
318                                 log.warn("Attribute Query Handle failed integrity check.");
319                                 throw new InvalidHandleException("Attribute Query Handle failed integrity check.");
320                         }
321
322                         log.debug("Attribute Query Handle recognized.");
323                         return handleEntry.principal;
324
325                 } catch (NoSuchAlgorithmException e) {
326                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Algorithm: " + e);
327                         throw new HandleRepositoryException("Appropriate JCE provider not found in the java environment.  Could not load Algorithm.");
328                 } catch (NoSuchPaddingException e) {
329                         log.error(
330                                 "Appropriate JCE provider not found in the java environment.  Could not load Padding method: " + e);
331                         throw new HandleRepositoryException("Appropriate JCE provider not found in the java environment.  Could not load Padding method.");
332                 } catch (InvalidKeyException e) {
333                         log.error("Could not use the supplied secret key: " + e);
334                         throw new HandleRepositoryException("Could not use the supplied secret key.");
335                 } catch (GeneralSecurityException e) {
336                         log.warn("Unable to decrypt the supplied Attribute Query Handle: " + e);
337                         throw new InvalidHandleException("Unable to decrypt the supplied Attribute Query Handle.");
338                 } catch (ClassNotFoundException e) {
339                         log.warn("The supplied Attribute Query Handle does not represent a serialized AuthNPrincipal: " + e);
340                         throw new InvalidHandleException("The supplied Attribute Query Handle does not represent a serialized AuthNPrincipal.");
341                 } catch (IOException e) {
342                         log.warn("The AuthNPrincipal could not be de-serialized from the supplied Attribute Query Handle: " + e);
343                         throw new InvalidHandleException("The AuthNPrincipal could not be de-serialized from the supplied Attribute Query Handle.");
344                 }
345         }
346
347         private void testEncryption() throws HandleRepositoryException {
348
349                 String decrypted;
350                 try {
351                         Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
352                         cipher.init(Cipher.ENCRYPT_MODE, secret);
353                         byte[] cipherText = cipher.doFinal("test".getBytes());
354                         cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
355                         cipher.init(Cipher.DECRYPT_MODE, secret);
356                         decrypted = new String(cipher.doFinal(cipherText));
357                 } catch (Exception e) {
358                         log.error("Round trip encryption/decryption test unsuccessful: " + e);
359                         throw new HandleRepositoryException("Round trip encryption/decryption test unsuccessful.");
360                 }
361
362                 if (decrypted == null || !decrypted.equals("test")) {
363                         log.error("Round trip encryption/decryption test unsuccessful.  Decrypted text did not match.");
364                         throw new HandleRepositoryException("Round trip encryption/decryption test unsuccessful.");
365                 }
366
367                 byte[] code;
368                 try {
369                         Mac mac = Mac.getInstance("HmacSHA1");
370                         mac.init(secret);
371                         mac.update("foo".getBytes());
372                         code = mac.doFinal();
373
374                 } catch (Exception e) {
375                         log.error("Message Authentication test unsuccessful: " + e);
376                         throw new HandleRepositoryException("Message Authentication test unsuccessful.");
377                 }
378
379                 if (code == null) {
380                         log.error("Message Authentication test unsuccessful.");
381                         throw new HandleRepositoryException("Message Authentication test unsuccessful.");
382                 }
383         }
384
385 }
386
387
388
389 /**
390  * <code>HandleEntry</code> extension class that performs message authentication.
391  * 
392  */
393 class HMACHandleEntry extends HandleEntry implements Serializable {
394
395         static final long serialVersionUID = 1L;
396         protected byte[] code;
397
398         protected HMACHandleEntry(AuthNPrincipal principal, long TTL, Mac mac) {
399                 super(principal, TTL);
400                 mac.update(this.principal.getName().getBytes());
401                 mac.update(new Long(this.expirationTime).byteValue());
402                 code = mac.doFinal();
403         }
404
405         protected HMACHandleEntry(HandleEntry handleEntry, Mac mac) {
406                 super(handleEntry.principal, handleEntry.expirationTime);
407                 mac.update(this.principal.getName().getBytes());
408                 mac.update(new Long(this.expirationTime).byteValue());
409                 code = mac.doFinal();
410         }
411
412         boolean isValid(Mac mac) {
413                 mac.update(this.principal.getName().getBytes());
414                 mac.update(new Long(this.expirationTime).byteValue());
415                 byte[] validationCode = mac.doFinal();
416                 return Arrays.equals(code, validationCode);
417         }
418 }