See Bugzilla #182.
[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.hs.HSNameIdentifierMapping;
68 import edu.internet2.middleware.shibboleth.utils.Base32;
69
70 /**
71  * {@link HSNameIdentifierMapping}implementation that uses symmetric encryption to store principal data inside
72  * Shibboleth Attribute Query Handles.
73  * 
74  * @author Walter Hoehn
75  * @author Derek Morr
76  */
77 public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSNameIdentifierMapping {
78
79         private static Logger log = Logger.getLogger(CryptoShibHandle.class.getName());
80         protected SecretKey secret;
81         private SecureRandom random = new SecureRandom();
82         private String cipherAlgorithm = "DESede/CBC/PKCS5Padding";
83         private String macAlgorithm = "HmacSHA1";
84         private String storeType = "JCEKS";
85
86         public CryptoShibHandle(Element config) throws NameIdentifierMappingException {
87
88                 super(config);
89                 try {
90
91                         String keyStorePath = getElementConfigData(config, "KeyStorePath", true);
92                         String keyStorePassword = getElementConfigData(config, "KeyStorePassword", true);
93                         String keyStoreKeyAlias = getElementConfigData(config, "KeyStoreKeyAlias", true);
94                         String keyStoreKeyPassword = getElementConfigData(config, "KeyStoreKeyPassword", true);
95
96                         String rawStoreType = getElementConfigData(config, "KeyStoreType", false);
97                         if (rawStoreType != null && !rawStoreType.equals("")) {
98                                 storeType = rawStoreType;
99                         }
100                         String rawCipherAlgorithm = getElementConfigData(config, "Cipher", false);
101                         if (rawCipherAlgorithm != null && !rawCipherAlgorithm.equals("")) {
102                                 cipherAlgorithm = rawCipherAlgorithm;
103                         }
104                         String rawMacAlgorithm = getElementConfigData(config, "MAC", false);
105                         if (rawMacAlgorithm != null && !rawMacAlgorithm.equals("")) {
106                                 macAlgorithm = rawMacAlgorithm;
107                         }
108
109                         KeyStore keyStore = KeyStore.getInstance(storeType);
110                         keyStore.load(new ShibResource(keyStorePath, this.getClass()).getInputStream(), keyStorePassword
111                                         .toCharArray());
112                         secret = (SecretKey) keyStore.getKey(keyStoreKeyAlias, keyStoreKeyPassword.toCharArray());
113
114                         // Before we finish initilization, make sure that things are working
115                         testEncryption();
116
117                         if (usingDefaultSecret()) {
118                                 log.warn("You are running Crypto AQH Name Mapping with the "
119                                                 + "default secret key.  This is UNSAFE!  Please change "
120                                                 + "this configuration and restart the origin.");
121                         }
122                 } catch (StreamCorruptedException e) {
123                         if (System.getProperty("java.version").startsWith("1.4.2")) {
124                                 log.error("There is a bug in some versions of Java 1.4.2.x that "
125                                                 + "prevent JCEKS keystores from being loaded properly.  "
126                                                 + "You probably need to upgrade or downgrade your JVM in order to make this work.");
127                         }
128                         log.error("An error occurred while loading the java keystore.  Unable to initialize "
129                                         + "Crypto Name Mapping: " + e);
130                         throw new NameIdentifierMappingException(
131                                         "An error occurred while loading the java keystore.  Unable to initialize Crypto "
132                                                         + "Name Mapping.");
133                 } catch (KeyStoreException e) {
134                         log.error("An error occurred while loading the java keystore.  Unable to initialize Crypto "
135                                         + "Name Mapping: " + e);
136                         throw new NameIdentifierMappingException(
137                                         "An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping.");
138                 } catch (CertificateException e) {
139                         log.error("The java keystore contained corrupted data.  Unable to initialize Crypto Name Mapping: " + e);
140                         throw new NameIdentifierMappingException(
141                                         "The java keystore contained corrupted data.  Unable to initialize Crypto Name Mapping.");
142                 } catch (NoSuchAlgorithmException e) {
143                         log.error("Appropriate JCE provider not found in the java environment. Unable "
144                                         + "to initialize Crypto Name Mapping: " + e);
145                         throw new NameIdentifierMappingException(
146                                         "Appropriate JCE provider not found in the java environment. Unable to initialize Crypto Name Mapping.");
147                 } catch (IOException e) {
148                         log.error("An error accessing while loading the java keystore.  Unable to initialize Crypto Name "
149                                         + "Mapping: " + e);
150                         throw new NameIdentifierMappingException(
151                                         "An error occurred while accessing the java keystore.  Unable to initialize Crypto Name Mapping.");
152                 } catch (UnrecoverableKeyException e) {
153                         log.error("Secret could not be loaded from the java keystore.  Verify that the alias and "
154                                         + "password are correct: " + e);
155                         throw new NameIdentifierMappingException(
156                                         "Secret could not be loaded from the java keystore.  Verify that the alias and password are correct. ");
157                 }
158         }
159
160         /**
161          * Decode an encrypted handle back into a principal
162          */
163         public AuthNPrincipal getPrincipal(SAMLNameIdentifier nameId, ServiceProvider sProv, IdentityProvider idProv)
164                         throws NameIdentifierMappingException, InvalidNameIdentifierException {
165
166                 verifyQualifier(nameId, idProv);
167
168                 try {
169                         byte[] in = Base32.decode(nameId.getName());
170
171                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
172                         int ivSize = cipher.getBlockSize();
173                         byte[] iv = new byte[ivSize];
174
175                         Mac mac = Mac.getInstance(macAlgorithm);
176                         mac.init(secret);
177                         int macSize = mac.getMacLength();
178
179                         if (in.length < ivSize) {
180                                 log.debug("Attribute Query Handle is malformed (not enough bytes).");
181                                 throw new NameIdentifierMappingException("Attribute Query Handle is malformed (not enough bytes).");
182                         }
183
184                         // extract the IV, setup the cipher and extract the encrypted handle
185                         System.arraycopy(in, 0, iv, 0, ivSize);
186                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
187                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
188
189                         byte[] encryptedHandle = new byte[in.length - iv.length];
190                         System.arraycopy(in, ivSize, encryptedHandle, 0, in.length - iv.length);
191
192                         // decrypt the rest of the data and setup the streams
193                         byte[] decryptedBytes = cipher.doFinal(encryptedHandle);
194                         ByteArrayInputStream byteStream = new ByteArrayInputStream(decryptedBytes);
195                         GZIPInputStream compressedData = new GZIPInputStream(byteStream);
196                         DataInputStream dataStream = new DataInputStream(compressedData);
197
198                         // extract the components
199                         byte[] decodedMac = new byte[macSize];
200                         int bytesRead = dataStream.read(decodedMac);
201                         if (bytesRead != macSize) {
202                                 log.error("Error parsing handle: Unable to extract HMAC.");
203                                 throw new NameIdentifierMappingException("Error parsing handle: Unable to extract HMAC.");
204                         }
205                         long decodedExpirationTime = dataStream.readLong();
206                         String decodedPrincipal = dataStream.readUTF();
207
208                         HMACHandleEntry macHandleEntry = new HMACHandleEntry(
209                                         createHandleEntry(new AuthNPrincipal(decodedPrincipal)));
210                         macHandleEntry.setExpirationTime(decodedExpirationTime);
211                         byte[] generatedMac = macHandleEntry.getMAC(mac);
212
213                         if (macHandleEntry.isExpired()) {
214                                 log.debug("Attribute Query Handle is expired.");
215                                 throw new InvalidNameIdentifierException("Attribute Query Handle is expired.", errorCodes);
216                         }
217
218                         if (!Arrays.equals(decodedMac, generatedMac)) {
219                                 log.warn("Attribute Query Handle failed integrity check.");
220                                 throw new NameIdentifierMappingException("Attribute Query Handle failed integrity check.");
221                         }
222
223                         log.debug("Attribute Query Handle recognized.");
224                         return macHandleEntry.principal;
225
226                 } catch (NoSuchAlgorithmException e) {
227                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Algorithm: " + e);
228                         throw new NameIdentifierMappingException(
229                                         "Appropriate JCE provider not found in the java environment.  Could not load Algorithm.");
230                 } catch (NoSuchPaddingException e) {
231                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Padding "
232                                         + "method: " + e);
233                         throw new NameIdentifierMappingException(
234                                         "Appropriate JCE provider not found in the java environment.  Could not load Padding method.");
235                 } catch (InvalidKeyException e) {
236                         log.error("Could not use the supplied secret key: " + e);
237                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
238                 } catch (GeneralSecurityException e) {
239                         log.warn("Unable to decrypt the supplied Attribute Query Handle: " + e);
240                         throw new NameIdentifierMappingException("Unable to decrypt the supplied Attribute Query Handle.");
241                 } catch (IOException e) {
242                         log.warn("IO error while decoding handle.");
243                         throw new NameIdentifierMappingException("IO error while decoding handle.");
244                 }
245         }
246
247         /**
248          * Encodes a principal into a cryptographic handle Format of encoded handle: [IV][HMAC][TTL][principal] where: [IV] =
249          * the Initialization Vector; byte-array [HMAC] = the HMAC; byte array [exprTime] = expiration time of the handle; 8
250          * bytes; Big-endian [principal] = the principal; a UTF-8-encoded string The [HMAC][exprTime][princLen][principal]
251          * byte stream is GZIPped. The IV is pre-pended to this byte stream, and the result is Base32-encoded. We don't need
252          * to encode the IV or MAC's lengths. They can be obtained from Cipher.getBlockSize() and Mac.getMacLength(),
253          * respectively.
254          */
255         public SAMLNameIdentifier getNameIdentifierName(AuthNPrincipal principal, ServiceProvider sProv,
256                         IdentityProvider idProv) throws NameIdentifierMappingException {
257
258                 try {
259                         if (principal == null) {
260                                 log.error("A principal must be supplied for Attribute Query Handle creation.");
261                                 throw new IllegalArgumentException("A principal must be supplied for Attribute Query Handle creation.");
262                         }
263
264                         Mac mac = Mac.getInstance(macAlgorithm);
265                         mac.init(secret);
266                         HandleEntry handleEntry = createHandleEntry(principal);
267                         HMACHandleEntry macHandleEntry = new HMACHandleEntry(handleEntry);
268
269                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
270                         byte[] iv = new byte[cipher.getBlockSize()];
271                         random.nextBytes(iv);
272                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
273                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
274
275                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
276                         GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
277                         DataOutputStream dataStream = new DataOutputStream(compressedStream);
278
279                         dataStream.write(macHandleEntry.getMAC(mac));
280                         dataStream.writeLong(macHandleEntry.getExpirationTime());
281                         dataStream.writeUTF(principal.getName());
282
283                         dataStream.flush();
284                         compressedStream.flush();
285                         compressedStream.finish();
286                         byteStream.flush();
287
288                         byte[] encryptedData = cipher.doFinal(byteStream.toByteArray());
289
290                         byte[] handleBytes = new byte[iv.length + encryptedData.length];
291                         System.arraycopy(iv, 0, handleBytes, 0, iv.length);
292                         System.arraycopy(encryptedData, 0, handleBytes, iv.length, encryptedData.length);
293
294                         String handle = Base32.encode(handleBytes);
295
296                         try {
297                                 return new SAMLNameIdentifier(handle.replaceAll(System.getProperty("line.separator"), ""), idProv
298                                                 .getProviderId(), getNameIdentifierFormat().toString());
299                         } catch (SAMLException e) {
300                                 throw new NameIdentifierMappingException("Unable to generate Attribute Query Handle: " + e);
301                         }
302
303                 } catch (KeyException e) {
304                         log.error("Could not use the supplied secret key: " + e);
305                         throw new NameIdentifierMappingException("Could not use the supplied secret key.");
306                 } catch (GeneralSecurityException e) {
307                         log.error("Appropriate JCE provider not found in the java environment.  Could not load Cipher: " + e);
308                         throw new NameIdentifierMappingException(
309                                         "Appropriate JCE provider not found in the java environment.  Could not load Cipher.");
310                 } catch (IOException e) {
311                         log.warn("IO error while decoding handle.");
312                         throw new NameIdentifierMappingException("IO error while decoding handle.");
313                 }
314
315         }
316
317         private String getElementConfigData(Element e, String itemName, boolean required)
318                         throws NameIdentifierMappingException {
319
320                 NodeList itemElements = e.getElementsByTagNameNS(NameIdentifierMapping.mappingNamespace, itemName);
321
322                 if (itemElements.getLength() < 1) {
323                         if (required) {
324                                 log.error(itemName + " not specified.");
325                                 throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName
326                                                 + "> specification.");
327                         } else {
328                                 return null;
329                         }
330                 }
331
332                 if (itemElements.getLength() > 1) {
333                         log.error("Multiple " + itemName + " specifications, using first.");
334                 }
335
336                 Node tnode = itemElements.item(0).getFirstChild();
337                 String item = null;
338                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
339                         item = tnode.getNodeValue();
340                 }
341                 if (item == null || item.equals("")) {
342                         log.error(itemName + " not specified.");
343                         throw new NameIdentifierMappingException("Crypto Name Mapping requires a valid <" + itemName
344                                         + "> specification.");
345                 }
346                 return item;
347         }
348
349         private void testEncryption() throws NameIdentifierMappingException {
350
351                 String decrypted;
352                 try {
353                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
354                         byte[] iv = new byte[cipher.getBlockSize()];
355                         random.nextBytes(iv);
356                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
357                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
358                         byte[] cipherText = cipher.doFinal("test".getBytes());
359                         cipher = Cipher.getInstance(cipherAlgorithm);
360                         cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
361                         decrypted = new String(cipher.doFinal(cipherText));
362                 } catch (Exception e) {
363                         log.error("Round trip encryption/decryption test unsuccessful: " + e);
364                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
365                 }
366
367                 if (decrypted == null || !decrypted.equals("test")) {
368                         log.error("Round trip encryption/decryption test unsuccessful.  Decrypted text did not match.");
369                         throw new NameIdentifierMappingException("Round trip encryption/decryption test unsuccessful.");
370                 }
371
372                 byte[] code;
373                 try {
374                         Mac mac = Mac.getInstance(macAlgorithm);
375                         mac.init(secret);
376                         mac.update("foo".getBytes());
377                         code = mac.doFinal();
378
379                 } catch (Exception e) {
380                         log.error("Message Authentication test unsuccessful: " + e);
381                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
382                 }
383
384                 if (code == null) {
385                         log.error("Message Authentication test unsuccessful.");
386                         throw new NameIdentifierMappingException("Message Authentication test unsuccessful.");
387                 }
388         }
389
390         private boolean usingDefaultSecret() {
391
392                 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
393                                 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
394                                 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
395                                 (byte) 0x61, (byte) 0xEF};
396                 byte[] encodedKey = secret.getEncoded();
397                 return Arrays.equals(defaultKey, encodedKey);
398         }
399
400 }
401
402 /**
403  * <code>HandleEntry</code> extension class that performs message authentication.
404  */
405
406 class HMACHandleEntry extends HandleEntry {
407
408         protected HMACHandleEntry(AuthNPrincipal principal, long TTL) {
409
410                 super(principal, TTL);
411         }
412
413         protected HMACHandleEntry(HandleEntry handleEntry) {
414
415                 super(handleEntry.principal, handleEntry.expirationTime);
416         }
417
418         private static byte[] getLongBytes(long longValue) {
419
420                 try {
421                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
422                         DataOutputStream dataStream = new DataOutputStream(byteStream);
423
424                         dataStream.writeLong(longValue);
425                         dataStream.flush();
426                         byteStream.flush();
427
428                         return byteStream.toByteArray();
429                 } catch (IOException ex) {
430                         return null;
431                 }
432         }
433
434         public byte[] getMAC(Mac mac) {
435
436                 mac.update(principal.getName().getBytes());
437                 mac.update(getLongBytes(expirationTime));
438
439                 return mac.doFinal();
440         }
441
442         public long getExpirationTime() {
443
444                 return expirationTime;
445         }
446
447         public void setExpirationTime(long expr) {
448
449                 expirationTime = expr;
450         }
451 }