View Javadoc

1   /*
2    * Licensed to the University Corporation for Advanced Internet Development, 
3    * Inc. (UCAID) under one or more contributor license agreements.  See the 
4    * NOTICE file distributed with this work for additional information regarding
5    * copyright ownership. The UCAID licenses this file to You under the Apache 
6    * License, Version 2.0 (the "License"); you may not use this file except in 
7    * compliance with the License.  You may obtain a copy of the License at
8    *
9    *    http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package edu.internet2.middleware.shibboleth.common.util;
19  
20  import java.io.ByteArrayInputStream;
21  import java.io.ByteArrayOutputStream;
22  import java.io.DataInputStream;
23  import java.io.DataOutputStream;
24  import java.io.FileInputStream;
25  import java.io.IOException;
26  import java.security.GeneralSecurityException;
27  import java.security.Key;
28  import java.security.KeyException;
29  import java.security.KeyStore;
30  import java.security.SecureRandom;
31  import java.util.Arrays;
32  import java.util.zip.GZIPInputStream;
33  import java.util.zip.GZIPOutputStream;
34  
35  import javax.crypto.Cipher;
36  import javax.crypto.Mac;
37  import javax.crypto.SecretKey;
38  import javax.crypto.spec.IvParameterSpec;
39  
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * Applies a MAC to time-limited information and encrypts with a symmetric key.
45   * 
46   * @author Scott Cantor
47   * @author Walter Hoehn
48   * @author Derek Morr
49   */
50  public class DataSealer {
51  
52      /** Class logger. */
53      private static Logger log = LoggerFactory.getLogger(DataSealer.class.getName());
54  
55      /** Key used for encryption. */
56      private SecretKey cipherKey;
57  
58      /** Key used for MAC. */
59      private SecretKey macKey;
60  
61      /** Source of secure random data. */
62      private SecureRandom random;
63  
64      /** Tye of keystore to use for access to keys. */
65      private String keystoreType = "JCEKS";
66  
67      /** Path to keystore. */
68      private String keystorePath;
69  
70      /** Password for keystore. */
71      private String keystorePassword;
72  
73      /** Keystore alias for the encryption key. */
74      private String cipherKeyAlias;
75  
76      /** Password for encryption key. */
77      private String cipherKeyPassword;
78  
79      /** Encryption algorithm to use. */
80      private String cipherAlgorithm = "AES/CBC/PKCS5Padding";
81  
82      /** Keystore alias for the MAC key. */
83      private String macKeyAlias;
84  
85      /** Password for MAC key. */
86      private String macKeyPassword;
87  
88      /** MAC algorithm to use. */
89      private String macAlgorithm = "HmacSHA256";
90  
91      /**
92       * Initialization method used after setting all relevant bean properties.
93       * @throws DataSealerException if initialization fails
94       */
95      public void init() throws DataSealerException {
96          try {
97              if (cipherKey == null) {
98                  if (keystoreType == null || keystorePath == null || keystorePassword == null || cipherKeyAlias == null
99                          || cipherKeyPassword == null) {
100                     throw new IllegalArgumentException("Missing a required configuration property.");
101                 }
102             }
103 
104             if (random == null) {
105                 random = new SecureRandom();
106             }
107 
108             loadKeys();
109 
110             // Before we finish initialization, make sure that things are working.
111             testEncryption();
112 
113         } catch (GeneralSecurityException e) {
114             log.error(e.getMessage());
115             throw new DataSealerException("Caught NoSuchAlgorithmException loading the java keystore.", e);
116         } catch (IOException e) {
117             log.error(e.getMessage());
118             throw new DataSealerException("Caught IOException loading the java keystore.", e);
119         }
120     }
121 
122     /**
123      * Returns the encryption key.
124      * @return the encryption key
125      */
126     public SecretKey getCipherKey() {
127         return cipherKey;
128     }
129 
130     /**
131      * Returns the MAC key, if different from the encryption key.
132      * @return the MAC key
133      */
134     public SecretKey getMacKey() {
135         return macKey;
136     }
137 
138     /**
139      * Returns the pseudorandom generator.
140      * @return the pseudorandom generator
141      */
142     public SecureRandom getRandom() {
143         return random;
144     }
145 
146     /**
147      * Returns the keystore type.
148      * @return the keystore type.
149      */
150     public String getKeystoreType() {
151         return keystoreType;
152     }
153 
154     /**
155      * Returns the keystore path.
156      * @return the keystore path
157      */
158     public String getKeystorePath() {
159         return keystorePath;
160     }
161 
162     /**
163      * Returns the keystore password.
164      * @return the keystore password
165      */
166     public String getKeystorePassword() {
167         return keystorePassword;
168     }
169 
170     /**
171      * Returns the encryption key alias.
172      * @return the encryption key alias
173      */
174     public String getCipherKeyAlias() {
175         return cipherKeyAlias;
176     }
177 
178     /**
179      * Returns the encryption key password.
180      * @return the encryption key password
181      */
182     public String getCipherKeyPassword() {
183         return cipherKeyPassword;
184     }
185 
186     /**
187      * Returns the encryption algorithm.
188      * @return the encryption algorithm
189      */
190     public String getCipherAlgorithm() {
191         return cipherAlgorithm;
192     }
193 
194     /**
195      * Returns the MAC key alias.
196      * @return the MAC key alias
197      */
198     public String getMacKeyAlias() {
199         return macKeyAlias;
200     }
201 
202     /**
203      * Returns the MAC key password.
204      * @return the MAC key password
205      */
206     public String getMacKeyPassword() {
207         return macKeyPassword;
208     }
209 
210     /**
211      * Returns the MAC algorithm.
212      * @return the MAC algorithm
213      */
214     public String getMacAlgorithm() {
215         return macAlgorithm;
216     }
217 
218     /**
219      * Sets the encryption key.
220      * @param key the encryption key to set
221      */
222     public void setCipherKey(SecretKey key) {
223         cipherKey = key;
224     }
225 
226     /**
227      * Sets the MAC key.
228      * @param key the MAC key to set
229      */
230     public void setMacKey(SecretKey key) {
231         macKey = key;
232     }
233 
234     /**
235      * Sets the pseudorandom generator.
236      * @param r the pseudorandom generator to set
237      */
238     public void setRandom(SecureRandom r) {
239         random = r;
240     }
241 
242     /**
243      * Sets the keystore type.
244      * @param type the keystore type to set
245      */
246     public void setKeystoreType(String type) {
247         keystoreType = type;
248     }
249 
250     /**
251      * Sets the keystore path.
252      * @param path the keystore path to set
253      */
254     public void setKeystorePath(String path) {
255         keystorePath = path;
256     }
257 
258     /**
259      * Sets the keystore password.
260      * @param password the keystore password to set
261      */
262     public void setKeystorePassword(String password) {
263         keystorePassword = password;
264     }
265 
266     /**
267      * Sets the encryption key alias.
268      * @param alias the encryption key alias to set
269      */
270     public void setCipherKeyAlias(String alias) {
271         cipherKeyAlias = alias;
272     }
273 
274     /**
275      * Sets the encryption key password.
276      * @param password the encryption key password to set
277      */
278     public void setCipherKeyPassword(String password) {
279         cipherKeyPassword = password;
280     }
281 
282     /**
283      * Sets the encryption algorithm.
284      * @param alg the encryption algorithm to set
285      */
286     public void setCipherAlgorithm(String alg) {
287         cipherAlgorithm = alg;
288     }
289 
290     /**
291      * Sets the MAC key alias.
292      * @param alias the MAC key alias to set
293      */
294     public void setMacKeyAlias(String alias) {
295         macKeyAlias = alias;
296     }
297 
298     /**
299      * Sets the MAC key password.
300      * @param password the the MAC key password to set
301      */
302     public void setMacKeyPassword(String password) {
303         macKeyPassword = password;
304     }
305 
306     /**
307      * Sets the MAC key algorithm.
308      * @param alg the MAC algorithm to set
309      */
310     public void setMacAlgorithm(String alg) {
311         macAlgorithm = alg;
312     }
313 
314     /**
315      * Decrypts and verifies an encrypted bundle of MAC'd data, and returns it.
316      * 
317      * @param wrapped the encoded blob
318      * @return the decrypted data, if it's unexpired
319      * @throws DataSealerException if the data cannot be unwrapped and verified
320      */
321     public String unwrap(String wrapped) throws DataSealerException {
322 
323         try {
324             byte[] in = Base32.decode(wrapped);
325 
326             Cipher cipher = Cipher.getInstance(cipherAlgorithm);
327             int ivSize = cipher.getBlockSize();
328             byte[] iv = new byte[ivSize];
329 
330             Mac mac = Mac.getInstance(macAlgorithm);
331             mac.init(macKey);
332             int macSize = mac.getMacLength();
333 
334             if (in.length < ivSize) {
335                 log.error("Wrapped data is malformed (not enough bytes).");
336                 throw new DataSealerException("Wrapped data is malformed (not enough bytes).");
337             }
338 
339             // extract the IV, setup the cipher and extract the encrypted handle
340             System.arraycopy(in, 0, iv, 0, ivSize);
341             IvParameterSpec ivSpec = new IvParameterSpec(iv);
342             cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec);
343 
344             byte[] encryptedHandle = new byte[in.length - iv.length];
345             System.arraycopy(in, ivSize, encryptedHandle, 0, in.length - iv.length);
346 
347             // decrypt the rest of the data and setup the streams
348             byte[] decryptedBytes = cipher.doFinal(encryptedHandle);
349             ByteArrayInputStream byteStream = new ByteArrayInputStream(decryptedBytes);
350             GZIPInputStream compressedData = new GZIPInputStream(byteStream);
351             DataInputStream dataStream = new DataInputStream(compressedData);
352 
353             // extract the components
354             byte[] decodedMac = new byte[macSize];
355             int bytesRead = dataStream.read(decodedMac);
356             if (bytesRead != macSize) {
357                 log.error("Error parsing unwrapped data, unable to extract HMAC.");
358                 throw new DataSealerException("Error parsing unwrapped data, unable to extract HMAC.");
359             }
360             long decodedExpirationTime = dataStream.readLong();
361             String decodedData = dataStream.readUTF();
362 
363             if (System.currentTimeMillis() > decodedExpirationTime) {
364                 log.info("Unwrapped data has expired.");
365                 throw new DataExpiredException("Unwrapped data has expired.");
366             }
367 
368             byte[] generatedMac = getMAC(mac, decodedData, decodedExpirationTime);
369 
370             if (!Arrays.equals(decodedMac, generatedMac)) {
371                 log.warn("Unwrapped data failed integrity check.");
372                 throw new DataSealerException("Unwrapped data failed integrity check.");
373             }
374 
375             log.debug("Unwrapped data verified.");
376             return decodedData;
377 
378         } catch (GeneralSecurityException e) {
379             log.error(e.getMessage());
380             throw new DataSealerException("Caught GeneralSecurityException unwrapping data.", e);
381         } catch (IOException e) {
382             log.error(e.getMessage());
383             throw new DataSealerException("Caught IOException unwrapping data.", e);
384         }
385     }
386 
387     /**
388      * Encodes data into a cryptographic blob: [IV][HMAC][exp][data] where: [IV] = the Initialization Vector; byte-array
389      * [HMAC] = the HMAC; byte array [exp] = expiration time of the data; 8 bytes; Big-endian [data] = the principal; a
390      * UTF-8-encoded string The bytes are then GZIP'd. The IV is pre-pended to this byte stream, and the result is
391      * Base32-encoded. We don't need to encode the IV or MAC's lengths. They can be obtained from Cipher.getBlockSize()
392      * and Mac.getMacLength(), respectively.
393      * 
394      * @param data the data to wrap
395      * @param exp expiration time
396      * @return the encoded blob
397      * @throws DataSealerException if the wrapping operation fails
398      */
399     public String wrap(String data, long exp) throws DataSealerException {
400 
401         if (data == null) {
402             throw new IllegalArgumentException("Data must be supplied for the wrapping operation.");
403         }
404 
405         try {
406             Mac mac = Mac.getInstance(macAlgorithm);
407             mac.init(macKey);
408 
409             Cipher cipher = Cipher.getInstance(cipherAlgorithm);
410             byte[] iv = new byte[cipher.getBlockSize()];
411             random.nextBytes(iv);
412             IvParameterSpec ivSpec = new IvParameterSpec(iv);
413             cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
414 
415             ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
416             GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
417             DataOutputStream dataStream = new DataOutputStream(compressedStream);
418 
419             dataStream.write(getMAC(mac, data, exp));
420             dataStream.writeLong(exp);
421             dataStream.writeUTF(data);
422 
423             dataStream.flush();
424             compressedStream.flush();
425             compressedStream.finish();
426             byteStream.flush();
427 
428             byte[] encryptedData = cipher.doFinal(byteStream.toByteArray());
429 
430             byte[] handleBytes = new byte[iv.length + encryptedData.length];
431             System.arraycopy(iv, 0, handleBytes, 0, iv.length);
432             System.arraycopy(encryptedData, 0, handleBytes, iv.length, encryptedData.length);
433 
434             return Base32.encode(handleBytes);
435 
436         } catch (KeyException e) {
437             log.error(e.getMessage());
438             throw new DataSealerException("Caught KeyException wrapping data.", e);
439         } catch (GeneralSecurityException e) {
440             log.error(e.getMessage());
441             throw new DataSealerException("Caught GeneralSecurityException wrapping data.", e);
442         } catch (IOException e) {
443             log.error(e.getMessage());
444             throw new DataSealerException("Caught IOException wrapping data.", e);
445         }
446 
447     }
448 
449     /**
450      * Run a test over the configured bean properties.
451      * @throws DataSealerException if the test fails
452      */
453     private void testEncryption() throws DataSealerException {
454 
455         String decrypted;
456         try {
457             Cipher cipher = Cipher.getInstance(cipherAlgorithm);
458             byte[] iv = new byte[cipher.getBlockSize()];
459             random.nextBytes(iv);
460             IvParameterSpec ivSpec = new IvParameterSpec(iv);
461             cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
462             byte[] cipherText = cipher.doFinal("test".getBytes());
463             cipher = Cipher.getInstance(cipherAlgorithm);
464             cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec);
465             decrypted = new String(cipher.doFinal(cipherText));
466         } catch (GeneralSecurityException e) {
467             log.error("Round trip encryption/decryption test unsuccessful: " + e);
468             throw new DataSealerException("Round trip encryption/decryption test unsuccessful.", e);
469         }
470         
471         if (decrypted == null || !"test".equals(decrypted)) {
472             log.error("Round trip encryption/decryption test unsuccessful. Decrypted text did not match.");
473             throw new DataSealerException("Round trip encryption/decryption test unsuccessful.");
474         }
475 
476         byte[] code;
477         try {
478             Mac mac = Mac.getInstance(macAlgorithm);
479             mac.init(macKey);
480             mac.update("foo".getBytes());
481             code = mac.doFinal();
482         } catch (GeneralSecurityException e) {
483             log.error("Message Authentication test unsuccessful: " + e);
484             throw new DataSealerException("Message Authentication test unsuccessful.", e);
485         }
486 
487         if (code == null) {
488             log.error("Message Authentication test unsuccessful.");
489             throw new DataSealerException("Message Authentication test unsuccessful.");
490         }
491     }
492 
493     /**
494      * Compute a MAC over a string, prefixed by an expiration time.
495      * @param mac   MAC object to use
496      * @param data  data to hash
497      * @param exp   timestamp to prefix the data with
498      * @return  the resulting MAC
499      */
500     private static byte[] getMAC(Mac mac, String data, long exp) {
501         mac.update(getLongBytes(exp));
502         mac.update(data.getBytes());
503         return mac.doFinal();
504     }
505 
506     /**
507      * Convert a long value into a byte array.
508      * @param longValue value to convert
509      * @return  a byte array
510      */
511     private static byte[] getLongBytes(long longValue) {
512         try {
513             ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
514             DataOutputStream dataStream = new DataOutputStream(byteStream);
515 
516             dataStream.writeLong(longValue);
517             dataStream.flush();
518             byteStream.flush();
519 
520             return byteStream.toByteArray();
521         } catch (IOException ex) {
522             return null;
523         }
524     }
525 
526     /**
527      * Load keys based on bean properties.
528      * @throws GeneralSecurityException if the keys fail due to a security-related issue
529      * @throws IOException if the load process fails
530      */
531     private void loadKeys() throws GeneralSecurityException, IOException {
532         if (cipherKey == null || macKey == null) {
533             KeyStore ks = KeyStore.getInstance(keystoreType);
534             FileInputStream fis = null;
535             try {
536                 fis = new java.io.FileInputStream(keystorePath);
537                 ks.load(fis, keystorePassword.toCharArray());
538             } finally {
539                 if (fis != null) {
540                     fis.close();
541                 }
542             }
543 
544             Key loadedKey;
545             if (cipherKey == null) {
546                 loadedKey = ks.getKey(cipherKeyAlias, cipherKeyPassword.toCharArray());
547                 if (!(loadedKey instanceof SecretKey)) {
548                     log.error("Cipher key {} is not a symmetric key.", cipherKeyAlias);
549                 }
550                 cipherKey = (SecretKey) loadedKey;
551             }
552 
553             if (macKey == null && macKeyAlias != null) {
554                 loadedKey = ks.getKey(macKeyAlias, macKeyPassword.toCharArray());
555                 if (!(loadedKey instanceof SecretKey)) {
556                     log.error("MAC key {} is not a symmetric key.", macKeyAlias);
557                 }
558                 macKey = (SecretKey) loadedKey;
559             } else if (macKey == null) {
560                 macKey = cipherKey;
561             }
562         }
563     }
564     
565 }