Errors from requests with invalid subjects are now implementation specific.
[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                 try {
151                         //Separate the IV and handle
152                         byte[] in = new BASE64Decoder().decodeBuffer(nameId.getName());
153                         if (in.length < 9) {
154                                 log.debug("Attribute Query Handle is malformed (not enough bytes).");
155                                 throw new NameIdentifierMappingException("Attribute Query Handle is malformed (not enough bytes).");
156                         }
157                         byte[] iv = new byte[8];
158                         System.arraycopy(in, 0, iv, 0, 8);
159                         byte[] encryptedHandle = new byte[in.length - iv.length];
160                         System.arraycopy(in, 8, encryptedHandle, 0, in.length - iv.length);
161
162                         Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
163                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
164                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
165
166                         byte[] objectArray = cipher.doFinal(encryptedHandle);
167                         GZIPInputStream zipBytesIn = new GZIPInputStream(new ByteArrayInputStream(objectArray));
168
169                         ObjectInputStream objectStream = new ObjectInputStream(zipBytesIn);
170
171                         HMACHandleEntry handleEntry = (HMACHandleEntry) objectStream.readObject();
172                         objectStream.close();
173
174                         if (handleEntry.isExpired()) {
175                                 log.debug("Attribute Query Handle is expired.");
176                                 throw new InvalidNameIdentifierException("Attribute Query Handle is expired.", errorCodes);
177                         }
178
179                         Mac mac = Mac.getInstance("HmacSHA1");
180                         mac.init(secret);
181                         if (!handleEntry.isValid(mac)) {
182                                 log.warn("Attribute Query Handle failed integrity check.");
183                                 throw new NameIdentifierMappingException("Attribute Query Handle failed integrity check.");
184                         }
185
186                         log.debug("Attribute Query Handle recognized.");
187                         return handleEntry.principal;
188
189                 } catch (NoSuchAlgorithmException e) {
190                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Algorithm: " + e);
191                         throw new NameIdentifierMappingException(
192                                         "Appropriate JCE provider not found in the java environment.  Could not load Algorithm.");
193                 } catch (NoSuchPaddingException e) {
194                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Padding method: "
195                                         + e);
196                         throw new NameIdentifierMappingException(
197                                         "Appropriate JCE provider not found in the java environment.  Could not load Padding method.");
198                 } catch (InvalidKeyException e) {
199                         log.error("Could not use the supplied secret key: " + e);
200                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
201                 } catch (GeneralSecurityException e) {
202                         log.warn("Unable to decrypt the supplied Attribute Query Handle: " + e);
203                         throw new NameIdentifierMappingException("Unable to decrypt the supplied Attribute Query Handle.");
204                 } catch (ClassNotFoundException e) {
205                         log.warn("The supplied Attribute Query Handle does not represent a serialized AuthNPrincipal: " + e);
206                         throw new NameIdentifierMappingException(
207                                         "The supplied Attribute Query Handle does not represent a serialized AuthNPrincipal.");
208                 } catch (IOException e) {
209                         log.warn("The AuthNPrincipal could not be de-serialized from the supplied Attribute Query Handle: " + e);
210                         throw new NameIdentifierMappingException(
211                                         "The AuthNPrincipal could not be de-serialized from the supplied Attribute Query Handle.");
212                 }
213         }
214
215         public SAMLNameIdentifier getNameIdentifierName(AuthNPrincipal principal, ServiceProvider sProv,
216                         IdentityProvider idProv) throws NameIdentifierMappingException {
217                 try {
218                         if (principal == null) {
219                                 log.error("A principal must be supplied for Attribute Query Handle creation.");
220                                 throw new IllegalArgumentException("A principal must be supplied for Attribute Query Handle creation.");
221                         }
222
223                         HandleEntry handleEntry = createHandleEntry(principal);
224
225                         Mac mac = Mac.getInstance("HmacSHA1");
226                         mac.init(secret);
227                         HMACHandleEntry macHandleEntry = new HMACHandleEntry(handleEntry, mac);
228
229                         ByteArrayOutputStream outStream = new ByteArrayOutputStream();
230                         ByteArrayOutputStream encStream = new ByteArrayOutputStream();
231
232                         Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
233                         byte[] iv = new byte[8];
234                         random.nextBytes(iv);
235                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
236                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
237
238                         //Handle contains 8 byte IV, followed by cipher text
239                         outStream.write(cipher.getIV());
240
241                         ObjectOutput objectStream = new ObjectOutputStream(new GZIPOutputStream(encStream));
242                         objectStream.writeObject(macHandleEntry);
243                         objectStream.close();
244
245                         outStream.write(cipher.doFinal(encStream.toByteArray()));
246                         encStream.close();
247
248                         String handle = new BASE64Encoder().encode(outStream.toByteArray());
249                         outStream.close();
250
251                         try {
252                                 return new SAMLNameIdentifier(handle.replaceAll(System.getProperty("line.separator"), ""), idProv
253                                                 .getProviderId(), getNameIdentifierFormat().toString());
254                         } catch (SAMLException e) {
255                                 throw new NameIdentifierMappingException("Unable to generate Attribute Query Handle: " + e);
256                         }
257
258                 } catch (KeyException e) {
259                         log.error("Could not use the supplied secret key: " + e);
260                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
261                 } catch (GeneralSecurityException e) {
262                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Cipher: " + e);
263                         throw new NameIdentifierMappingException(
264                                         "Appropriate JCE provider not found in the java environment.  Could not load Cipher.");
265                 } catch (IOException e) {
266                         log.error("Could not serialize Principal for handle creation: " + e);
267                         throw new NameIdentifierMappingException(
268                                         "Could not serialize Principal for Attribute Query Handle creation.");
269                 }
270         }
271
272         private String getElementConfigData(Element e, String itemName) throws NameIdentifierMappingException {
273
274                 NodeList itemElements = e.getElementsByTagNameNS(NameIdentifierMapping.mappingNamespace, itemName);
275
276                 if (itemElements.getLength() < 1) {
277                         log.error(itemName + " not specified.");
278                         throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName + "> specification.");
279                 }
280
281                 if (itemElements.getLength() > 1) {
282                         log.error("Multiple " + itemName + " specifications, using first.");
283                 }
284
285                 Node tnode = itemElements.item(0).getFirstChild();
286                 String item = null;
287                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
288                         item = tnode.getNodeValue();
289                 }
290                 if (item == null || item.equals("")) {
291                         log.error(itemName + " not specified.");
292                         throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName + "> specification.");
293                 }
294                 return item;
295         }
296
297         private void testEncryption() throws NameIdentifierMappingException {
298
299                 String decrypted;
300                 try {
301                         Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
302                         cipher.init(Cipher.ENCRYPT_MODE, secret);
303                         byte[] cipherText = cipher.doFinal("test".getBytes());
304                         cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
305                         cipher.init(Cipher.DECRYPT_MODE, secret);
306                         decrypted = new String(cipher.doFinal(cipherText));
307                 } catch (Exception e) {
308                         log.error("Round trip encryption/decryption test unsuccessful: " + e);
309                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
310                 }
311
312                 if (decrypted == null || !decrypted.equals("test")) {
313                         log.error("Round trip encryption/decryption test unsuccessful.  Decrypted text did not match.");
314                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
315                 }
316
317                 byte[] code;
318                 try {
319                         Mac mac = Mac.getInstance("HmacSHA1");
320                         mac.init(secret);
321                         mac.update("foo".getBytes());
322                         code = mac.doFinal();
323
324                 } catch (Exception e) {
325                         log.error("Message Authentication test unsuccessful: " + e);
326                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
327                 }
328
329                 if (code == null) {
330                         log.error("Message Authentication test unsuccessful.");
331                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
332                 }
333         }
334
335         private boolean usingDefaultSecret() {
336                 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
337                                 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
338                                 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
339                                 (byte) 0x61, (byte) 0xEF};
340                 byte[] encodedKey = secret.getEncoded();
341                 return Arrays.equals(defaultKey, encodedKey);
342         }
343
344 }
345
346 /**
347  * <code>HandleEntry</code> extension class that performs message authentication.
348  */
349
350 class HMACHandleEntry extends HandleEntry implements Serializable {
351
352         static final long       serialVersionUID        = 1L;
353         protected byte[]        code;
354
355         protected HMACHandleEntry(AuthNPrincipal principal, long TTL, Mac mac) {
356                 super(principal, TTL);
357                 mac.update(this.principal.getName().getBytes());
358                 mac.update(new Long(this.expirationTime).byteValue());
359                 code = mac.doFinal();
360         }
361
362         protected HMACHandleEntry(HandleEntry handleEntry, Mac mac) {
363                 super(handleEntry.principal, handleEntry.expirationTime);
364                 mac.update(this.principal.getName().getBytes());
365                 mac.update(new Long(this.expirationTime).byteValue());
366                 code = mac.doFinal();
367         }
368
369         boolean isValid(Mac mac) {
370                 mac.update(this.principal.getName().getBytes());
371                 mac.update(new Long(this.expirationTime).byteValue());
372                 byte[] validationCode = mac.doFinal();
373                 return Arrays.equals(code, validationCode);
374         }
375 }