Search code examples
javaandroidkotlinencryptionaes

Android Kotlin AES/GCM Decryption javax.crypto.AEADBadTagException


I'm trying to decrypt a string encoded in AES with GCM mode by another application.

When I pass my test string to my decrypt method, I get this fatal execption :

javax.crypto.AEADBadTagException: error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT
at java.lang.reflect.Constructor.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
at com.android.org.conscrypt.OpenSSLCipher$EVP_AEAD.throwAEADBadTagExceptionIfAvailable(OpenSSLCipher.java:1216) 
at com.android.org.conscrypt.OpenSSLCipher$EVP_AEAD.doFinalInternal(OpenSSLCipher.java:1245)
at com.android.org.conscrypt.OpenSSLCipher.engineDoFinal(OpenSSLCipher.java:363)
at javax.crypto.Cipher.doFinal(Cipher.java:2055)
at fr.lundimatin.core.utils.AESEncryptionUtil$Companion.decrypt(AESEncryptionUtil.kt:40)

I'm using one method to convert the encryption key string I have to a SecretKey object and the other to decrypt a test string, see my code :

const val IV_LENGTH = 16

fun getSecretKey(key: String) : SecretKey
{
    val decodedKey: ByteArray = Base64.getDecoder().decode(key)
    val originalKey: SecretKey = SecretKeySpec(decodedKey, 0, decodedKey.size, "AES")
    return originalKey
}

//message : "MjY2NjI4QkI0NDYwRTExMxPQoQrBDTPMWTTIAhJ5QH4cKjzmWIIfW0mBv3zIC7yHJr5nqfmmEYk34BW9ag=="
fun decrypt(message: String): String {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val ivString = message.substring(0, IV_LENGTH)
    val ivEncrypted = Base64.getDecoder().decode(ivString)
    val spec = GCMParameterSpec(128, ivEncrypted)
    cipher.init(Cipher.DECRYPT_MODE, getEncryptionKey(), spec)
    val messageEncrypted = Base64.getDecoder().decode(message.substring(IV_LENGTH))
    val decryptedBytes = cipher.doFinal(messageEncrypted)
    return String(decryptedBytes, StandardCharsets.UTF_8) // 400172870-327468066356882-CHE
}

fun getEncryptionKey() : SecretKey {
    return getSecretKey("k8h1k9LyiHgcStf/crHlkw==")
}

Unfortunately the message is not very helpful and I havent found a solution on StackOverflow that would apply to my case.

Edit : added my test data within the code.


Solution

  • The length of the encrypted data and the absence of an explicit IV and tag are an indication that a concatenation of IV, ciphertext and tag is most likely used (assuming the usual order IV|ciphertext|tag, which is just a guess).
    Based on the plaintext length of 29 bytes (which also means a ciphertext length of 29 bytes), it can be concluded that a 16 bytes IV (and not the IV length of 12 bytes recommended for GCM) and a 16 bytes tag were used:

    Base64 MjY2NjI4QkI0NDYwRTExMxPQoQrBDTPMWTTIAhJ5QH4cKjzmWIIfW0mBv3zIC7yHJr5nqfmmEYk34BW9ag==
    
           IV                               ciphertext                                                 tag 
    Hex    32363636323842423434363045313133 13d0a10ac10d33cc5934c8021279407e1c2a3ce658821f5b4981bf7cc8 0bbc8726be67a9f9a6118937e015bd6a  
    

    Nevertheless, decryption with this data fails. Since the encryption code is not known, one can only try to guess possible reasons for this.
    One possibility is that the key does not have to be Base64 decoded, but UTF-8 encoded (i.e. AES-192 is used instead of AES-128).
    With this change, the decryption is successful, as can be seen here on CyberChef.


    Now that the encryption logic has been deduced with the help of the test data, the code can be adapted:

    • UTF-8 encode the key
    • Base64 decode the encoded data before IV and ciphertext/tag are separated (use a separate parameter for the IV length, as IV and tag are different things)
    • UTF-8 decode the decoded data
    import javax.crypto.Cipher
    import javax.crypto.spec.GCMParameterSpec
    import javax.crypto.spec.SecretKeySpec
    import javax.crypto.SecretKey
    import java.util.Base64
    import java.nio.charset.StandardCharsets
    
    fun main() {
      println(decrypt("MjY2NjI4QkI0NDYwRTExMxPQoQrBDTPMWTTIAhJ5QH4cKjzmWIIfW0mBv3zIC7yHJr5nqfmmEYk34BW9ag=="))
    }
    
    const val TAG_LENGTH = 16
    const val IV_LENGTH = 16
    
    fun getSecretKey(key: String) : SecretKey {
        val decodedKey: ByteArray = key.toByteArray(StandardCharsets.UTF_8) // Fix 1: UTF-8 encode key
        val originalKey: SecretKey = SecretKeySpec(decodedKey, 0, decodedKey.size, "AES")
        return originalKey
    }
    
    fun decrypt(messageB64: String): String {
        val message  = Base64.getDecoder().decode(messageB64) // Fix 2: Base64 decode before separating IV and ciphertext/tag
        val iv = message.take(IV_LENGTH).toByteArray() 
        val ciphertext = message.drop(IV_LENGTH).toByteArray() 
        val spec = GCMParameterSpec(TAG_LENGTH * 8, iv)
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.DECRYPT_MODE, getEncryptionKey(), spec) 
        val decryptedBytes = cipher.doFinal(ciphertext)
        return String(decryptedBytes, StandardCharsets.UTF_8) // Fix 3: UTF-8 decode data
    }
    
    fun getEncryptionKey() : SecretKey {
        return getSecretKey("k8h1k9LyiHgcStf/crHlkw==")
    }
    

    Security: Although the encryption code is not known, there are hints of vulnerabilities/inefficiencies:

    • For performance and efficiency reasons, a 12 bytes IV should be used if there are no good reasons not to.
    • The key should be Base64 decoded (i.e. AES-128 should be used). If AES-192 or AES-256 is to be applied, a random key of the required size should be applied (and not a 16 bytes key expanded to 24 bytes by Base64 encoding).
    • The IV appears to be UTF-8 encodable, i.e. not a random byte sequence. In this case it is important to ensure that there is no reuse of key/IV pairs as this is a serious vulnerability for CTR based algorithms such as GCM.