Search code examples
androidencryptionaesexoplayer2.x

Android + exoplayer: play AES encrypted videos, locally


On a linux box, I have an MP4 video that is encrypted with openssl:

openssl enc -aes-128-ecb -a -in video.mp4 -out video.enc -K `cat aes.key`

Please note, this is an exercise, the strength of the algo doesn't matter.

That file is sent to an Android app, and I'm trying to play it using ExoPlayer.

I've done some tests beforehand with text files to make sure the decryption was working properly

fun decrypt(key: ByteArray, data: ByteArray): ByteArray {
    val spec = SecretKeySpec(key, "AES")
    val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
    cipher.init(Cipher.DECRYPT_MODE, spec)
    globalCipher.init(Cipher.DECRYPT_MODE, spec)
    return cipher.doFinal(data)
}

Regarding ExoPlayer, it's a bit overwhelming between AesCipherDataSource, AesCipherDataSink, SimpleCache etc. I was not able to put together a simple way to play the video.

fun playVideo() {
    val player = SimpleExoPlayer.Builder(this).build()
    playerView.player = player

    val dataSourceFactory = DefaultDataSourceFactory? // <-- what's the factory?
    val dataSource = AesCipherDataSource(globalCipher, ?) // <-- what's the data source?
    val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
    try {
        val uri = Uri.fromFile(File(path, "video.enc"))
        val videoSource =
                ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory, null, null)
        player.prepare(videoSource)
        player.playWhenReady = true
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

So questions:

  1. how to achieve playing this encrypted video locally?
  2. what would need to change once that video is served over HTTP? (need to add a manifest? headers?)

Solution

  • here's the solution. Might need some adjustments to handle skipping frames, fast forward etc, but this plays an AES/ECB/PKCS5Padding encrypted video

    class EncryptedDataSourceFactory(
        private val key: String
    ) : DataSource.Factory {
        override fun createDataSource(): EncryptedDataSource =
            EncryptedDataSource(key)
    }
    
    class EncryptedDataSource(private val key: String) : DataSource {
        private var inputStream: CipherInputStream? = null
        private lateinit var uri: Uri
    
        override fun addTransferListener(transferListener: TransferListener) {}
    
        override fun open(dataSpec: DataSpec): Long {
            uri = dataSpec.uri
            try {
                val file = File(uri.path)
                val skeySpec = SecretKeySpec(key.toByteArray(), KeyProperties.KEY_ALGORITHM_AES)
                val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
                cipher.init(Cipher.DECRYPT_MODE, skeySpec)
                inputStream = CipherInputStream(file.inputStream(), cipher)
            } catch (e: Exception) {
                
            }
            return dataSpec.length
        }
    
        @Throws(IOException::class)
        override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int =
            if (readLength == 0) {
                0
            } else {
                inputStream?.read(buffer, offset, readLength) ?: 0
            }
    
        override fun getUri(): Uri? =
            uri
    
        @Throws(IOException::class)
        override fun close() {
            inputStream?.close()
        }
    }
    
        private fun playVideo(key: String) {
            val player = SimpleExoPlayer.Builder(this).build()
            playerView.player = player
    
            val dataSourceFactory: DataSource.Factory = EncryptedDataSourceFactory(key)
            val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
            try {
                val uri = Uri.fromFile(video)
                val videoSource: MediaSource = ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory, null, null)
                player.prepare(videoSource)
                player.playWhenReady = true
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }