javaflutterkotlindartcryptography

Decrypting Dart AES encrypted data in Kotlin results in BadPaddingException


I have a Dart application that encrypts a string using AES encryption, and I'm trying to decrypt this string in a Kotlin application. However, when I attempt to decrypt, I encounter a javax.crypto.BadPaddingException. I cannot modify the Dart code, so I need to adjust the Kotlin code to correctly decrypt the data.

Here's the relevant part of the Dart encryption code:

import 'dart:convert';
import 'dart:typed_data';

import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart';

class CryptoService {
  static String encryptString(String input) {
    const keyString = "ABCdefg1234!!";
    final keyBytes = sha256.convert(utf8.encode(keyString)).bytes;
    final key = Key(Uint8List.fromList(keyBytes));
    final iv = IV.fromSecureRandom(16);
    final encrypter = Encrypter(AES(key));

    final encrypted = encrypter.encrypt(input, iv: iv);
    final combined = iv.bytes + encrypted.bytes;

    return base64Url.encode(combined);
  }

  static String decryptString(String encoded) {
    const keyString = "ABCdefg1234!!";
    final keyBytes = sha256.convert(utf8.encode(keyString)).bytes;
    final key = Key(Uint8List.fromList(keyBytes));
    final encrypter = Encrypter(AES(key));

    final combinedBytes = base64Url.decode(encoded);
    final ivBytes = combinedBytes.sublist(0, 16);
    final encryptedBytes = combinedBytes.sublist(16);

    final iv = IV(Uint8List.fromList(ivBytes));
    final encrypted = Encrypted(encryptedBytes);
    final decrypted = encrypter.decrypt(encrypted, iv: iv);

    return decrypted;
  }
}

void main() {
  print(CryptoService.encryptString("abcdef"));
}

And here's the Kotlin code that's supposed to decrypt it:

import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.*
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom

object CryptoService {
    private const val keyString = "ABCdefg1234!!"

    private fun generateKey(): SecretKey {
        val keyBytes = MessageDigest.getInstance("SHA-256").digest(keyString.toByteArray(StandardCharsets.UTF_8))
        return SecretKeySpec(keyBytes, "AES")
    }

    fun encryptString(input: String): String {
        val key = generateKey()
        val iv = ByteArray(16).apply { SecureRandom().nextBytes(this) }
        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
            init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv))
        }
        val encryptedBytes = cipher.doFinal(input.toByteArray(StandardCharsets.UTF_8))
        val combined = iv + encryptedBytes
        return Base64.getUrlEncoder().encodeToString(combined)
    }

    fun decryptString(encoded: String): String {
        val key = generateKey()
        val combinedBytes = Base64.getUrlDecoder().decode(encoded)
        val iv = combinedBytes.sliceArray(0 until 16)
        val encryptedBytes = combinedBytes.sliceArray(16 until combinedBytes.size)
        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
            init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
        }
        val decryptedBytes = cipher.doFinal(encryptedBytes)
        return String(decryptedBytes, StandardCharsets.UTF_8)
    }
}


fun main() {
   println(CryptoService.decryptString("FzaO3XI3ZVtUDAdM1So357dhQwg37fh0vzVr6jkPDaY="))

   println("end")
}

The error message I receive is:

Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
 at com.sun.crypto.provider.CipherCore.unpad (:-1) 
 at com.sun.crypto.provider.CipherCore.fillOutputBuffer (:-1) 
 at com.sun.crypto.provider.CipherCore.doFinal (:-1) 

How can I adjust the Kotlin code to properly decrypt the data encrypted by the Dart application?


Solution

  • I've managed to resolve the compatibility issues between Kotlin and Dart encryption by switching to AES/CTR mode and using Bouncy Castle for encryption and decryption in Kotlin. Below is the working solution:

    import java.nio.charset.StandardCharsets
    import java.security.MessageDigest
    import java.util.*
    import javax.crypto.Cipher
    import javax.crypto.KeyGenerator
    import javax.crypto.SecretKey
    import javax.crypto.spec.IvParameterSpec
    import javax.crypto.spec.SecretKeySpec
    import java.security.SecureRandom
    import org.bouncycastle.jce.provider.BouncyCastleProvider
    import javax.crypto.SecretKey
    
    object CryptoService {
        private const val keyString = "ABCdefg1234!!"
    
        init {
            Security.addProvider(BouncyCastleProvider())
        }
    
        private fun generateKey(): SecretKey {
            val keyBytes = MessageDigest.getInstance("SHA-256").digest(keyString.toByteArray(StandardCharsets.UTF_8))
            return SecretKeySpec(keyBytes, "AES")
        }
    
        fun encryptString(input: String): String {
            val key = generateKey()
            val iv = ByteArray(16).apply { SecureRandom().nextBytes(this) }
            val cipher = Cipher.getInstance("AES/CTR/PKCS5Padding", "BC").apply {
                init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv))
            }
            val encryptedBytes = cipher.doFinal(input.toByteArray(StandardCharsets.UTF_8))
            val combined = iv + encryptedBytes
            return Base64.getUrlEncoder().encodeToString(combined)
        }
    
        fun decryptString(encoded: String): String {
            val key = generateKey()
            val combinedBytes = Base64.getUrlDecoder().decode(encoded)
            val iv = combinedBytes.sliceArray(0 until 16)
            val encryptedBytes = combinedBytes.sliceArray(16 until combinedBytes.size)
            val cipher = Cipher.getInstance("AES/CTR/PKCS5Padding", "BC").apply {
                init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
            }
            val decryptedBytes = cipher.doFinal(encryptedBytes)
            return String(decryptedBytes, StandardCharsets.UTF_8)
        }
    }
    
    
    fun main() {
       println(CryptoService.decryptString("FzaO3XI3ZVtUDAdM1So357dhQwg37fh0vzVr6jkPDaY="))
    
       println("end")
    }
    

    This solution uses the AES/CTR mode (Counter Mode), which seems to match more closely with Dart's default encryption behavior. As noted by the commenter dave_thompson_085, Dart appears to use counter mode by default. By using AES/CTR/PKCS5Padding in Kotlin with the Bouncy Castle provider (BC), the results become compatible with Dart's encryption/decryption processes.