· 7 years ago · Nov 11, 2018, 03:52 PM
1package at.favre.lib.armadillo;
2
3import org.junit.Test;
4
5import java.nio.ByteBuffer;
6import java.nio.charset.StandardCharsets;
7import java.security.MessageDigest;
8import java.security.SecureRandom;
9import java.util.Arrays;
10
11import javax.crypto.Cipher;
12import javax.crypto.Mac;
13import javax.crypto.SecretKey;
14import javax.crypto.spec.IvParameterSpec;
15import javax.crypto.spec.SecretKeySpec;
16
17import at.favre.lib.crypto.HKDF;
18
19import static org.junit.Assert.assertArrayEquals;
20import static org.junit.Assert.assertFalse;
21
22/**
23 * Companion code to my article about AES+CBC with Encrypt-then-MAC
24 */
25public class AesCbcExample {
26
27 private final SecureRandom secureRandom = new SecureRandom();
28
29 @Test
30 public void testEncryption() throws Exception {
31 // create a random key
32 SecureRandom secureRandom = new SecureRandom();
33 byte[] key = new byte[16];
34 secureRandom.nextBytes(key);
35
36 // the possible plain text
37 byte[] plainText = "A secret message we created.".getBytes(StandardCharsets.UTF_8);
38
39 // data to add to the authentication tag - possibly protocol version
40 byte[] aad = new byte[] {0x01, 0x02};
41
42 byte[] cipherText = encrypt(key, plainText, aad);
43 byte[] decrypted = decrypt(key, cipherText, aad);
44
45 // plaintext and decrypted must be equal
46 assertArrayEquals(plainText, decrypted);
47 // plaintext must not be equal to cipher text
48 assertFalse(Arrays.equals(plainText, cipherText));
49 }
50
51 /**
52 * Encrpyt given plaintext with given key.
53 *
54 * @param key must be strong 16, 24 or 32 byte secret key
55 * @param plainText to encrypt
56 * @param associatedData optional data added to the authentication tag
57 * @return encrypted message including mac & iv
58 * @throws Exception
59 */
60 private byte[] encrypt(byte[] key, byte[] plainText, byte[] associatedData) throws Exception {
61 byte[] iv = new byte[16];
62 secureRandom.nextBytes(iv);
63
64 byte[] encKey = HKDF.fromHmacSha256().expand(key, "encKey".getBytes(StandardCharsets.UTF_8), 16);
65 byte[] authKey = HKDF.fromHmacSha256().expand(key, "authKey".getBytes(StandardCharsets.UTF_8), 32); //HMAC-SHA256 key is 32 byte
66
67 final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //actually uses PKCS#7
68 cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encKey, "AES"), new IvParameterSpec(iv));
69 byte[] cipherText = cipher.doFinal(plainText);
70
71 SecretKey macKey = new SecretKeySpec(authKey, "HmacSHA256");
72 Mac hmac = Mac.getInstance("HmacSHA256");
73 hmac.init(macKey);
74 hmac.update(iv);
75 hmac.update(cipherText);
76
77 if (associatedData != null) {
78 hmac.update(associatedData);
79 }
80
81 byte[] mac = hmac.doFinal();
82
83 ByteBuffer byteBuffer = ByteBuffer.allocate(1 + iv.length + 1 + mac.length + cipherText.length);
84 byteBuffer.put((byte) iv.length);
85 byteBuffer.put(iv);
86 byteBuffer.put((byte) mac.length);
87 byteBuffer.put(mac);
88 byteBuffer.put(cipherText);
89 byte[] cipherMessage = byteBuffer.array();
90
91 Arrays.fill(authKey, (byte) 0);
92 Arrays.fill(encKey, (byte) 0);
93
94 return cipherMessage;
95 }
96
97 /**
98 * Decrypt previously encrypted message with {@link #encrypt(byte[], byte[], byte[])}.
99 *
100 * @param key same secret used during encrpytion
101 * @param cipherMessage the message returned by encrypt
102 * @param associatedData optional data added to the authentication tag
103 * @return the plain text
104 * @throws Exception
105 */
106 private byte[] decrypt(byte[] key, byte[] cipherMessage, byte[] associatedData) throws Exception {
107 ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
108
109 int ivLength = (byteBuffer.get());
110 if (ivLength != 16) { // check input parameter
111 throw new IllegalArgumentException("invalid iv length");
112 }
113 byte[] iv = new byte[ivLength];
114 byteBuffer.get(iv);
115
116 int macLength = (byteBuffer.get());
117 if (macLength != 32) { // check input parameter
118 throw new IllegalArgumentException("invalid mac length");
119 }
120 byte[] mac = new byte[macLength];
121 byteBuffer.get(mac);
122
123 byte[] cipherText = new byte[byteBuffer.remaining()];
124 byteBuffer.get(cipherText);
125
126 byte[] encKey = HKDF.fromHmacSha256().expand(key, "encKey".getBytes(StandardCharsets.UTF_8), 16);
127 byte[] authKey = HKDF.fromHmacSha256().expand(key, "authKey".getBytes(StandardCharsets.UTF_8), 32);
128
129 SecretKey macKey = new SecretKeySpec(authKey, "HmacSHA256");
130 Mac hmac = Mac.getInstance("HmacSHA256");
131 hmac.init(macKey);
132 hmac.update(iv);
133 hmac.update(cipherText);
134 if (associatedData != null) {
135 hmac.update(associatedData);
136 }
137 byte[] refMac = hmac.doFinal();
138
139 if (!MessageDigest.isEqual(refMac, mac)) {
140 throw new SecurityException("could not authenticate");
141 }
142
143 final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
144 cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(encKey, "AES"), new IvParameterSpec(iv));
145 byte[] plainText = cipher.doFinal(cipherText);
146
147 return plainText;
148 }
149}