1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
45
46
47
48
49
50 public class DataSealer {
51
52
53 private static Logger log = LoggerFactory.getLogger(DataSealer.class.getName());
54
55
56 private SecretKey cipherKey;
57
58
59 private SecretKey macKey;
60
61
62 private SecureRandom random;
63
64
65 private String keystoreType = "JCEKS";
66
67
68 private String keystorePath;
69
70
71 private String keystorePassword;
72
73
74 private String cipherKeyAlias;
75
76
77 private String cipherKeyPassword;
78
79
80 private String cipherAlgorithm = "AES/CBC/PKCS5Padding";
81
82
83 private String macKeyAlias;
84
85
86 private String macKeyPassword;
87
88
89 private String macAlgorithm = "HmacSHA256";
90
91
92
93
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
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
124
125
126 public SecretKey getCipherKey() {
127 return cipherKey;
128 }
129
130
131
132
133
134 public SecretKey getMacKey() {
135 return macKey;
136 }
137
138
139
140
141
142 public SecureRandom getRandom() {
143 return random;
144 }
145
146
147
148
149
150 public String getKeystoreType() {
151 return keystoreType;
152 }
153
154
155
156
157
158 public String getKeystorePath() {
159 return keystorePath;
160 }
161
162
163
164
165
166 public String getKeystorePassword() {
167 return keystorePassword;
168 }
169
170
171
172
173
174 public String getCipherKeyAlias() {
175 return cipherKeyAlias;
176 }
177
178
179
180
181
182 public String getCipherKeyPassword() {
183 return cipherKeyPassword;
184 }
185
186
187
188
189
190 public String getCipherAlgorithm() {
191 return cipherAlgorithm;
192 }
193
194
195
196
197
198 public String getMacKeyAlias() {
199 return macKeyAlias;
200 }
201
202
203
204
205
206 public String getMacKeyPassword() {
207 return macKeyPassword;
208 }
209
210
211
212
213
214 public String getMacAlgorithm() {
215 return macAlgorithm;
216 }
217
218
219
220
221
222 public void setCipherKey(SecretKey key) {
223 cipherKey = key;
224 }
225
226
227
228
229
230 public void setMacKey(SecretKey key) {
231 macKey = key;
232 }
233
234
235
236
237
238 public void setRandom(SecureRandom r) {
239 random = r;
240 }
241
242
243
244
245
246 public void setKeystoreType(String type) {
247 keystoreType = type;
248 }
249
250
251
252
253
254 public void setKeystorePath(String path) {
255 keystorePath = path;
256 }
257
258
259
260
261
262 public void setKeystorePassword(String password) {
263 keystorePassword = password;
264 }
265
266
267
268
269
270 public void setCipherKeyAlias(String alias) {
271 cipherKeyAlias = alias;
272 }
273
274
275
276
277
278 public void setCipherKeyPassword(String password) {
279 cipherKeyPassword = password;
280 }
281
282
283
284
285
286 public void setCipherAlgorithm(String alg) {
287 cipherAlgorithm = alg;
288 }
289
290
291
292
293
294 public void setMacKeyAlias(String alias) {
295 macKeyAlias = alias;
296 }
297
298
299
300
301
302 public void setMacKeyPassword(String password) {
303 macKeyPassword = password;
304 }
305
306
307
308
309
310 public void setMacAlgorithm(String alg) {
311 macAlgorithm = alg;
312 }
313
314
315
316
317
318
319
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
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
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
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
389
390
391
392
393
394
395
396
397
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
451
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
495
496
497
498
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
508
509
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
528
529
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 }