I'm trying to implement AES-CBC cipher using built-in ECB Java implementation by following steps for CBC mode as specified here. Note, I'm not concerned with the actual security (e.g., no padding, or using key as IV) of my implementation.
The problem is that only about some part of the bytes is encoded correctly when compared to this site when using PKCS5Padding
.
Key: mvLBiZsiTbGwrfJB
Input: abcdabcdabcdabcd
My result: e9qdKeY1m4OAIsPerfnUi5F35z814ywucLJKKi4rTP8=
Result from site: e9qdKeY1m4OAIsPerfnUi9jNsRJtdELZliFtebuJrrc=
Key: mvLBiZsiTbGwrfJB
Input: abcdabcdabcdabcdabcdabcdabcdabcd
My result: e9qdKeY1m4OAIsPerfnUi5F35z814ywucLJKKi4rTP8=uf5VPLwumm+66ESiQMlKXJF35z814ywucLJKKi4rTP8=
Result from site: e9qdKeY1m4OAIsPerfnUi7I+cPTpraAgZIQvr8OLf7Iu4eKRG1MIcq5yQGsRt3PS
For NoPadding
option:
Key: mvLBiZsiTbGwrfJB
Input: abcdabcdabcdabcd
My result: e9qdKeY1m4OAIsPerfnUiw==
Key: mvLBiZsiTbGwrfJB
Input: abcdabcdabcdabcdabcdabcdabcdabcd
My result: e9qdKeY1m4OAIsPerfnUiw==uf5VPLwumm+66ESiQMlKXA==
Also, the decryption doesn't work at all, for PKCS5Padding
I've got exception:
Exception in thread "main" javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
at java.base/com.sun.crypto.provider.CipherCore.prepareInputBuffer(CipherCore.java:1005)
at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:848)
at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202)
at AesEcb.decrypt(AesEcb.kt:27)
at AesEcb.decryptToByteArray(AesEcb.kt:36)
at AesCbcOwn.decrypt(AesCbcOwn.kt:32)
and for NoPadding
:
Exception in thread "main" javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes
at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1109)
at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053)
at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853)
at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202)
at AesEcb.decrypt(AesEcb.kt:27)
at AesEcb.decryptToByteArray(AesEcb.kt:36)
at AesCbcOwn.decrypt(AesCbcOwn.kt:32)
I thought that these problems were related to padding options but I still can't make this work regardless of chosen option.
AES-ECB implementation which is used in CBC implementation:
object AesEcb : Aes {
override val cipher: Cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
override fun encrypt(input: String, key: String): String {
val encrypted: ByteArray = try {
val secretKey = SecretKeySpec(key.toByteArray(), "AES")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
cipher.doFinal(input.toByteArray())
} catch (e: InvalidKeyException) {
throw e
}
return Base64.getEncoder().run { encodeToString(encrypted) }
}
override fun decrypt(input: String, key: String): String {
val output: ByteArray = try {
val secretKey = SecretKeySpec(key.toByteArray(), "AES")
cipher.init(Cipher.DECRYPT_MODE, secretKey)
Base64.getDecoder().run { cipher.doFinal(decode(input)) }
} catch (e: InvalidKeyException) {
throw e
}
return String(output)
}
fun encryptToByteArray(bytes: ByteArray, key: String) = encrypt(String(bytes), key).toByteArray()
fun decryptToByteArray(bytes: ByteArray, key: String) = decrypt(String(bytes), key).toByteArray()
}
My CBC implementation:
object AesCbcOwn {
fun encrypt(plainText: String, key: String): String {
val iv = key.take(16).toByteArray()
val blocks = plainText.chunked(16).map { it.toByteArray() }
val encryptedBytes = with(blocks.iterator()) {
generateSequence(
AesEcb.encryptToByteArray(iv xor next(), key)
) {
try {
AesEcb.encryptToByteArray(it xor next(), key)
} catch (e: NoSuchElementException) {
null
}
}
}
return encryptedBytes.joinToString("") { String(it) }
}
fun decrypt(encryptedText: String, key: String): String {
val iv = key.take(16).toByteArray()
val encryptedBlocks = encryptedText.chunked(16).map { it.toByteArray() }
val decryptedBytes = with(encryptedBlocks.iterator()) {
generateSequence(
AesEcb.decryptToByteArray(next(), key) xor iv
) {
try {
AesEcb.decryptToByteArray(next(), key) xor it
} catch (e: NoSuchElementException) {
null
}
}
}
return decryptedBytes.joinToString("") { String(it) }
}
private infix fun ByteArray.xor(other: ByteArray) =
this.zip(other) { thisByte, otherByte -> thisByte xor otherByte }.toByteArray()
}
The ECB method you've programmed performs padding. That should not be happening: the CBC plaintext needs to be padded, not the blocks fed to the AES cipher. Currently the ECB method returns two blocks instead of one.
The vector is not updated. The IV (Initialization Vector) is only XOR-ed with the initial plaintext block, after than the last ciphertext block needs to be XOR-ed with the next plaintext block. In other words, the ciphertext block becomes the next vector.