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