Search code examples
flutterencryptionaesbouncycastlespongycastle

Determining attributes of flutter encrypt.dart


I am using encrypt.dart to AES encrypt a string ("text") based on a 32 digit password ("password") as follows:

encryptPass(String text, String password) {
   final key = getKey(password);
   final iv = encrypt.IV.fromLength(16);
   final encrypter = encrypt.Encrypter(encrypt.AES(key)); //Uses AES/SIC/PKCS7
   final e = encrypter.encrypt(text, iv: iv);
   String encryptedString = e.base64.toString();
   return encryptedString;
}

 getKey(String masterPass) {
   String keyString = masterPass;
   if (keyString.length < 32) {
     int count = 32 - keyString.length;
     for (var i = 0; i < count; i++) {
       keyString += ".";
     }
   }
   final keyReturn = encrypt.Key.fromUtf8(keyString);
   return keyReturn;
 }

Side note: This works, but it produces the same value every time for a given input string, even though my "iv" and "salt" are supposedly random. How does this happen?

MAIN PROBLEM: I am trying to recreate this process using spongy castle in kotlin. The problem is that I don't know certain important attributes of the encrypt.dart AES functions. What values are used for:

salt length: 16, 32, 128, 256?? ("desiredKeyLength" var in encrypted.dart. not specified anywhere) iteration count: (I think this is 100, but I am not certain.) Secret Key algorithm: I assumed PBKDF2WithHmacSHA1 based on "final pbkdf2" of encrypted.dart. key length: ?

Here is my current attempt at spongy castle implementation for reference:

fun encryptAESBasic(input: String, password: String): String {
    Security.insertProviderAt(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
    val masterpw = password.toCharArray()
    val random = SecureRandom()
    val salt = ByteArray(256)
    random.nextBytes(salt)
    val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
    val spec: KeySpec = PBEKeySpec(masterpw, salt, 100, 128)
    val tmp: SecretKey = factory.generateSecret(spec)
    val key: SecretKey = tmp
    val cipher = Cipher.getInstance("AES/SIC/PKCS7PADDING", "SC")
    val iv = ByteArray(16)
    SecureRandom().nextBytes(iv)
    cipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv))
    val cipherText: ByteArray = cipher.doFinal(input.toByteArray())
    return cipherText.toString()
}

Solution

  • The Dart code uses a zero IV (an IV consisting of only 0x00 values), which is why always the same ciphertext is generated.

    As you already figured out, the Dart code applies the SIC mode and PKCS7 padding by default. The SIC mode is another name for the CTR mode, which is a stream cipher mode and therefore does not require any padding. The PKCS7 padding used in the Dart code is therefore unnecessary.

    Note that using CTR mode in conjunction with a static IV (such as a zero IV) is a fatal bug and in general extremely insecure (s. here).

    As key derivation, the Dart code pads the password with . until the key size is 32 bytes, which is required for AES-256. This key derivation is also very insecure. When using a password, a reliable key derivation function such as PBKDF2 should always be used (as in the Kotlin Code).

    The Dart code should therefore be revised and made more secure before porting to Kotlin. This requires the following changes:

    • A random IV is to be generated for each encryption.
    • PKCS7 padding should be disabled.
    • The code does not check the authenticity/integrity of the ciphertext. An additional authentication tag (MAC) must be applied for this purpose. It is recommended to switch from CTR to GCM mode, which is based on CTR mode but includes data authenticity/integrity in addition to confidentiality (authenticated encryption) and generates the tag implicitly.
    • A secure key derivation (e.g. PBKDF2, see Kotlin code) must be used. In combination with this, a random salt is to be generated for each key derivation (s. also the other answer).
    • Salt and IV (both not secret), as well as the tag are to be concatenated with the ciphertext (salt|IV|ciphertext|tag). Note that for GCM, many libraries perform concatenation of ciphertext and tag implicitly.

    Of course - from a technical point of view - the Dart code can be ported to Kotlin, e.g.

    fun encryptPass(text: String, password: String): String {
        val secretKeySpec = SecretKeySpec(getKey(password), "AES")                              // Apply a reliable key derivation function (getKey() is insecure)
        val cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING")                                 // Disable padding (CTR doesn't require padding)
        val iv = ByteArray(16)                                                                  // Generate random IV (CTR with static IV is extremely insecure)
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, IvParameterSpec(iv))
        val cipherText: ByteArray = cipher.doFinal(text.toByteArray(Charset.forName("UTF-8")))  // Authenticity/integrity missing
        return Base64.encodeToString(cipherText, Base64.DEFAULT);                               // Concatenation of salt, IV, ciphertext and authentication tag missing
    }
    
    fun getKey(masterPass: String): ByteArray {
        return masterPass.padEnd(32, '.').toByteArray(Charset.forName("UTF-8"))
    }
    

    which gives the same result as the Dart code (the use of SpongyCastle is not necessary), but this code should not be used for security reasons.