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