Last active
May 14, 2024 05:25
-
-
Save patrickfav/b323f0d9cbd81d5fa9cc4c971b732c77 to your computer and use it in GitHub Desktop.
Companion code to my article about AES+CBC with Encrypt-then-MAC.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package at.favre.lib.armadillo; | |
import org.junit.Test; | |
import java.nio.ByteBuffer; | |
import java.nio.charset.StandardCharsets; | |
import java.security.MessageDigest; | |
import java.security.SecureRandom; | |
import java.util.Arrays; | |
import javax.crypto.Cipher; | |
import javax.crypto.Mac; | |
import javax.crypto.SecretKey; | |
import javax.crypto.spec.IvParameterSpec; | |
import javax.crypto.spec.SecretKeySpec; | |
import at.favre.lib.crypto.HKDF; | |
import static org.junit.Assert.assertArrayEquals; | |
import static org.junit.Assert.assertFalse; | |
/** | |
* Companion code to my article about AES+CBC with Encrypt-then-MAC | |
*/ | |
public class AesCbcExample { | |
private final SecureRandom secureRandom = new SecureRandom(); | |
@Test | |
public void testEncryption() throws Exception { | |
// create a random key | |
SecureRandom secureRandom = new SecureRandom(); | |
byte[] key = new byte[16]; | |
secureRandom.nextBytes(key); | |
// the possible plain text | |
byte[] plainText = "A secret message we created.".getBytes(StandardCharsets.UTF_8); | |
// data to add to the authentication tag - possibly protocol version | |
byte[] aad = new byte[] {0x01, 0x02}; | |
byte[] cipherText = encrypt(key, plainText, aad); | |
byte[] decrypted = decrypt(key, cipherText, aad); | |
// plaintext and decrypted must be equal | |
assertArrayEquals(plainText, decrypted); | |
// plaintext must not be equal to cipher text | |
assertFalse(Arrays.equals(plainText, cipherText)); | |
} | |
/** | |
* Encrpyt given plaintext with given key. | |
* | |
* @param key must be strong 16, 24 or 32 byte secret key | |
* @param plainText to encrypt | |
* @param associatedData optional data added to the authentication tag | |
* @return encrypted message including mac & iv | |
* @throws Exception | |
*/ | |
private byte[] encrypt(byte[] key, byte[] plainText, byte[] associatedData) throws Exception { | |
byte[] iv = new byte[16]; | |
secureRandom.nextBytes(iv); | |
byte[] encKey = HKDF.fromHmacSha256().expand(key, "encKey".getBytes(StandardCharsets.UTF_8), 16); | |
byte[] authKey = HKDF.fromHmacSha256().expand(key, "authKey".getBytes(StandardCharsets.UTF_8), 32); //HMAC-SHA256 key is 32 byte | |
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //actually uses PKCS#7 | |
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encKey, "AES"), new IvParameterSpec(iv)); | |
byte[] cipherText = cipher.doFinal(plainText); | |
SecretKey macKey = new SecretKeySpec(authKey, "HmacSHA256"); | |
Mac hmac = Mac.getInstance("HmacSHA256"); | |
hmac.init(macKey); | |
hmac.update(iv); | |
hmac.update(cipherText); | |
if (associatedData != null) { | |
hmac.update(associatedData); | |
} | |
byte[] mac = hmac.doFinal(); | |
ByteBuffer byteBuffer = ByteBuffer.allocate(1 + iv.length + 1 + mac.length + cipherText.length); | |
byteBuffer.put((byte) iv.length); | |
byteBuffer.put(iv); | |
byteBuffer.put((byte) mac.length); | |
byteBuffer.put(mac); | |
byteBuffer.put(cipherText); | |
byte[] cipherMessage = byteBuffer.array(); | |
Arrays.fill(authKey, (byte) 0); | |
Arrays.fill(encKey, (byte) 0); | |
return cipherMessage; | |
} | |
/** | |
* Decrypt previously encrypted message with {@link #encrypt(byte[], byte[], byte[])}. | |
* | |
* @param key same secret used during encrpytion | |
* @param cipherMessage the message returned by encrypt | |
* @param associatedData optional data added to the authentication tag | |
* @return the plain text | |
* @throws Exception | |
*/ | |
private byte[] decrypt(byte[] key, byte[] cipherMessage, byte[] associatedData) throws Exception { | |
ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage); | |
int ivLength = (byteBuffer.get()); | |
if (ivLength != 16) { // check input parameter | |
throw new IllegalArgumentException("invalid iv length"); | |
} | |
byte[] iv = new byte[ivLength]; | |
byteBuffer.get(iv); | |
int macLength = (byteBuffer.get()); | |
if (macLength != 32) { // check input parameter | |
throw new IllegalArgumentException("invalid mac length"); | |
} | |
byte[] mac = new byte[macLength]; | |
byteBuffer.get(mac); | |
byte[] cipherText = new byte[byteBuffer.remaining()]; | |
byteBuffer.get(cipherText); | |
byte[] encKey = HKDF.fromHmacSha256().expand(key, "encKey".getBytes(StandardCharsets.UTF_8), 16); | |
byte[] authKey = HKDF.fromHmacSha256().expand(key, "authKey".getBytes(StandardCharsets.UTF_8), 32); | |
SecretKey macKey = new SecretKeySpec(authKey, "HmacSHA256"); | |
Mac hmac = Mac.getInstance("HmacSHA256"); | |
hmac.init(macKey); | |
hmac.update(iv); | |
hmac.update(cipherText); | |
if (associatedData != null) { | |
hmac.update(associatedData); | |
} | |
byte[] refMac = hmac.doFinal(); | |
if (!MessageDigest.isEqual(refMac, mac)) { | |
throw new SecurityException("could not authenticate"); | |
} | |
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | |
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(encKey, "AES"), new IvParameterSpec(iv)); | |
byte[] plainText = cipher.doFinal(cipherText); | |
return plainText; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Why do you need line 131 : hmac.init(macKey) ?