View Javadoc

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