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