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