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