Added javadoc for NameMapper classes.
[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  * <code>HSNameIdentifierMapping</code> implementation that uses symmetric
96  * encryption to securely transport principal data inside Shibboleth Attribute
97  * 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.getId(),
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                 //TODO move to namespace aware method
291                 //NodeList itemElements =
292                 // e.getElementsByTagNameNS(NameIdentifierMapping.mappingNamespace,
293                 // itemName);
294                 NodeList itemElements = e.getElementsByTagName(itemName);
295                 if (itemElements.getLength() < 1) {
296                         log.error(itemName + " not specified.");
297                         throw new NameIdentifierMappingException(
298                                 "Crypto Name Mapping requires a <" + itemName + "> specification.");
299                 }
300
301                 if (itemElements.getLength() > 1) {
302                         log.error("Multiple " + itemName + " specifications, using first.");
303                 }
304
305                 Node tnode = itemElements.item(0).getFirstChild();
306                 String item = null;
307                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
308                         item = tnode.getNodeValue();
309                 }
310                 if (item == null || item.equals("")) {
311                         log.error(itemName + " not specified.");
312                         throw new NameIdentifierMappingException(
313                                 "Crypto Name Mapping requires a <" + itemName + "> specification.");
314                 }
315                 return item;
316         }
317
318         private void testEncryption() throws NameIdentifierMappingException {
319
320                 String decrypted;
321                 try {
322                         Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
323                         cipher.init(Cipher.ENCRYPT_MODE, secret);
324                         byte[] cipherText = cipher.doFinal("test".getBytes());
325                         cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
326                         cipher.init(Cipher.DECRYPT_MODE, secret);
327                         decrypted = new String(cipher.doFinal(cipherText));
328                 } catch (Exception e) {
329                         log.error("Round trip encryption/decryption test unsuccessful: " + e);
330                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
331                 }
332
333                 if (decrypted == null || !decrypted.equals("test")) {
334                         log.error("Round trip encryption/decryption test unsuccessful.  Decrypted text did not match.");
335                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
336                 }
337
338                 byte[] code;
339                 try {
340                         Mac mac = Mac.getInstance("HmacSHA1");
341                         mac.init(secret);
342                         mac.update("foo".getBytes());
343                         code = mac.doFinal();
344
345                 } catch (Exception e) {
346                         log.error("Message Authentication test unsuccessful: " + e);
347                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
348                 }
349
350                 if (code == null) {
351                         log.error("Message Authentication test unsuccessful.");
352                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
353                 }
354         }
355
356         private boolean usingDefaultSecret() {
357                 byte[] defaultKey =
358                         new byte[] {
359                                 (byte) 0xC7,
360                                 (byte) 0x49,
361                                 (byte) 0x80,
362                                 (byte) 0xD3,
363                                 (byte) 0x02,
364                                 (byte) 0x4A,
365                                 (byte) 0x61,
366                                 (byte) 0xEF,
367                                 (byte) 0x25,
368                                 (byte) 0x5D,
369                                 (byte) 0xE3,
370                                 (byte) 0x2F,
371                                 (byte) 0x57,
372                                 (byte) 0x51,
373                                 (byte) 0x20,
374                                 (byte) 0x15,
375                                 (byte) 0xC7,
376                                 (byte) 0x49,
377                                 (byte) 0x80,
378                                 (byte) 0xD3,
379                                 (byte) 0x02,
380                                 (byte) 0x4A,
381                                 (byte) 0x61,
382                                 (byte) 0xEF };
383                 byte[] encodedKey = secret.getEncoded();
384                 return Arrays.equals(defaultKey, encodedKey);
385         }
386
387 }
388
389 /**
390  * <code>HandleEntry</code> extension class that performs message
391  * authentication.
392  *  
393  */
394 class HMACHandleEntry extends HandleEntry implements Serializable {
395
396         static final long serialVersionUID = 1L;
397         protected byte[] code;
398
399         protected HMACHandleEntry(AuthNPrincipal principal, long TTL, Mac mac) {
400                 super(principal, TTL);
401                 mac.update(this.principal.getName().getBytes());
402                 mac.update(new Long(this.expirationTime).byteValue());
403                 code = mac.doFinal();
404         }
405
406         protected HMACHandleEntry(HandleEntry handleEntry, Mac mac) {
407                 super(handleEntry.principal, handleEntry.expirationTime);
408                 mac.update(this.principal.getName().getBytes());
409                 mac.update(new Long(this.expirationTime).byteValue());
410                 code = mac.doFinal();
411         }
412
413         boolean isValid(Mac mac) {
414                 mac.update(this.principal.getName().getBytes());
415                 mac.update(new Long(this.expirationTime).byteValue());
416                 byte[] validationCode = mac.doFinal();
417                 return Arrays.equals(code, validationCode);
418         }
419 }