Unifed NameMapper and HSNameMapper interfaces for use by the new IdPResponder servlet.
[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 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.hs.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 = new HMACHandleEntry(
208                                         createHandleEntry(new AuthNPrincipal(decodedPrincipal)));
209                         macHandleEntry.setExpirationTime(decodedExpirationTime);
210                         byte[] generatedMac = macHandleEntry.getMAC(mac);
211
212                         if (macHandleEntry.isExpired()) {
213                                 log.debug("Attribute Query Handle is expired.");
214                                 throw new InvalidNameIdentifierException("Attribute Query Handle is expired.", errorCodes);
215                         }
216
217                         if (!Arrays.equals(decodedMac, generatedMac)) {
218                                 log.warn("Attribute Query Handle failed integrity check.");
219                                 throw new NameIdentifierMappingException("Attribute Query Handle failed integrity check.");
220                         }
221
222                         log.debug("Attribute Query Handle recognized.");
223                         return macHandleEntry.principal;
224
225                 } catch (NoSuchAlgorithmException e) {
226                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Algorithm: " + e);
227                         throw new NameIdentifierMappingException(
228                                         "Appropriate JCE provider not found in the java environment.  Could not load Algorithm.");
229                 } catch (NoSuchPaddingException e) {
230                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Padding "
231                                         + "method: " + e);
232                         throw new NameIdentifierMappingException(
233                                         "Appropriate JCE provider not found in the java environment.  Could not load Padding method.");
234                 } catch (InvalidKeyException e) {
235                         log.error("Could not use the supplied secret key: " + e);
236                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
237                 } catch (GeneralSecurityException e) {
238                         log.warn("Unable to decrypt the supplied Attribute Query Handle: " + e);
239                         throw new NameIdentifierMappingException("Unable to decrypt the supplied Attribute Query Handle.");
240                 } catch (IOException e) {
241                         log.warn("IO error while decoding handle.");
242                         throw new NameIdentifierMappingException("IO error while decoding handle.");
243                 }
244         }
245
246         /**
247          * Encodes a principal into a cryptographic handle Format of encoded handle: [IV][HMAC][TTL][principal] where: [IV] =
248          * the Initialization Vector; byte-array [HMAC] = the HMAC; byte array [exprTime] = expiration time of the handle; 8
249          * bytes; Big-endian [principal] = the principal; a UTF-8-encoded string The [HMAC][exprTime][princLen][principal]
250          * byte stream is GZIPped. The IV is pre-pended to this byte stream, and the result is Base32-encoded. We don't need
251          * to encode the IV or MAC's lengths. They can be obtained from Cipher.getBlockSize() and Mac.getMacLength(),
252          * respectively.
253          */
254         public SAMLNameIdentifier getNameIdentifierName(AuthNPrincipal principal, ServiceProvider sProv,
255                         IdentityProvider idProv) throws NameIdentifierMappingException {
256
257                 try {
258                         if (principal == null) {
259                                 log.error("A principal must be supplied for Attribute Query Handle creation.");
260                                 throw new IllegalArgumentException("A principal must be supplied for Attribute Query Handle creation.");
261                         }
262
263                         Mac mac = Mac.getInstance(macAlgorithm);
264                         mac.init(secret);
265                         HandleEntry handleEntry = createHandleEntry(principal);
266                         HMACHandleEntry macHandleEntry = new HMACHandleEntry(handleEntry);
267
268                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
269                         byte[] iv = new byte[cipher.getBlockSize()];
270                         random.nextBytes(iv);
271                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
272                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
273
274                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
275                         GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
276                         DataOutputStream dataStream = new DataOutputStream(compressedStream);
277
278                         dataStream.write(macHandleEntry.getMAC(mac));
279                         dataStream.writeLong(macHandleEntry.getExpirationTime());
280                         dataStream.writeUTF(principal.getName());
281
282                         dataStream.flush();
283                         compressedStream.flush();
284                         compressedStream.finish();
285                         byteStream.flush();
286
287                         byte[] encryptedData = cipher.doFinal(byteStream.toByteArray());
288
289                         byte[] handleBytes = new byte[iv.length + encryptedData.length];
290                         System.arraycopy(iv, 0, handleBytes, 0, iv.length);
291                         System.arraycopy(encryptedData, 0, handleBytes, iv.length, encryptedData.length);
292
293                         String handle = Base32.encode(handleBytes);
294
295                         try {
296                                 return new SAMLNameIdentifier(handle.replaceAll(System.getProperty("line.separator"), ""), idProv
297                                                 .getProviderId(), getNameIdentifierFormat().toString());
298                         } catch (SAMLException e) {
299                                 throw new NameIdentifierMappingException("Unable to generate Attribute Query Handle: " + e);
300                         }
301
302                 } catch (KeyException e) {
303                         log.error("Could not use the supplied secret key: " + e);
304                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
305                 } catch (GeneralSecurityException e) {
306                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Cipher: " + e);
307                         throw new NameIdentifierMappingException(
308                                         "Appropriate JCE provider not found in the java environment.  Could not load Cipher.");
309                 } catch (IOException e) {
310                         log.warn("IO error while decoding handle.");
311                         throw new NameIdentifierMappingException("IO error while decoding handle.");
312                 }
313
314         }
315
316         private String getElementConfigData(Element e, String itemName, boolean required)
317                         throws NameIdentifierMappingException {
318
319                 NodeList itemElements = e.getElementsByTagNameNS(NameIdentifierMapping.mappingNamespace, itemName);
320
321                 if (itemElements.getLength() < 1) {
322                         if (required) {
323                                 log.error(itemName + " not specified.");
324                                 throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName
325                                                 + "> specification.");
326                         } else {
327                                 return null;
328                         }
329                 }
330
331                 if (itemElements.getLength() > 1) {
332                         log.error("Multiple " + itemName + " specifications, using first.");
333                 }
334
335                 Node tnode = itemElements.item(0).getFirstChild();
336                 String item = null;
337                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
338                         item = tnode.getNodeValue();
339                 }
340                 if (item == null || item.equals("")) {
341                         log.error(itemName + " not specified.");
342                         throw new NameIdentifierMappingException("Crypto Name Mapping requires a valid <" + itemName
343                                         + "> specification.");
344                 }
345                 return item;
346         }
347
348         private void testEncryption() throws NameIdentifierMappingException {
349
350                 String decrypted;
351                 try {
352                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
353                         byte[] iv = new byte[cipher.getBlockSize()];
354                         random.nextBytes(iv);
355                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
356                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
357                         byte[] cipherText = cipher.doFinal("test".getBytes());
358                         cipher = Cipher.getInstance(cipherAlgorithm);
359                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
360                         decrypted = new String(cipher.doFinal(cipherText));
361                 } catch (Exception e) {
362                         log.error("Round trip encryption/decryption test unsuccessful: " + e);
363                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
364                 }
365
366                 if (decrypted == null || !decrypted.equals("test")) {
367                         log.error("Round trip encryption/decryption test unsuccessful.  Decrypted text did not match.");
368                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
369                 }
370
371                 byte[] code;
372                 try {
373                         Mac mac = Mac.getInstance(macAlgorithm);
374                         mac.init(secret);
375                         mac.update("foo".getBytes());
376                         code = mac.doFinal();
377
378                 } catch (Exception e) {
379                         log.error("Message Authentication test unsuccessful: " + e);
380                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
381                 }
382
383                 if (code == null) {
384                         log.error("Message Authentication test unsuccessful.");
385                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
386                 }
387         }
388
389         private boolean usingDefaultSecret() {
390
391                 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
392                                 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
393                                 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
394                                 (byte) 0x61, (byte) 0xEF};
395                 byte[] encodedKey = secret.getEncoded();
396                 return Arrays.equals(defaultKey, encodedKey);
397         }
398
399 }
400
401 /**
402  * <code>HandleEntry</code> extension class that performs message authentication.
403  */
404
405 class HMACHandleEntry extends HandleEntry {
406
407         protected HMACHandleEntry(AuthNPrincipal principal, long TTL) {
408
409                 super(principal, TTL);
410         }
411
412         protected HMACHandleEntry(HandleEntry handleEntry) {
413
414                 super(handleEntry.principal, handleEntry.expirationTime);
415         }
416
417         private static byte[] getLongBytes(long longValue) {
418
419                 try {
420                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
421                         DataOutputStream dataStream = new DataOutputStream(byteStream);
422
423                         dataStream.writeLong(longValue);
424                         dataStream.flush();
425                         byteStream.flush();
426
427                         return byteStream.toByteArray();
428                 } catch (IOException ex) {
429                         return null;
430                 }
431         }
432
433         public byte[] getMAC(Mac mac) {
434
435                 mac.update(principal.getName().getBytes());
436                 mac.update(getLongBytes(expirationTime));
437
438                 return mac.doFinal();
439         }
440
441         public long getExpirationTime() {
442
443                 return expirationTime;
444         }
445
446         public void setExpirationTime(long expr) {
447
448                 expirationTime = expr;
449         }
450 }