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