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