06737eed9a418deb6242f4badc82f77a60e1dba6
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / 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 above
6  * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7  * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8  * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2 Project.
9  * 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, nor
11  * 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 contact
13  * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14  * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15  * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16  * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18  * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19  * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 package edu.internet2.middleware.shibboleth.common.provider;
27
28 import java.io.ByteArrayInputStream;
29 import java.io.ByteArrayOutputStream;
30 import java.io.DataInputStream;
31 import java.io.DataOutputStream;
32 import java.io.IOException;
33 import java.io.StreamCorruptedException;
34 import java.security.GeneralSecurityException;
35 import java.security.InvalidKeyException;
36 import java.security.KeyException;
37 import java.security.KeyStore;
38 import java.security.KeyStoreException;
39 import java.security.NoSuchAlgorithmException;
40 import java.security.SecureRandom;
41 import java.security.UnrecoverableKeyException;
42 import java.security.cert.CertificateException;
43 import java.util.Arrays;
44 import java.util.zip.GZIPInputStream;
45 import java.util.zip.GZIPOutputStream;
46
47 import javax.crypto.Cipher;
48 import javax.crypto.Mac;
49 import javax.crypto.NoSuchPaddingException;
50 import javax.crypto.SecretKey;
51 import javax.crypto.spec.IvParameterSpec;
52
53 import org.apache.log4j.Logger;
54 import org.opensaml.SAMLException;
55 import org.opensaml.SAMLNameIdentifier;
56 import org.w3c.dom.Element;
57 import org.w3c.dom.Node;
58 import org.w3c.dom.NodeList;
59
60 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
61 import edu.internet2.middleware.shibboleth.common.IdentityProvider;
62 import edu.internet2.middleware.shibboleth.common.InvalidNameIdentifierException;
63 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
64 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
65 import edu.internet2.middleware.shibboleth.common.ServiceProvider;
66 import edu.internet2.middleware.shibboleth.common.ShibResource;
67 import edu.internet2.middleware.shibboleth.utils.Base32;
68
69 /**
70  * {@link NameIdentifierMapping}implementation that uses symmetric encryption to store principal data inside Shibboleth
71  * Attribute Query Handles.
72  * 
73  * @author Walter Hoehn
74  * @author Derek Morr
75  */
76 public class CryptoShibHandle extends AQHNameIdentifierMapping implements NameIdentifierMapping {
77
78         private static Logger log = Logger.getLogger(CryptoShibHandle.class.getName());
79         protected SecretKey secret;
80         private SecureRandom random = new SecureRandom();
81         private String cipherAlgorithm = "DESede/CBC/PKCS5Padding";
82         private String macAlgorithm = "HmacSHA1";
83         private String storeType = "JCEKS";
84
85         public CryptoShibHandle(Element config) throws NameIdentifierMappingException {
86
87                 super(config);
88                 try {
89
90                         String keyStorePath = getElementConfigData(config, "KeyStorePath", true);
91                         String keyStorePassword = getElementConfigData(config, "KeyStorePassword", true);
92                         String keyStoreKeyAlias = getElementConfigData(config, "KeyStoreKeyAlias", true);
93                         String keyStoreKeyPassword = getElementConfigData(config, "KeyStoreKeyPassword", true);
94
95                         String rawStoreType = getElementConfigData(config, "KeyStoreType", false);
96                         if (rawStoreType != null && !rawStoreType.equals("")) {
97                                 storeType = rawStoreType;
98                         }
99                         String rawCipherAlgorithm = getElementConfigData(config, "Cipher", false);
100                         if (rawCipherAlgorithm != null && !rawCipherAlgorithm.equals("")) {
101                                 cipherAlgorithm = rawCipherAlgorithm;
102                         }
103                         String rawMacAlgorithm = getElementConfigData(config, "MAC", false);
104                         if (rawMacAlgorithm != null && !rawMacAlgorithm.equals("")) {
105                                 macAlgorithm = rawMacAlgorithm;
106                         }
107
108                         KeyStore keyStore = KeyStore.getInstance(storeType);
109                         keyStore.load(new ShibResource(keyStorePath, this.getClass()).getInputStream(), keyStorePassword
110                                         .toCharArray());
111                         secret = (SecretKey) keyStore.getKey(keyStoreKeyAlias, keyStoreKeyPassword.toCharArray());
112
113                         // Before we finish initilization, make sure that things are working
114                         testEncryption();
115
116                         if (usingDefaultSecret()) {
117                                 log.warn("You are running Crypto AQH Name Mapping with the "
118                                                 + "default secret key.  This is UNSAFE!  Please change "
119                                                 + "this configuration and restart the origin.");
120                         }
121                 } catch (StreamCorruptedException e) {
122                         if (System.getProperty("java.version").startsWith("1.4.2")) {
123                                 log.error("There is a bug in some versions of Java 1.4.2.x that "
124                                                 + "prevent JCEKS keystores from being loaded properly.  "
125                                                 + "You probably need to upgrade or downgrade your JVM in order to make this work.");
126                         }
127                         log.error("An error occurred while loading the java keystore.  Unable to initialize "
128                                         + "Crypto Name Mapping: " + e);
129                         throw new NameIdentifierMappingException(
130                                         "An error occurred while loading the java keystore.  Unable to initialize Crypto "
131                                                         + "Name Mapping.");
132                 } catch (KeyStoreException e) {
133                         log.error("An error occurred while loading the java keystore.  Unable to initialize Crypto "
134                                         + "Name Mapping: " + e);
135                         throw new NameIdentifierMappingException(
136                                         "An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping.");
137                 } catch (CertificateException e) {
138                         log.error("The java keystore contained corrupted data.  Unable to initialize Crypto Name Mapping: " + e);
139                         throw new NameIdentifierMappingException(
140                                         "The java keystore contained corrupted data.  Unable to initialize Crypto Name Mapping.");
141                 } catch (NoSuchAlgorithmException e) {
142                         log.error("Appropriate JCE provider not found in the java environment. Unable "
143                                         + "to initialize Crypto Name Mapping: " + e);
144                         throw new NameIdentifierMappingException(
145                                         "Appropriate JCE provider not found in the java environment. Unable to initialize Crypto Name Mapping.");
146                 } catch (IOException e) {
147                         log.error("An error accessing while loading the java keystore.  Unable to initialize Crypto Name "
148                                         + "Mapping: " + e);
149                         throw new NameIdentifierMappingException(
150                                         "An error occurred while accessing the java keystore.  Unable to initialize Crypto Name Mapping.");
151                 } catch (UnrecoverableKeyException e) {
152                         log.error("Secret could not be loaded from the java keystore.  Verify that the alias and "
153                                         + "password are correct: " + e);
154                         throw new NameIdentifierMappingException(
155                                         "Secret could not be loaded from the java keystore.  Verify that the alias and password are correct. ");
156                 }
157         }
158
159         /**
160          * Decode an encrypted handle back into a principal
161          */
162         public AuthNPrincipal getPrincipal(SAMLNameIdentifier nameId, ServiceProvider sProv, IdentityProvider idProv)
163                         throws NameIdentifierMappingException, InvalidNameIdentifierException {
164
165                 verifyQualifier(nameId, idProv);
166
167                 try {
168                         byte[] in = Base32.decode(nameId.getName());
169
170                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
171                         int ivSize = cipher.getBlockSize();
172                         byte[] iv = new byte[ivSize];
173
174                         Mac mac = Mac.getInstance(macAlgorithm);
175                         mac.init(secret);
176                         int macSize = mac.getMacLength();
177
178                         if (in.length < ivSize) {
179                                 log.error("Attribute Query Handle is malformed (not enough bytes).");
180                                 throw new NameIdentifierMappingException("Attribute Query Handle is malformed (not enough bytes).");
181                         }
182
183                         // extract the IV, setup the cipher and extract the encrypted handle
184                         System.arraycopy(in, 0, iv, 0, ivSize);
185                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
186                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
187
188                         byte[] encryptedHandle = new byte[in.length - iv.length];
189                         System.arraycopy(in, ivSize, encryptedHandle, 0, in.length - iv.length);
190
191                         // decrypt the rest of the data and setup the streams
192                         byte[] decryptedBytes = cipher.doFinal(encryptedHandle);
193                         ByteArrayInputStream byteStream = new ByteArrayInputStream(decryptedBytes);
194                         GZIPInputStream compressedData = new GZIPInputStream(byteStream);
195                         DataInputStream dataStream = new DataInputStream(compressedData);
196
197                         // extract the components
198                         byte[] decodedMac = new byte[macSize];
199                         int bytesRead = dataStream.read(decodedMac);
200                         if (bytesRead != macSize) {
201                                 log.error("Error parsing handle: Unable to extract HMAC.");
202                                 throw new NameIdentifierMappingException("Error parsing handle: Unable to extract HMAC.");
203                         }
204                         long decodedExpirationTime = dataStream.readLong();
205                         String decodedPrincipal = dataStream.readUTF();
206
207                         HMACHandleEntry macHandleEntry = createHMACHandleEntry(new AuthNPrincipal(decodedPrincipal));
208                         macHandleEntry.setExpirationTime(decodedExpirationTime);
209                         byte[] generatedMac = macHandleEntry.getMAC(mac);
210
211                         if (macHandleEntry.isExpired()) {
212                                 log.debug("Attribute Query Handle is expired.");
213                                 throw new InvalidNameIdentifierException("Attribute Query Handle is expired.", errorCodes);
214                         }
215
216                         if (!Arrays.equals(decodedMac, generatedMac)) {
217                                 log.warn("Attribute Query Handle failed integrity check.");
218                                 throw new NameIdentifierMappingException("Attribute Query Handle failed integrity check.");
219                         }
220
221                         log.debug("Attribute Query Handle recognized.");
222                         return macHandleEntry.principal;
223
224                 } catch (NoSuchAlgorithmException e) {
225                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Algorithm: " + e);
226                         throw new NameIdentifierMappingException(
227                                         "Appropriate JCE provider not found in the java environment.  Could not load Algorithm.");
228                 } catch (NoSuchPaddingException e) {
229                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Padding "
230                                         + "method: " + e);
231                         throw new NameIdentifierMappingException(
232                                         "Appropriate JCE provider not found in the java environment.  Could not load Padding method.");
233                 } catch (InvalidKeyException e) {
234                         log.error("Could not use the supplied secret key: " + e);
235                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
236                 } catch (GeneralSecurityException e) {
237                         log.warn("Unable to decrypt the supplied Attribute Query Handle: " + e);
238                         throw new NameIdentifierMappingException("Unable to decrypt the supplied Attribute Query Handle.");
239                 } catch (IOException e) {
240                         log.warn("IO error while decoding handle.");
241                         throw new NameIdentifierMappingException("IO error while decoding handle.");
242                 }
243         }
244
245         /**
246          * Encodes a principal into a cryptographic handle Format of encoded handle: [IV][HMAC][TTL][principal] where: [IV] =
247          * the Initialization Vector; byte-array [HMAC] = the HMAC; byte array [exprTime] = expiration time of the handle; 8
248          * bytes; Big-endian [principal] = the principal; a UTF-8-encoded string The [HMAC][exprTime][princLen][principal]
249          * byte stream is GZIPped. The IV is pre-pended to this byte stream, and the result is Base32-encoded. We don't need
250          * to encode the IV or MAC's lengths. They can be obtained from Cipher.getBlockSize() and Mac.getMacLength(),
251          * respectively.
252          */
253         public SAMLNameIdentifier getNameIdentifier(AuthNPrincipal principal, ServiceProvider sProv, IdentityProvider idProv)
254                         throws NameIdentifierMappingException {
255
256                 if (principal == null) {
257                         log.error("A principal must be supplied for Attribute Query Handle creation.");
258                         throw new IllegalArgumentException("A principal must be supplied for Attribute Query Handle creation.");
259                 }
260
261                 try {
262
263                         Mac mac = Mac.getInstance(macAlgorithm);
264                         mac.init(secret);
265                         HMACHandleEntry macHandleEntry = createHMACHandleEntry(principal);
266
267                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
268                         byte[] iv = new byte[cipher.getBlockSize()];
269                         random.nextBytes(iv);
270                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
271                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
272
273                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
274                         GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
275                         DataOutputStream dataStream = new DataOutputStream(compressedStream);
276
277                         dataStream.write(macHandleEntry.getMAC(mac));
278                         dataStream.writeLong(macHandleEntry.getExpirationTime());
279                         dataStream.writeUTF(principal.getName());
280
281                         dataStream.flush();
282                         compressedStream.flush();
283                         compressedStream.finish();
284                         byteStream.flush();
285
286                         byte[] encryptedData = cipher.doFinal(byteStream.toByteArray());
287
288                         byte[] handleBytes = new byte[iv.length + encryptedData.length];
289                         System.arraycopy(iv, 0, handleBytes, 0, iv.length);
290                         System.arraycopy(encryptedData, 0, handleBytes, iv.length, encryptedData.length);
291
292                         String handle = Base32.encode(handleBytes);
293
294                         try {
295                                 return new SAMLNameIdentifier(handle.replaceAll(System.getProperty("line.separator"), ""), idProv
296                                                 .getProviderId(), getNameIdentifierFormat().toString());
297                         } catch (SAMLException e) {
298                                 throw new NameIdentifierMappingException("Unable to generate Attribute Query Handle: " + e);
299                         }
300
301                 } catch (KeyException e) {
302                         log.error("Could not use the supplied secret key: " + e);
303                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
304                 } catch (GeneralSecurityException e) {
305                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Cipher: " + e);
306                         throw new NameIdentifierMappingException(
307                                         "Appropriate JCE provider not found in the java environment.  Could not load Cipher.");
308                 } catch (IOException e) {
309                         log.error("IO error while decoding handle.");
310                         throw new NameIdentifierMappingException("IO error while decoding handle.");
311                 }
312
313         }
314
315         private String getElementConfigData(Element e, String itemName, boolean required)
316                         throws NameIdentifierMappingException {
317
318                 NodeList itemElements = e.getElementsByTagNameNS(NameIdentifierMapping.mappingNamespace, itemName);
319
320                 if (itemElements.getLength() < 1) {
321                         if (required) {
322                                 log.error(itemName + " not specified.");
323                                 throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName
324                                                 + "> specification.");
325                         } else {
326                                 return null;
327                         }
328                 }
329
330                 if (itemElements.getLength() > 1) {
331                         log.error("Multiple " + itemName + " specifications, using first.");
332                 }
333
334                 Node tnode = itemElements.item(0).getFirstChild();
335                 String item = null;
336                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
337                         item = tnode.getNodeValue();
338                 }
339                 if (item == null || item.equals("")) {
340                         log.error(itemName + " not specified.");
341                         throw new NameIdentifierMappingException("Crypto Name Mapping requires a valid <" + itemName
342                                         + "> specification.");
343                 }
344                 return item;
345         }
346
347         private void testEncryption() throws NameIdentifierMappingException {
348
349                 String decrypted;
350                 try {
351                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
352                         byte[] iv = new byte[cipher.getBlockSize()];
353                         random.nextBytes(iv);
354                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
355                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
356                         byte[] cipherText = cipher.doFinal("test".getBytes());
357                         cipher = Cipher.getInstance(cipherAlgorithm);
358                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
359                         decrypted = new String(cipher.doFinal(cipherText));
360                 } catch (Exception e) {
361                         log.error("Round trip encryption/decryption test unsuccessful: " + e);
362                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
363                 }
364
365                 if (decrypted == null || !decrypted.equals("test")) {
366                         log.error("Round trip encryption/decryption test unsuccessful.  Decrypted text did not match.");
367                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
368                 }
369
370                 byte[] code;
371                 try {
372                         Mac mac = Mac.getInstance(macAlgorithm);
373                         mac.init(secret);
374                         mac.update("foo".getBytes());
375                         code = mac.doFinal();
376
377                 } catch (Exception e) {
378                         log.error("Message Authentication test unsuccessful: " + e);
379                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
380                 }
381
382                 if (code == null) {
383                         log.error("Message Authentication test unsuccessful.");
384                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
385                 }
386         }
387
388         private boolean usingDefaultSecret() {
389
390                 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
391                                 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
392                                 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
393                                 (byte) 0x61, (byte) 0xEF};
394                 byte[] encodedKey = secret.getEncoded();
395                 return Arrays.equals(defaultKey, encodedKey);
396         }
397
398         protected HMACHandleEntry createHMACHandleEntry(AuthNPrincipal principal) {
399
400                 return new HMACHandleEntry(principal, handleTTL);
401         }
402
403 }
404
405 /**
406  * <code>HandleEntry</code> extension class that performs message authentication.
407  */
408
409 class HMACHandleEntry extends HandleEntry {
410
411         protected HMACHandleEntry(AuthNPrincipal principal, long TTL) {
412
413                 super(principal, TTL);
414         }
415
416         private static byte[] getLongBytes(long longValue) {
417
418                 try {
419                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
420                         DataOutputStream dataStream = new DataOutputStream(byteStream);
421
422                         dataStream.writeLong(longValue);
423                         dataStream.flush();
424                         byteStream.flush();
425
426                         return byteStream.toByteArray();
427                 } catch (IOException ex) {
428                         return null;
429                 }
430         }
431
432         public byte[] getMAC(Mac mac) {
433
434                 mac.update(principal.getName().getBytes());
435                 mac.update(getLongBytes(expirationTime));
436
437                 return mac.doFinal();
438         }
439 }