Update to sync with interface.
[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.NameIdentifierMapping;
90 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
91 import edu.internet2.middleware.shibboleth.common.ServiceProvider;
92 import edu.internet2.middleware.shibboleth.common.ShibResource;
93 import edu.internet2.middleware.shibboleth.hs.HSNameIdentifierMapping;
94
95 /**
96  * <code>HSNameIdentifierMapping</code> implementation that uses symmetric
97  * encryption to securely transport principal data inside Shibboleth Attribute
98  * Query Handles.
99  * 
100  * @author Walter Hoehn
101  */
102 public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSNameIdentifierMapping {
103
104         private static Logger log = Logger.getLogger(CryptoShibHandle.class.getName());
105         protected SecretKey secret;
106         private SecureRandom random = new SecureRandom();
107
108         public CryptoShibHandle(Element config) throws NameIdentifierMappingException {
109                 super(config);
110                 try {
111
112                         String keyStorePath = getElementConfigData(config, "KeyStorePath");
113                         String keyStorePassword = getElementConfigData(config, "KeyStorePassword");
114                         String keyStoreKeyAlias = getElementConfigData(config, "KeyStoreKeyAlias");
115                         String keyStoreKeyPassword = getElementConfigData(config, "KeyStoreKeyPassword");
116
117                         KeyStore keyStore = KeyStore.getInstance("JCEKS");
118                         keyStore.load(
119                                 new ShibResource(keyStorePath, this.getClass()).getInputStream(),
120                                 keyStorePassword.toCharArray());
121                         secret = (SecretKey) keyStore.getKey(keyStoreKeyAlias, keyStoreKeyPassword.toCharArray());
122
123                         //Before we finish initilization, make sure that things are
124                         // working
125                         testEncryption();
126
127                         if (usingDefaultSecret()) {
128                                 log.warn(
129                                         "You are running Crypto AQH Name Mapping with the default secret key.  This is UNSAFE!  Please change "
130                                                 + "this configuration and restart the origin.");
131                         }
132                 } catch (StreamCorruptedException e) {
133                         if (System.getProperty("java.version").startsWith("1.4.2")) {
134                                 log.error(
135                                         "There is a bug in Java 1.4.2 that prevents JCEKS keystores from being loaded properly.  "
136                                                 + "You probably need to upgrade or downgrade your JVM in order to make this work.");
137                         }
138                         log.error(
139                                 "An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping: " + e);
140                         throw new NameIdentifierMappingException("An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping.");
141                 } catch (KeyStoreException e) {
142                         log.error(
143                                 "An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping: " + e);
144                         throw new NameIdentifierMappingException("An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping.");
145                 } catch (CertificateException e) {
146                         log.error("The java keystore contained corrupted data.  Unable to initialize Crypto Name Mapping: " + e);
147                         throw new NameIdentifierMappingException("The java keystore contained corrupted data.  Unable to initialize Crypto Name Mapping.");
148                 } catch (NoSuchAlgorithmException e) {
149                         log.error(
150                                 "Appropriate JCE provider not found in the java environment. Unable to initialize Crypto Name Mapping: "
151                                         + e);
152                         throw new NameIdentifierMappingException("Appropriate JCE provider not found in the java environment. Unable to initialize Crypto Name Mapping.");
153                 } catch (IOException e) {
154                         log.error(
155                                 "An error accessing while loading the java keystore.  Unable to initialize Crypto Name Mapping: " + e);
156                         throw new NameIdentifierMappingException("An error occurred while accessing the java keystore.  Unable to initialize Crypto Name Mapping.");
157                 } catch (UnrecoverableKeyException e) {
158                         log.error(
159                                 "Secret could not be loaded from the java keystore.  Verify that the alias and password are correct: "
160                                         + e);
161                         throw new NameIdentifierMappingException("Secret could not be loaded from the java keystore.  Verify that the alias and password are correct. ");
162                 }
163         }
164
165         public AuthNPrincipal getPrincipal(SAMLNameIdentifier nameId, ServiceProvider sProv, IdentityProvider idProv)
166                 throws NameIdentifierMappingException, InvalidNameIdentifierException {
167
168                 try {
169                         //Separate the IV and handle
170                         byte[] in = new BASE64Decoder().decodeBuffer(nameId.getName());
171                         if (in.length < 9) {
172                                 log.debug("Attribute Query Handle is malformed (not enough bytes).");
173                                 throw new InvalidNameIdentifierException("Attribute Query Handle is malformed (not enough bytes).");
174                         }
175                         byte[] iv = new byte[8];
176                         System.arraycopy(in, 0, iv, 0, 8);
177                         byte[] encryptedHandle = new byte[in.length - iv.length];
178                         System.arraycopy(in, 8, encryptedHandle, 0, in.length - iv.length);
179
180                         Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
181                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
182                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
183
184                         byte[] objectArray = cipher.doFinal(encryptedHandle);
185                         GZIPInputStream zipBytesIn = new GZIPInputStream(new ByteArrayInputStream(objectArray));
186
187                         ObjectInputStream objectStream = new ObjectInputStream(zipBytesIn);
188
189                         HMACHandleEntry handleEntry = (HMACHandleEntry) objectStream.readObject();
190                         objectStream.close();
191
192                         if (handleEntry.isExpired()) {
193                                 log.debug("Attribute Query Handle is expired.");
194                                 throw new InvalidNameIdentifierException("Attribute Query Handle is expired.");
195                         }
196
197                         Mac mac = Mac.getInstance("HmacSHA1");
198                         mac.init(secret);
199                         if (!handleEntry.isValid(mac)) {
200                                 log.warn("Attribute Query Handle failed integrity check.");
201                                 throw new InvalidNameIdentifierException("Attribute Query Handle failed integrity check.");
202                         }
203
204                         log.debug("Attribute Query Handle recognized.");
205                         return handleEntry.principal;
206
207                 } catch (NoSuchAlgorithmException e) {
208                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Algorithm: " + e);
209                         throw new InvalidNameIdentifierException("Appropriate JCE provider not found in the java environment.  Could not load Algorithm.");
210                 } catch (NoSuchPaddingException e) {
211                         log.error(
212                                 "Appropriate JCE provider not found in the java environment.  Could not load Padding method: " + e);
213                         throw new InvalidNameIdentifierException("Appropriate JCE provider not found in the java environment.  Could not load Padding method.");
214                 } catch (InvalidKeyException e) {
215                         log.error("Could not use the supplied secret key: " + e);
216                         throw new InvalidNameIdentifierException("Could not use the supplied secret key.");
217                 } catch (GeneralSecurityException e) {
218                         log.warn("Unable to decrypt the supplied Attribute Query Handle: " + e);
219                         throw new InvalidNameIdentifierException("Unable to decrypt the supplied Attribute Query Handle.");
220                 } catch (ClassNotFoundException e) {
221                         log.warn("The supplied Attribute Query Handle does not represent a serialized AuthNPrincipal: " + e);
222                         throw new InvalidNameIdentifierException("The supplied Attribute Query Handle does not represent a serialized AuthNPrincipal.");
223                 } catch (IOException e) {
224                         log.warn("The AuthNPrincipal could not be de-serialized from the supplied Attribute Query Handle: " + e);
225                         throw new InvalidNameIdentifierException("The AuthNPrincipal could not be de-serialized from the supplied Attribute Query Handle.");
226                 }
227         }
228
229         public SAMLNameIdentifier getNameIdentifierName(
230                 AuthNPrincipal principal,
231                 ServiceProvider sProv,
232                 IdentityProvider idProv)
233                 throws NameIdentifierMappingException {
234                 try {
235                         if (principal == null) {
236                                 log.error("A principal must be supplied for Attribute Query Handle creation.");
237                                 throw new IllegalArgumentException("A principal must be supplied for Attribute Query Handle creation.");
238                         }
239
240                         HandleEntry handleEntry = createHandleEntry(principal);
241
242                         Mac mac = Mac.getInstance("HmacSHA1");
243                         mac.init(secret);
244                         HMACHandleEntry macHandleEntry = new HMACHandleEntry(handleEntry, mac);
245
246                         ByteArrayOutputStream outStream = new ByteArrayOutputStream();
247                         ByteArrayOutputStream encStream = new ByteArrayOutputStream();
248
249                         Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
250                         byte[] iv = new byte[8];
251                         random.nextBytes(iv);
252                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
253                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
254
255                         //Handle contains 8 byte IV, followed by cipher text
256                         outStream.write(cipher.getIV());
257
258                         ObjectOutput objectStream = new ObjectOutputStream(new GZIPOutputStream(encStream));
259                         objectStream.writeObject(macHandleEntry);
260                         objectStream.close();
261
262                         outStream.write(cipher.doFinal(encStream.toByteArray()));
263                         encStream.close();
264
265                         String handle = new BASE64Encoder().encode(outStream.toByteArray());
266                         outStream.close();
267
268                         try {
269                                 return new SAMLNameIdentifier(
270                                         handle.replaceAll(System.getProperty("line.separator"), ""),
271                                         idProv.getProviderId(),
272                                         getNameIdentifierFormat().toString());
273                         } catch (SAMLException e) {
274                                 throw new NameIdentifierMappingException("Unable to generate Attribute Query Handle: " + e);
275                         }
276
277                 } catch (KeyException e) {
278                         log.error("Could not use the supplied secret key: " + e);
279                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
280                 } catch (GeneralSecurityException e) {
281                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Cipher: " + e);
282                         throw new NameIdentifierMappingException("Appropriate JCE provider not found in the java environment.  Could not load Cipher.");
283                 } catch (IOException e) {
284                         log.error("Could not serialize Principal for handle creation: " + e);
285                         throw new NameIdentifierMappingException("Could not serialize Principal for Attribute Query Handle creation.");
286                 }
287         }
288
289         private String getElementConfigData(Element e, String itemName) throws NameIdentifierMappingException {
290
291                 NodeList itemElements = e.getElementsByTagNameNS(NameIdentifierMapping.mappingNamespace, itemName);
292
293                 if (itemElements.getLength() < 1) {
294                         log.error(itemName + " not specified.");
295                         throw new NameIdentifierMappingException(
296                                 "Crypto Name Mapping requires a <" + itemName + "> specification.");
297                 }
298
299                 if (itemElements.getLength() > 1) {
300                         log.error("Multiple " + itemName + " specifications, using first.");
301                 }
302
303                 Node tnode = itemElements.item(0).getFirstChild();
304                 String item = null;
305                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
306                         item = tnode.getNodeValue();
307                 }
308                 if (item == null || item.equals("")) {
309                         log.error(itemName + " not specified.");
310                         throw new NameIdentifierMappingException(
311                                 "Crypto Name Mapping requires a <" + itemName + "> specification.");
312                 }
313                 return item;
314         }
315
316         private void testEncryption() throws NameIdentifierMappingException {
317
318                 String decrypted;
319                 try {
320                         Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
321                         cipher.init(Cipher.ENCRYPT_MODE, secret);
322                         byte[] cipherText = cipher.doFinal("test".getBytes());
323                         cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
324                         cipher.init(Cipher.DECRYPT_MODE, secret);
325                         decrypted = new String(cipher.doFinal(cipherText));
326                 } catch (Exception e) {
327                         log.error("Round trip encryption/decryption test unsuccessful: " + e);
328                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
329                 }
330
331                 if (decrypted == null || !decrypted.equals("test")) {
332                         log.error("Round trip encryption/decryption test unsuccessful.  Decrypted text did not match.");
333                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
334                 }
335
336                 byte[] code;
337                 try {
338                         Mac mac = Mac.getInstance("HmacSHA1");
339                         mac.init(secret);
340                         mac.update("foo".getBytes());
341                         code = mac.doFinal();
342
343                 } catch (Exception e) {
344                         log.error("Message Authentication test unsuccessful: " + e);
345                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
346                 }
347
348                 if (code == null) {
349                         log.error("Message Authentication test unsuccessful.");
350                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
351                 }
352         }
353
354         private boolean usingDefaultSecret() {
355                 byte[] defaultKey =
356                         new byte[] {
357                                 (byte) 0xC7,
358                                 (byte) 0x49,
359                                 (byte) 0x80,
360                                 (byte) 0xD3,
361                                 (byte) 0x02,
362                                 (byte) 0x4A,
363                                 (byte) 0x61,
364                                 (byte) 0xEF,
365                                 (byte) 0x25,
366                                 (byte) 0x5D,
367                                 (byte) 0xE3,
368                                 (byte) 0x2F,
369                                 (byte) 0x57,
370                                 (byte) 0x51,
371                                 (byte) 0x20,
372                                 (byte) 0x15,
373                                 (byte) 0xC7,
374                                 (byte) 0x49,
375                                 (byte) 0x80,
376                                 (byte) 0xD3,
377                                 (byte) 0x02,
378                                 (byte) 0x4A,
379                                 (byte) 0x61,
380                                 (byte) 0xEF };
381                 byte[] encodedKey = secret.getEncoded();
382                 return Arrays.equals(defaultKey, encodedKey);
383         }
384
385 }
386
387 /**
388  * <code>HandleEntry</code> extension class that performs message
389  * authentication.
390  *  
391  */
392 class HMACHandleEntry extends HandleEntry implements Serializable {
393
394         static final long serialVersionUID = 1L;
395         protected byte[] code;
396
397         protected HMACHandleEntry(AuthNPrincipal principal, long TTL, Mac mac) {
398                 super(principal, TTL);
399                 mac.update(this.principal.getName().getBytes());
400                 mac.update(new Long(this.expirationTime).byteValue());
401                 code = mac.doFinal();
402         }
403
404         protected HMACHandleEntry(HandleEntry handleEntry, Mac mac) {
405                 super(handleEntry.principal, handleEntry.expirationTime);
406                 mac.update(this.principal.getName().getBytes());
407                 mac.update(new Long(this.expirationTime).byteValue());
408                 code = mac.doFinal();
409         }
410
411         boolean isValid(Mac mac) {
412                 mac.update(this.principal.getName().getBytes());
413                 mac.update(new Long(this.expirationTime).byteValue());
414                 byte[] validationCode = mac.doFinal();
415                 return Arrays.equals(code, validationCode);
416         }
417 }