Reputation: 5105
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:
Upvotes: 2
Views: 5835
Reputation: 173
I know it might be too late to answer this question, but for the sake of others who might be struggling with seek issues, here is the full implementation of ExoPlayer DataSource which plays an encrypted local file(either in ECB or other modes) and fully supports backward and forward seek as well.
I also wrote an Article in Medium in which I explained the nitty-gritty details of it as well as elaborated on how to support playing an encrypted file from a remote server on the fly.
To have your encrypted local file played by ExoPlayer supporing backward and foreward seek, please take the following steps:
First: Implement a custom DataSource which takes your key
and iv
(if you are using CTR or other modes with IvSpecParameter
) in the constructor as follows:
class FileCipherEncryptedDataSource(private val key: ByteArray, private val iv: ByteArray?) :
DataSource {
private var encryptedFileStream: FileInputStream? = null
private var cipherInputStream: FileCipherInputStream? = null
private var bytesToRead: Long = 0
private var bytesRead: Int = 0
private var isOpen = false
private var dataSpec: DataSpec? = null
@Throws(IOException::class)
override fun open(dataSpec: DataSpec): Long {
this.dataSpec = dataSpec
if (isOpen) return bytesToRead
try {
setupCipherInputStream()
cipherInputStream?.forceSkip(dataSpec.position)
if (dataSpec.length != C.LENGTH_UNSET.toLong()) {
bytesToRead = dataSpec.length
return bytesToRead
}
if (bytesToRead == Int.MAX_VALUE.toLong()) {
bytesToRead = C.LENGTH_UNSET.toLong()
return bytesToRead
}
bytesToRead = cipherInputStream!!.available().toLong()
} catch (e: IOException) {
throw IOException(e)
}
isOpen = true
return bytesToRead
}
private fun setupCipherInputStream() {
val path = uri?.path ?: throw RuntimeException("Path can NOT be empty!")
encryptedFileStream = File(path).inputStream()
val keySpec = SecretKeySpec(
key, // use you won byte array key
"AES", //use your own alg.
)
// if you are using ECB mode, it is not required.
val ivParameterSpec = IvParameterSpec(
iv, // ues you own iv key
)
val cipher = Cipher.getInstance(
"AES/ECB/PKCS5Padding" // use your own mode.
)
cipherInputStream = FileCipherInputStream(
encryptedFileStream!!,
cipher,
keySpec,
ivParameterSpec,// if you are using ECB, remove this line.
)
}
@Throws(IOException::class)
override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int {
if (bytesToRead == 0L) {
return C.RESULT_END_OF_INPUT
}
bytesRead = try {
cipherInputStream!!.read(buffer, offset, readLength)
} catch (e: IOException) {
throw IOException(e)
}
if (bytesRead < 0) {
if (bytesToRead != C.LENGTH_UNSET.toLong())
throw IOException(EOFException())
return C.RESULT_END_OF_INPUT
}
if (bytesToRead != C.LENGTH_UNSET.toLong())
bytesToRead -= bytesRead
return bytesRead
}
override fun addTransferListener(transferListener: TransferListener) {}
override fun getUri(): Uri? = dataSpec?.uri
@Throws(IOException::class)
override fun close() {
try {
encryptedFileStream?.close()
cipherInputStream?.close()
} catch (e: IOException) {
throw IOException(e)
} finally {
if (isOpen) {
isOpen = false
}
}
}
}
Second: Impelement a custom CipherInputStream which has forceSkip
function replacing the original skip
function sice it is not going to work in our case:
class FileCipherInputStream(
private val upstream: FileInputStream,
private var cipher: Cipher,
private val secretKeySpec: SecretKeySpec,
private val ivParameterSpec: IvParameterSpec? = null,
) : CipherInputStream(upstream, cipher) {
// in case of ECB mode, use this method
fun forceSkip(bytesToSkip: Long): Long {
val skipOverFlow = bytesToSkip % cipher.blockSize
val skipBlockPosition = bytesToSkip - skipOverFlow
try {
if (skipBlockPosition <= 0) {
initCipher()
return 0L
}
var upstreamSkipped = upstream.skip(skipBlockPosition)
while (upstreamSkipped < skipBlockPosition) {
upstream.read()
upstreamSkipped++
}
val cipherBlockArray = ByteArray(cipher.blockSize)
upstream.read(cipherBlockArray)
initCipher()
val cipherSkipped = skip(skipBlockPosition)
val negligibleBytes = ByteArray(skipOverFlow.toInt())
read(negligibleBytes)
return cipherSkipped
} catch (e: Exception) {
e.printStackTrace()
return 0
}
}
// in case of other modes with IV, use this method
fun forceIvSkip(bytesToSkip: Long): Long {
val skipped: Long = upstream.skip(bytesToSkip)
try {
val skipOverFlow = (bytesToSkip % cipher.blockSize).toInt()
val skipBlockPosition = bytesToSkip - skipOverFlow
val blocksNumber = skipBlockPosition / cipher.blockSize
val ivOffset = BigInteger(1, ivParameterSpec!!.iv).add(
BigInteger.valueOf(blocksNumber)
)
val ivOffsetBytes = ivOffset.toByteArray()
val skippedIvSpec = if (ivOffsetBytes.size < cipher.blockSize) {
val resizedIvOffsetBytes = ByteArray(cipher.blockSize)
System.arraycopy(
ivOffsetBytes,
0,
resizedIvOffsetBytes,
cipher.blockSize - ivOffsetBytes.size,
ivOffsetBytes.size
)
IvParameterSpec(resizedIvOffsetBytes)
} else {
IvParameterSpec(
ivOffsetBytes,
ivOffsetBytes.size - cipher.blockSize,
cipher.blockSize
)
}
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, skippedIvSpec)
val skipBuffer = ByteArray(skipOverFlow)
cipher.update(skipBuffer, 0, skipOverFlow, skipBuffer)
} catch (e: java.lang.Exception) {
e.printStackTrace()
return 0
}
return skipped
}
@Throws(IOException::class)
override fun available(): Int {
return upstream.available()
}
}
Third: Implement a custom DataSource.Factory
which instantiates and returns the previous FileCipherEncryptedDataSource
:
class EncryptedDataSourceFactory(
private val key: ByteArray,
private val iv: ByteArray,
) : DataSource.Factory {
override fun createDataSource(): DataSource =
FileCipherEncryptedDataSource(key, iv)
}
Fourth an the final: Inject your DataSource.Factory
inside MediaSourceFactory
of ExoPlayer:
private fun setStreamingSourceFactory(key: ByteArray, iv: ByteArray) {
val mediaItem = MediaItem.Builder()
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)// or BASE_TYPE_VIDEO
.setUri(url) // your file's url
.build()
val dataSourceFactory = EncryptedDataSourceFactory(key, iv)
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(mediaItem)
exoPlayer?.setMediaSource(mediaSource)
exoPlayer?.prepare()
}
You can also find the full implementation in my GitHup page.
Upvotes: 0
Reputation: 5105
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()
}
}
Upvotes: 6