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)
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)
}