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