· 6 years ago · May 26, 2019, 10:56 PM
1import org.apache.commons.codec.binary.Base64;
2
3import javax.crypto.Cipher;
4import javax.crypto.KeyGenerator;
5import javax.crypto.Mac;
6import javax.crypto.SecretKey;
7import javax.crypto.SecretKeyFactory;
8import javax.crypto.spec.IvParameterSpec;
9import javax.crypto.spec.PBEKeySpec;
10import javax.crypto.spec.SecretKeySpec;
11import java.io.UnsupportedEncodingException;
12import java.security.GeneralSecurityException;
13import java.security.InvalidKeyException;
14import java.security.NoSuchAlgorithmException;
15import java.security.SecureRandom;
16import java.security.spec.KeySpec;
17import java.util.Arrays;
18
19/**
20 * Simple library for the "right" defaults for AES key generation, encryption,
21 * and decryption using 128-bit AES, CBC, PKCS5 padding, and a random 16-byte IV
22 * with SHA1PRNG. Integrity with HmacSHA256.
23 *
24 * @author chenyun
25 * @date 2019-05-16
26 */
27public class AesCbcWithIntegrity {
28 private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
29 private static final String CIPHER = "AES";
30 private static final int AES_KEY_LENGTH_BITS = 128;
31 private static final int IV_LENGTH_BYTES = 16;
32 private static final int PBE_ITERATION_COUNT = 10000;
33 private static final int PBE_SALT_LENGTH_BITS = AES_KEY_LENGTH_BITS;
34 private static final String PBE_ALGORITHM = "PBKDF2WithHmacSHA1";
35
36 private static final String HMAC_ALGORITHM = "HmacSHA256";
37 private static final int HMAC_KEY_LENGTH_BITS = 256;
38
39 /**
40 * Converts the given AES/HMAC keys into a base64 encoded string suitable for
41 * storage. Sister function of keys.
42 *
43 * @param keys The combined aes and hmac keys
44 * @return a base 64 encoded AES string and hmac key as base64(aesKey) : base64(hmacKey)
45 */
46 public static String keyString(SecretKeys keys) {
47 return keys.toString();
48 }
49
50 /**
51 * An aes key derived from a base64 encoded key. This does not generate the
52 * key. It's not random or a PBE key.
53 *
54 * @param keysStr a base64 encoded AES key / hmac key as base64(aesKey) : base64(hmacKey).
55 * @return an AES and HMAC key set suitable for other functions.
56 */
57 public static SecretKeys deriveKey(String keysStr) throws InvalidKeyException {
58 String[] keysArr = keysStr.split(":");
59
60 if (keysArr.length != 2) {
61 throw new IllegalArgumentException("Cannot parse aesKey:hmacKey");
62
63 } else {
64 byte[] confidentialityKey = Base64.decodeBase64(keysArr[0]);
65 if (confidentialityKey.length != AES_KEY_LENGTH_BITS / 8) {
66 throw new InvalidKeyException("Base64 decoded key is not " + AES_KEY_LENGTH_BITS + " bytes");
67 }
68 byte[] integrityKey = Base64.decodeBase64(keysArr[1]);
69 if (integrityKey.length != HMAC_KEY_LENGTH_BITS / 8) {
70 throw new InvalidKeyException("Base64 decoded key is not " + HMAC_KEY_LENGTH_BITS + " bytes");
71 }
72
73 return new SecretKeys(
74 new SecretKeySpec(confidentialityKey, 0, confidentialityKey.length, CIPHER),
75 new SecretKeySpec(integrityKey, HMAC_ALGORITHM));
76 }
77 }
78
79 /**
80 * A function that generates random AES and HMAC keys and prints out exceptions but
81 * doesn't throw them since none should be encountered. If they are
82 * encountered, the return value is null.
83 *
84 * @return The AES and HMAC keys.
85 * @throws GeneralSecurityException if AES is not implemented on this system,
86 * or a suitable RNG is not available
87 */
88 public static SecretKeys generateKey() throws GeneralSecurityException {
89 KeyGenerator keyGen = KeyGenerator.getInstance(CIPHER);
90 // No need to provide a SecureRandom or set a seed since that will
91 // happen automatically.
92 keyGen.init(AES_KEY_LENGTH_BITS);
93 SecretKey confidentialityKey = keyGen.generateKey();
94
95 //Now make the HMAC key
96 byte[] integrityKeyBytes = randomBytes(HMAC_KEY_LENGTH_BITS / 8);
97 SecretKey integrityKey = new SecretKeySpec(integrityKeyBytes, HMAC_ALGORITHM);
98
99 return new SecretKeys(confidentialityKey, integrityKey);
100 }
101
102 /**
103 * A function that generates password-based AES and HMAC keys. It prints out exceptions but
104 * doesn't throw them since none should be encountered. If they are
105 * encountered, the return value is null.
106 *
107 * @param password The password to derive the keys from.
108 * @return The AES and HMAC keys.
109 * @throws GeneralSecurityException if AES is not implemented on this system,
110 * or a suitable RNG is not available
111 */
112 public static SecretKeys generateKeyFromPassword(String password, byte[] salt) throws GeneralSecurityException {
113 //Get enough random bytes for both the AES key and the HMAC key:
114 KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt,
115 PBE_ITERATION_COUNT, AES_KEY_LENGTH_BITS + HMAC_KEY_LENGTH_BITS);
116 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBE_ALGORITHM);
117 byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
118
119 // Split the random bytes into two parts:
120 byte[] confidentialityKeyBytes = copyOfRange(keyBytes, 0, AES_KEY_LENGTH_BITS / 8);
121 byte[] integrityKeyBytes = copyOfRange(keyBytes, AES_KEY_LENGTH_BITS / 8, AES_KEY_LENGTH_BITS / 8 + HMAC_KEY_LENGTH_BITS / 8);
122
123 //Generate the AES key
124 SecretKey confidentialityKey = new SecretKeySpec(confidentialityKeyBytes, CIPHER);
125
126 //Generate the HMAC key
127 SecretKey integrityKey = new SecretKeySpec(integrityKeyBytes, HMAC_ALGORITHM);
128
129 return new SecretKeys(confidentialityKey, integrityKey);
130 }
131
132 /**
133 * A function that generates password-based AES and HMAC keys. See generateKeyFromPassword.
134 *
135 * @param password The password to derive the AES/HMAC keys from
136 * @param salt A string version of the salt; base64 encoded.
137 * @return The AES and HMAC keys.
138 * @throws GeneralSecurityException
139 */
140 public static SecretKeys generateKeyFromPassword(String password, String salt) throws GeneralSecurityException {
141 return generateKeyFromPassword(password, Base64.decodeBase64(salt));
142 }
143
144 /**
145 * Generates a random salt.
146 *
147 * @return The random salt suitable for generateKeyFromPassword.
148 */
149 public static byte[] generateSalt() {
150 return randomBytes(PBE_SALT_LENGTH_BITS);
151 }
152
153 /**
154 * Converts the given salt into a base64 encoded string suitable for
155 * storage.
156 *
157 * @param salt
158 * @return a base 64 encoded salt string suitable to pass into generateKeyFromPassword.
159 */
160 public static String saltString(byte[] salt) {
161 return Base64.encodeBase64String(salt);
162 }
163
164
165 /**
166 * Creates a random Initialization Vector (IV) of IV_LENGTH_BYTES.
167 *
168 * @return The byte array of this IV
169 */
170 public static byte[] generateIv() {
171 return randomBytes(IV_LENGTH_BYTES);
172 }
173
174 private static byte[] randomBytes(int length) {
175 SecureRandom random = new SecureRandom();
176 byte[] b = new byte[length];
177 random.nextBytes(b);
178 return b;
179 }
180
181 /*
182 * -----------------------------------------------------------------
183 * Encryption
184 * -----------------------------------------------------------------
185 */
186
187 /**
188 * Generates a random IV and encrypts this plain text with the given key. Then attaches
189 * a hashed MAC, which is contained in the CipherTextIvMac class.
190 *
191 * @param plaintext The text that will be encrypted, which
192 * will be serialized with UTF-8
193 * @param secretKeys The AES and HMAC keys with which to encrypt
194 * @return a tuple of the IV, ciphertext, mac
195 * @throws GeneralSecurityException if AES is not implemented on this system
196 * @throws UnsupportedEncodingException if UTF-8 is not supported in this system
197 */
198 public static CipherTextIvMac encrypt(String plaintext, SecretKeys secretKeys)
199 throws UnsupportedEncodingException, GeneralSecurityException {
200 return encrypt(plaintext, secretKeys, "UTF-8");
201 }
202
203 /**
204 * Generates a random IV and encrypts this plain text with the given key. Then attaches
205 * a hashed MAC, which is contained in the CipherTextIvMac class.
206 *
207 * @param plaintext The bytes that will be encrypted
208 * @param secretKeys The AES and HMAC keys with which to encrypt
209 * @return a tuple of the IV, ciphertext, mac
210 * @throws GeneralSecurityException if AES is not implemented on this system
211 * @throws UnsupportedEncodingException if the specified encoding is invalid
212 */
213 public static CipherTextIvMac encrypt(String plaintext, SecretKeys secretKeys, String encoding)
214 throws UnsupportedEncodingException, GeneralSecurityException {
215 return encrypt(plaintext.getBytes(encoding), secretKeys);
216 }
217
218 /**
219 * Generates a random IV and encrypts this plain text with the given key. Then attaches
220 * a hashed MAC, which is contained in the CipherTextIvMac class.
221 *
222 * @param plaintext The text that will be encrypted
223 * @param secretKeys The combined AES and HMAC keys with which to encrypt
224 * @return a tuple of the IV, ciphertext, mac
225 * @throws GeneralSecurityException if AES is not implemented on this system
226 */
227 public static CipherTextIvMac encrypt(byte[] plaintext, SecretKeys secretKeys)
228 throws GeneralSecurityException {
229 byte[] iv = generateIv();
230 Cipher aesCipherForEncryption = Cipher.getInstance(CIPHER_TRANSFORMATION);
231 aesCipherForEncryption.init(Cipher.ENCRYPT_MODE, secretKeys.getConfidentialityKey(), new IvParameterSpec(iv));
232
233 /*
234 * Now we get back the IV that will actually be used. Some Android
235 * versions do funny stuff w/ the IV, so this is to work around bugs:
236 */
237 iv = aesCipherForEncryption.getIV();
238 byte[] byteCipherText = aesCipherForEncryption.doFinal(plaintext);
239 byte[] ivCipherConcat = CipherTextIvMac.ivCipherConcat(iv, byteCipherText);
240
241 byte[] integrityMac = generateMac(ivCipherConcat, secretKeys.getIntegrityKey());
242 return new CipherTextIvMac(byteCipherText, iv, integrityMac);
243 }
244
245 /*
246 * -----------------------------------------------------------------
247 * Decryption
248 * -----------------------------------------------------------------
249 */
250
251 /**
252 * AES CBC decrypt.
253 *
254 * @param civ The cipher text, IV, and mac
255 * @param secretKeys The AES and HMAC keys
256 * @param encoding The string encoding to use to decode the bytes after decryption
257 * @return A string derived from the decrypted bytes (not base64 encoded)
258 * @throws GeneralSecurityException if AES is not implemented on this system
259 * @throws UnsupportedEncodingException if the encoding is unsupported
260 */
261 public static String decryptString(CipherTextIvMac civ, SecretKeys secretKeys, String encoding)
262 throws UnsupportedEncodingException, GeneralSecurityException {
263 return new String(decrypt(civ, secretKeys), encoding);
264 }
265
266 /**
267 * AES CBC decrypt.
268 *
269 * @param civ The cipher text, IV, and mac
270 * @param secretKeys The AES and HMAC keys
271 * @return A string derived from the decrypted bytes, which are interpreted
272 * as a UTF-8 String
273 * @throws GeneralSecurityException if AES is not implemented on this system
274 * @throws UnsupportedEncodingException if UTF-8 is not supported
275 */
276 public static String decryptString(CipherTextIvMac civ, SecretKeys secretKeys)
277 throws UnsupportedEncodingException, GeneralSecurityException {
278 return decryptString(civ, secretKeys, "UTF-8");
279 }
280
281 /**
282 * AES CBC decrypt.
283 *
284 * @param civ the cipher text, iv, and mac
285 * @param secretKeys the AES and HMAC keys
286 * @return The raw decrypted bytes
287 * @throws GeneralSecurityException if MACs don't match or AES is not implemented
288 */
289 public static byte[] decrypt(CipherTextIvMac civ, SecretKeys secretKeys)
290 throws GeneralSecurityException {
291
292 byte[] ivCipherConcat = CipherTextIvMac.ivCipherConcat(civ.getIv(), civ.getCipherText());
293 byte[] computedMac = generateMac(ivCipherConcat, secretKeys.getIntegrityKey());
294 if (constantTimeEq(computedMac, civ.getMac())) {
295 Cipher aesCipherForDecryption = Cipher.getInstance(CIPHER_TRANSFORMATION);
296 aesCipherForDecryption.init(Cipher.DECRYPT_MODE, secretKeys.getConfidentialityKey(),
297 new IvParameterSpec(civ.getIv()));
298 return aesCipherForDecryption.doFinal(civ.getCipherText());
299 } else {
300 throw new GeneralSecurityException("MAC stored in civ does not match computed MAC.");
301 }
302 }
303
304 /*
305 * -----------------------------------------------------------------
306 * Helper Code
307 * -----------------------------------------------------------------
308 */
309
310 /**
311 * Generate the mac based on HMAC_ALGORITHM
312 *
313 * @param integrityKey The key used for hmac
314 * @param byteCipherText the cipher text
315 * @return A byte array of the HMAC for the given key and ciphertext
316 * @throws NoSuchAlgorithmException
317 * @throws InvalidKeyException
318 */
319 public static byte[] generateMac(byte[] byteCipherText, SecretKey integrityKey) throws NoSuchAlgorithmException, InvalidKeyException {
320 //Now compute the mac for later integrity checking
321 Mac sha256_HMAC = Mac.getInstance(HMAC_ALGORITHM);
322 sha256_HMAC.init(integrityKey);
323 return sha256_HMAC.doFinal(byteCipherText);
324 }
325
326 /**
327 * Holder class that has both the secret AES key for encryption (confidentiality)
328 * and the secret HMAC key for integrity.
329 */
330
331 public static class SecretKeys {
332 private SecretKey confidentialityKey;
333 private SecretKey integrityKey;
334
335 /**
336 * Construct the secret keys container.
337 *
338 * @param confidentialityKeyIn The AES key
339 * @param integrityKeyIn the HMAC key
340 */
341 public SecretKeys(SecretKey confidentialityKeyIn, SecretKey integrityKeyIn) {
342 setConfidentialityKey(confidentialityKeyIn);
343 setIntegrityKey(integrityKeyIn);
344 }
345
346 public SecretKey getConfidentialityKey() {
347 return confidentialityKey;
348 }
349
350 public void setConfidentialityKey(SecretKey confidentialityKey) {
351 this.confidentialityKey = confidentialityKey;
352 }
353
354 public SecretKey getIntegrityKey() {
355 return integrityKey;
356 }
357
358 public void setIntegrityKey(SecretKey integrityKey) {
359 this.integrityKey = integrityKey;
360 }
361
362 /**
363 * Encodes the two keys as a string
364 *
365 * @return base64(confidentialityKey):base64(integrityKey)
366 */
367 @Override
368 public String toString() {
369 return Base64.encodeBase64String(getConfidentialityKey().getEncoded())
370 + ":" + Base64.encodeBase64String(getIntegrityKey().getEncoded());
371 }
372
373 @Override
374 public int hashCode() {
375 final int prime = 31;
376 int result = 1;
377 result = prime * result + confidentialityKey.hashCode();
378 result = prime * result + integrityKey.hashCode();
379 return result;
380 }
381
382 @Override
383 public boolean equals(Object obj) {
384 if (this == obj) {
385 return true;
386 }
387 if (obj == null) {
388 return false;
389 }
390 if (getClass() != obj.getClass()) {
391 return false;
392 }
393 SecretKeys other = (SecretKeys) obj;
394 if (!integrityKey.equals(other.integrityKey)) {
395 return false;
396 }
397 if (!confidentialityKey.equals(other.confidentialityKey)) {
398 return false;
399 }
400 return true;
401 }
402 }
403
404
405 /**
406 * Simple constant-time equality of two byte arrays. Used for security to avoid timing attacks.
407 *
408 * @param a
409 * @param b
410 * @return true iff the arrays are exactly equal.
411 */
412 public static boolean constantTimeEq(byte[] a, byte[] b) {
413 if (a.length != b.length) {
414 return false;
415 }
416 int result = 0;
417 for (int i = 0; i < a.length; i++) {
418 result |= a[i] ^ b[i];
419 }
420 return result == 0;
421 }
422
423 /**
424 * Holder class that allows us to bundle ciphertext and IV together.
425 */
426 public static class CipherTextIvMac {
427 private final byte[] cipherText;
428 private final byte[] iv;
429 private final byte[] mac;
430
431 public byte[] getCipherText() {
432 return cipherText;
433 }
434
435 public byte[] getIv() {
436 return iv;
437 }
438
439 public byte[] getMac() {
440 return mac;
441 }
442
443 /**
444 * Construct a new bundle of ciphertext and IV.
445 *
446 * @param c The ciphertext
447 * @param i The IV
448 * @param h The mac
449 */
450 public CipherTextIvMac(byte[] c, byte[] i, byte[] h) {
451 cipherText = new byte[c.length];
452 System.arraycopy(c, 0, cipherText, 0, c.length);
453 iv = new byte[i.length];
454 System.arraycopy(i, 0, iv, 0, i.length);
455 mac = new byte[h.length];
456 System.arraycopy(h, 0, mac, 0, h.length);
457 }
458
459 /**
460 * Constructs a new bundle of ciphertext and IV from a string of the
461 * format <code>base64(iv):base64(ciphertext)</code>.
462 *
463 * @param base64IvAndCiphertext A string of the format
464 * <code>iv:ciphertext</code> The IV and ciphertext must each
465 * be base64-encoded.
466 */
467 public CipherTextIvMac(String base64IvAndCiphertext) {
468 String[] civArray = base64IvAndCiphertext.split(":");
469 if (civArray.length != 3) {
470 throw new IllegalArgumentException("Cannot parse iv:mac:ciphertext");
471 } else {
472 iv = Base64.decodeBase64(civArray[0]);
473 mac = Base64.decodeBase64(civArray[1]);
474 cipherText = Base64.decodeBase64(civArray[2]);
475 }
476 }
477
478 /**
479 * Concatinate the IV to the cipherText using array copy.
480 * This is used e.g. before computing mac.
481 *
482 * @param iv The IV to prepend
483 * @param cipherText the cipherText to append
484 * @return iv:cipherText, a new byte array.
485 */
486 public static byte[] ivCipherConcat(byte[] iv, byte[] cipherText) {
487 byte[] combined = new byte[iv.length + cipherText.length];
488 System.arraycopy(iv, 0, combined, 0, iv.length);
489 System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length);
490 return combined;
491 }
492
493 /**
494 * Encodes this ciphertext, IV, mac as a string.
495 *
496 * @return base64(iv) : base64(mac) : base64(ciphertext).
497 * The iv and mac go first because they're fixed length.
498 */
499 @Override
500 public String toString() {
501 String ivString = Base64.encodeBase64String(iv);
502 String cipherTextString = Base64.encodeBase64String(cipherText);
503 String macString = Base64.encodeBase64String(mac);
504 return String.format(ivString + ":" + macString + ":" + cipherTextString);
505 }
506
507 @Override
508 public int hashCode() {
509 final int prime = 31;
510 int result = 1;
511 result = prime * result + Arrays.hashCode(cipherText);
512 result = prime * result + Arrays.hashCode(iv);
513 result = prime * result + Arrays.hashCode(mac);
514 return result;
515 }
516
517 @Override
518 public boolean equals(Object obj) {
519 if (this == obj) {
520 return true;
521 }
522 if (obj == null) {
523 return false;
524 }
525 if (getClass() != obj.getClass()) {
526 return false;
527 }
528 CipherTextIvMac other = (CipherTextIvMac) obj;
529 if (!Arrays.equals(cipherText, other.cipherText)) {
530 return false;
531 }
532 if (!Arrays.equals(iv, other.iv)) {
533 return false;
534 }
535 if (!Arrays.equals(mac, other.mac)) {
536 return false;
537 }
538 return true;
539 }
540 }
541
542 /**
543 * Copy the elements from the start to the end
544 *
545 * @param from the source
546 * @param start the start index to copy
547 * @param end the end index to finish
548 * @return the new buffer
549 */
550 private static byte[] copyOfRange(byte[] from, int start, int end) {
551 int length = end - start;
552 byte[] result = new byte[length];
553 System.arraycopy(from, start, result, 0, length);
554 return result;
555 }
556}