9f3609f0e66b1c306a409260579934dbc3e87384
[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 HSNameIdentifierMapping}implementation that uses symmetric encryption to store principal data inside
71  * Shibboleth 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.debug("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 getNameIdentifierName(AuthNPrincipal principal, ServiceProvider sProv,
254                         IdentityProvider idProv) throws NameIdentifierMappingException {
255
256                 try {
257                         if (principal == null) {
258                                 log.error("A principal must be supplied for Attribute Query Handle creation.");
259                                 throw new IllegalArgumentException("A principal must be supplied for Attribute Query Handle creation.");
260                         }
261
262                         Mac mac = Mac.getInstance(macAlgorithm);
263                         mac.init(secret);
264                         HMACHandleEntry macHandleEntry = createHMACHandleEntry(principal);
265
266                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
267                         byte[] iv = new byte[cipher.getBlockSize()];
268                         random.nextBytes(iv);
269                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
270                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
271
272                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
273                         GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
274                         DataOutputStream dataStream = new DataOutputStream(compressedStream);
275
276                         dataStream.write(macHandleEntry.getMAC(mac));
277                         dataStream.writeLong(macHandleEntry.getExpirationTime());
278                         dataStream.writeUTF(principal.getName());
279
280                         dataStream.flush();
281                         compressedStream.flush();
282                         compressedStream.finish();
283                         byteStream.flush();
284
285                         byte[] encryptedData = cipher.doFinal(byteStream.toByteArray());
286
287                         byte[] handleBytes = new byte[iv.length + encryptedData.length];
288                         System.arraycopy(iv, 0, handleBytes, 0, iv.length);
289                         System.arraycopy(encryptedData, 0, handleBytes, iv.length, encryptedData.length);
290
291                         String handle = Base32.encode(handleBytes);
292
293                         try {
294                                 return new SAMLNameIdentifier(handle.replaceAll(System.getProperty("line.separator"), ""), idProv
295                                                 .getProviderId(), getNameIdentifierFormat().toString());
296                         } catch (SAMLException e) {
297                                 throw new NameIdentifierMappingException("Unable to generate Attribute Query Handle: " + e);
298                         }
299
300                 } catch (KeyException e) {
301                         log.error("Could not use the supplied secret key: " + e);
302                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
303                 } catch (GeneralSecurityException e) {
304                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Cipher: " + e);
305                         throw new NameIdentifierMappingException(
306                                         "Appropriate JCE provider not found in the java environment.  Could not load Cipher.");
307                 } catch (IOException e) {
308                         log.warn("IO error while decoding handle.");
309                         throw new NameIdentifierMappingException("IO error while decoding handle.");
310                 }
311
312         }
313
314         private String getElementConfigData(Element e, String itemName, boolean required)
315                         throws NameIdentifierMappingException {
316
317                 NodeList itemElements = e.getElementsByTagNameNS(NameIdentifierMapping.mappingNamespace, itemName);
318
319                 if (itemElements.getLength() < 1) {
320                         if (required) {
321                                 log.error(itemName + " not specified.");
322                                 throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName
323                                                 + "> specification.");
324                         } else {
325                                 return null;
326                         }
327                 }
328
329                 if (itemElements.getLength() > 1) {
330                         log.error("Multiple " + itemName + " specifications, using first.");
331                 }
332
333                 Node tnode = itemElements.item(0).getFirstChild();
334                 String item = null;
335                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
336                         item = tnode.getNodeValue();
337                 }
338                 if (item == null || item.equals("")) {
339                         log.error(itemName + " not specified.");
340                         throw new NameIdentifierMappingException("Crypto Name Mapping requires a valid <" + itemName
341                                         + "> specification.");
342                 }
343                 return item;
344         }
345
346         private void testEncryption() throws NameIdentifierMappingException {
347
348                 String decrypted;
349                 try {
350                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
351                         byte[] iv = new byte[cipher.getBlockSize()];
352                         random.nextBytes(iv);
353                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
354                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
355                         byte[] cipherText = cipher.doFinal("test".getBytes());
356                         cipher = Cipher.getInstance(cipherAlgorithm);
357                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
358                         decrypted = new String(cipher.doFinal(cipherText));
359                 } catch (Exception e) {
360                         log.error("Round trip encryption/decryption test unsuccessful: " + e);
361                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
362                 }
363
364                 if (decrypted == null || !decrypted.equals("test")) {
365                         log.error("Round trip encryption/decryption test unsuccessful.  Decrypted text did not match.");
366                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
367                 }
368
369                 byte[] code;
370                 try {
371                         Mac mac = Mac.getInstance(macAlgorithm);
372                         mac.init(secret);
373                         mac.update("foo".getBytes());
374                         code = mac.doFinal();
375
376                 } catch (Exception e) {
377                         log.error("Message Authentication test unsuccessful: " + e);
378                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
379                 }
380
381                 if (code == null) {
382                         log.error("Message Authentication test unsuccessful.");
383                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
384                 }
385         }
386
387         private boolean usingDefaultSecret() {
388
389                 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
390                                 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
391                                 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
392                                 (byte) 0x61, (byte) 0xEF};
393                 byte[] encodedKey = secret.getEncoded();
394                 return Arrays.equals(defaultKey, encodedKey);
395         }
396
397         protected HMACHandleEntry createHMACHandleEntry(AuthNPrincipal principal) {
398
399                 return new HMACHandleEntry(principal, handleTTL);
400         }
401
402 }
403
404 /**
405  * <code>HandleEntry</code> extension class that performs message authentication.
406  */
407
408 class HMACHandleEntry extends HandleEntry {
409
410         protected HMACHandleEntry(AuthNPrincipal principal, long TTL) {
411
412                 super(principal, TTL);
413         }
414
415         private static byte[] getLongBytes(long longValue) {
416
417                 try {
418                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
419                         DataOutputStream dataStream = new DataOutputStream(byteStream);
420
421                         dataStream.writeLong(longValue);
422                         dataStream.flush();
423                         byteStream.flush();
424
425                         return byteStream.toByteArray();
426                 } catch (IOException ex) {
427                         return null;
428                 }
429         }
430
431         public byte[] getMAC(Mac mac) {
432
433                 mac.update(principal.getName().getBytes());
434                 mac.update(getLongBytes(expirationTime));
435
436                 return mac.doFinal();
437         }
438
439         public long getExpirationTime() {
440
441                 return expirationTime;
442         }
443
444         public void setExpirationTime(long expr) {
445
446                 expirationTime = expr;
447         }
448 }