I'm trying to encrypt data using AES in both JavaScript (with CryptoJS) and Java/Scala, but the Java encryption cannot be decrypted by a service while the CryptoJS version works correctly.
Here's my working CryptoJS code:
const encrypted = CryptoJS.AES.encrypt(data, secret);
And here's my Java/Scala implementation that's not working:
val sha256 = java.security.MessageDigest.getInstance("SHA-256")
val key = new SecretKeySpec(sha256.digest(secret.getBytes("UTF-8")), "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val iv = new Array[Byte](16)
new java.security.SecureRandom().nextBytes(iv)
val ivSpec = new IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec)
val encrypted = cipher.doFinal(data.getBytes("UTF-8"))
val combined = iv ++ encrypted
Base64.getEncoder.encodeToString(combined)
Expected Behavior:
Both implementations should produce encrypted data that the service can decrypt
Actual Behavior:
CryptoJS encryption works and can be decrypted Java/Scala encryption cannot be decrypted by the service
Questions
CryptoJS.AES.encrypt()
?If the key material is passed as a string in CryptoJS, CryptoJS generates a random 8 bytes salt during encryption and derives a 32 bytes key and 16 bytes IV using the OpenSSL proprietary key derivation function EVP_BytesToKey()
. For this, an iteration count of 1 and the digest MD5 is applied.
This key derivation is missing in your Scala Code. You can find various implementations of EVP_BytesToKey()
on the web. Since not all of the functionality is required for the key/IV derivation in the context of CryptoJS, a lightweight implementation may also be enough, e.g:
def getKeyIv(password: Array[Byte], salt: Array[Byte]): (Array[Byte], Array[Byte]) = {
var key, iv, last = Array[Byte]()
val md = MessageDigest.getInstance("MD5")
while (key.length < 32) {
last = md.digest(last ++ password ++ salt)
key = key ++ last
}
iv = md.digest(last ++ password ++ salt)
(key, iv)
}
CryptoJS.AES.encrypt()
returns the result as a CipherParams
object whose toString()
function is overloaded to return the result in the Base64 encoded OpenSSL format:
val ciphertextOpenSSLFrmt = "Salted__".getBytes(StandardCharsets.UTF_8) ++ salt ++ ciphertext
Security: Note that EVP_BytesToKey()
is considered insecure nowadays (PBKDF2, which is also supported by CryptoJS, is more secure).
Test:
Due to the random salt, a different key and IV and therefore a different ciphertext are generated for each encryption. The implementation can therefore not be checked by comparing the results, but by checking whether decryption, e.g. with CryptoJS, is successful.
Sample implementation for encryption:
import java.security.{SecureRandom, MessageDigest}
import javax.crypto.Cipher
import javax.crypto.spec.{IvParameterSpec, SecretKeySpec}
import java.nio.charset.StandardCharsets
import java.util.Base64
...
val plaintext: Array[Byte] = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8)
val password: Array[Byte] = "my password".getBytes(StandardCharsets.UTF_8)
val secureRandom = new SecureRandom() // create random salt
val salt: Array[Byte] = new Array[Byte](8)
secureRandom.nextBytes(salt)
val (key, iv) = getKeyIv(password, salt) // derive key and IV
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") // encrypt
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv))
val ciphertext = cipher.doFinal(plaintext)
val ciphertextOpenSSLFrmt = "Salted__".getBytes(StandardCharsets.UTF_8) ++ salt ++ ciphertext // concatenate result
println(Base64.getEncoder.encodeToString(ciphertextOpenSSLFrmt)) // sample output: U2FsdGVkX1/joJPOllaIZiV+ehbDM5KP4IAn/DtCl3Uvfi+8BA8kyznFD+gZJfjFMhz7dD2ke94BR9mvrKTF0Q==
Decryption with CryptoJS:
const decrypted = CryptoJS.AES.decrypt("U2FsdGVkX1/joJPOllaIZiV+ehbDM5KP4IAn/DtCl3Uvfi+8BA8kyznFD+gZJfjFMhz7dD2ke94BR9mvrKTF0Q==", "my password")
console.log(decrypted.toString(CryptoJS.enc.Utf8))
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>