androidencryptionmemory-managementout-of-memoryaes

Android AES Large-File Encryption/Decryption Failed with Out Of Memory


I'm trying to encrypt large files in Android with AES but failed with an error of out of memory.

Here's the code I'm using:

import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import timber.log.Timber
import java.io.IOException
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

suspend fun encryptFile(
    context: Context,
    file: DocumentFile,
    secretByte: ByteArray,
    iv: ByteArray
) {
    try {
        val bufferSize = 1024 * 1024
        val secretKeySpec = SecretKeySpec(secretByte, "AES")

        val encryptedFileUri = createEncryptedFileUri(file)
            ?: throw IOException("Failed to create URI for encrypted file")
        
        context.contentResolver.openOutputStream(encryptedFileUri)?.use { outputStream ->


            context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
                val buffer = ByteArray(bufferSize)
                var bytesRead: Int

                val cipher = Cipher.getInstance("AES/GCM/NoPadding")
                val gcmSpec = GCMParameterSpec(128, iv)
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmSpec)

                CipherOutputStream(outputStream, cipher).use { cipherOutputStream ->
                    while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                        cipherOutputStream.write(buffer, 0, bytesRead)
                    }
                    cipherOutputStream.flush()
                }

            } ?: throw IOException("Failed to open input stream for file: ${file.uri}")
        } ?: throw IOException("Failed to open output stream for file: $encryptedFileUri")

        if (!file.delete()) {
            Timber.e("Failed to delete original file: ${file.uri}")
        }
    } catch (e: Exception) {
        e.printStackTrace()
        Timber.e("Error during file encryption: ${e.message}")
    }
}

private fun createEncryptedFileUri(file: DocumentFile): Uri? {
    val parentDirectory = file.parentFile ?: throw IllegalArgumentException("No parent directory")
    return parentDirectory.createFile("*/*", file.name + ".aesEncr")?.uri
}

Error I'm getting:

java.lang.OutOfMemoryError: Failed to allocate a 266338320 byte allocation with 25165824 free bytes and 121MB until OOM, target footprint 166187728, growth limit 268435456
    at com.android.org.conscrypt.OpenSSLAeadCipher.expand(OpenSSLAeadCipher.java:127)
    at com.android.org.conscrypt.OpenSSLAeadCipher.updateInternal(OpenSSLAeadCipher.java:300)
    at com.android.org.conscrypt.OpenSSLCipher.engineUpdate(OpenSSLCipher.java:332)
    at javax.crypto.Cipher.update(Cipher.java:1741)
    at javax.crypto.CipherOutputStream.write(CipherOutputStream.java:158)                                                                                                   
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
    at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
    at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Suppressed: java.lang.OutOfMemoryError: Failed to allocate a 132120608 byte allocation with 25165824 free bytes and 121MB until OOM, target footprint 166188144, growth limit 268435456
    at com.android.org.conscrypt.OpenSSLCipher.engineDoFinal(OpenSSLCipher.java:359)
    at javax.crypto.Cipher.doFinal(Cipher.java:1957)

Solution

  • I'm able to fix this using the below code.
    Instead of using CipherOutputStream, use the buffer as independent bytes to encrypt

     fun encryptFile(
            context: Context,
            file: DocumentFile,
            secretByte: ByteArray,
            iv: ByteArray
        ) {
            try {
                val bufferSize = 1024 * 1024
                val encryptedFileUri = createEncryptedFileUri(file)
                    ?: throw IOException("Failed to create URI for encrypted file")
        
                context.contentResolver.openInputStream(file.uri).use { inputStream ->
                    context.contentResolver.openOutputStream(encryptedFileUri).use { outputStream ->
                        val buffer = ByteArray(bufferSize)
                        var bytesRead: Int
                        while (inputStream!!.read(buffer).also { bytesRead = it } != -1) {
                            val resultByte = encryptByte(buffer, secretByte, iv = iv)
                            outputStream?.write(resultByte, 0, resultByte.size)
                        }
                    }
                }
        
                if (!file.delete()) {
                    Timber.e("Failed to delete original file: ${file.uri}")
                }
        
            } catch (e: Exception) {
                e.printStackTrace()
                Timber.e("Error during file encryption: ${e.message}")
            }
        }
        
        
        fun encryptByte(byte: ByteArray, key: ByteArray, iv: ByteArray): ByteArray {
            val cipher = Cipher.getInstance("AES/GCM/NoPadding")
            val gcmSpec = GCMParameterSpec(128, iv)
            val secretKey = SecretKeySpec(key, "AES")
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)
            return cipher.doFinal(byte)
        }