I AES/GCM/NoPadding encrypted videos inside internal app storage and I want to play them using ExoPlayer.
Nothing really worked for me. I have tried:
And these in various versions.
Am I missing something?
Here is some of the code:
My PlayerSetup
fun setupPlayer(photoId: Int) = viewModelScope.launch(Dispatchers.IO) {
val photo = photoRepository.get(photoId)
player = SimpleExoPlayer.Builder(app)
.setMediaSourceFactory(createMediaSourceFactory())
.build()
player!!.apply {
onMain {
setMediaItem(createMediaItem(photo))
prepare()
playWhenReady = true
}
}
}
private fun createMediaSourceFactory(): MediaSourceFactory {
val aesDataSource = AesCipherDataSource(encryptionManager.encodedKey, FileDataSource())
val factory = DataSource.Factory {
aesDataSource
}
return ProgressiveMediaSource.Factory(factory)
}
private fun createMediaItem(photo: Photo): MediaItem {
val uri = Uri.fromFile(app.getFileStreamPath(photo.internalFileName).canonicalFile)
return MediaItem.Builder()
.setMimeType(photo.type.mimeType)
.setUri(uri)
.build()
}
And my custom DataSource try (not used in code above):
class AesGCMDataSource(
private val upstream: DataSource,
private val encryptionManager: EncryptionManager
) : DataSource {
private var cipherInputStream: CipherInputStream? = null
override fun addTransferListener(transferListener: TransferListener) {
upstream.addTransferListener(transferListener)
}
override fun open(dataSpec: DataSpec): Long {
val inputStream = DataSourceInputStream(upstream, dataSpec)
cipherInputStream = encryptionManager.createCipherInputStream(inputStream)
inputStream.open()
return C.LENGTH_UNSET.toLong()
}
override fun read(target: ByteArray, offset: Int, length: Int): Int {
Assertions.checkNotNull<Any>(cipherInputStream)
val read = cipherInputStream!!.read(target, offset, length)
return if (read < 0) {
C.RESULT_END_OF_INPUT
} else {
read
}
}
override fun getResponseHeaders(): MutableMap<String, MutableList<String>> {
return upstream.responseHeaders
}
override fun getUri(): Uri? = upstream.uri
override fun close() = upstream.close()
}
I found a solution for this:
Create this DataSource:
class AesDataSource(
private val cipher: Cipher
) : DataSource {
private var inputStream: CipherInputStream? = null
private lateinit var uri: Uri
override fun open(dataSpec: DataSpec): Long {
uri = dataSpec.uri
uri.path ?: return 0
val file = File(uri.path!!).canonicalFile
inputStream = CipherInputStream(file.inputStream(), cipher)
if (dataSpec.position != 0L) {
inputStream?.forceSkip(dataSpec.position) // Needed for skipping
}
return dataSpec.length
}
@Throws(IOException::class)
override fun read(target: ByteArray, offset: Int, length: Int): Int =
if (length == 0) {
0
} else {
inputStream?.read(target, offset, length) ?: 0
}
override fun addTransferListener(transferListener: TransferListener) {}
override fun getUri(): Uri = uri
override fun close() {
inputStream?.close()
}
}
The DataSource uses this Extension function:
/**
* Skip bytes by reading them to a specific point.
* This is needed in GCM because the Authorisation Tag wont match when bytes are really skipped.
*/
fun CipherInputStream.forceSkip(bytesToSkip: Long): Long {
var processedBytes = 0L
while (processedBytes < bytesToSkip) {
read()
processedBytes++
}
return processedBytes
}
Use it like this:
fun setupPlayer(file: Uri) {
player = SimpleExoPlayer.Builder(app)
.setMediaSourceFactory(createMediaSourceFactory())
.build()
.apply {
setMediaItem(createMediaItem(file))
prepare()
playWhenReady = true
}
}
private fun createMediaSourceFactory(): MediaSourceFactory {
val aesDataSource = AesDataSource(yourCipher) // Use your Cipher instance
val factory = DataSource.Factory {
aesDataSource
}
return ProgressiveMediaSource.Factory(factory)
}
private fun createMediaItem(file: Uri): MediaItem {
return MediaItem.fromUri(uri)
}