How to play an encrypted Video or Audio file from a remote server or a local file on Android (Part 1)
Introduction
Playing an Audio or a Video file can become very challenging on Android when it comes to protecting the media content so that it can’t be reclaimed through the decompilation process. To tackle this issue, a widely used approach is leveraging Advanced Encryption Standard (AES) Encryption Algorithms alongside a private key that is only shared between the server and the client.
In this article, I am going to elaborate on the essentials of how to play an encrypted file on Android using ExoPlayer from either a remote server or a local file. However, the former will be discussed in Part 2 of this article.
Through this essay, I am using Cipher as the originally supported decryptor tool to decrypt an AES/ECB/PKCS5Padding-encrypted file. Nevertheless, other standard AES encryption modes are supported as well with a slight difference.
The source code of this article is also available on my GitHub page.
What is AES Encryption All About?
AES comes with different modes (e.g. ECB/CBC/CTR) and three different paddings (NoPadding/PKCS5Padding/PKCS7Padding) allowing us to make well-secured encryption. Through the encryption process, you need a secret key which will also be used for the decryption process. If you are using other modes rather than ECB you will need an IV alongside the secret key and that’s exactly what makes these modes more secure.
It’s important to note that, as described here, ECB is not recommended for crucial contexts since it’s of less security than the other modes. In addition, using padding can make the decryption process a bit slow especially when skipping. So to have security and performance at the same time, using AES/CTR/NoPadding can be a great option.
What is an ExoPlayer DataSource?
As the document mentions, DataSource
is simply an interface that reads data from URI-identified resources.
ExoPlayer’s upstream package already contains a number of
DataSource
implementations for different use cases. You may want to implement your ownDataSource
class to load data in another way, such as over a custom protocol, using a custom HTTP stack, or from a custom persistent cache.
Accommodating a vast range of applications, ExoPlayer has provided enormous implementations of this Interface allowing us to use them regarding our needs. Take DefaultHttpDataSource as an example, which is used to play from a remote server on the fly. When dealing with specialized media sources or encrypted content that requires a unique data retrieval process, a custom implementation of DataSource is mandatory.
Let’s take a look at the source code of DataSource
, as there are some subjects to discuss about:
At first glance, we can see that 4 main methods should be implemented to serve our intention which are:
Open(DataSpec dataSpec)
which takes the responsibility of opening the file at the beginning. It will also be called when skipping out of the buffered range of ExoPlayer. On the other hand, the DataSpec
includes some valuable information like uri
, position
, length
and so forth, related to the underlying MediaFile. Here we will set up our encrypted InputStream and also init the cipher to decrypt it.
read(byte[] target, int offset, int length)
that its only responsibility is to read the target ByteArray provided by open()
. Consider that, read()
has no idea about the position nor the length of CipherInputStream
as our entire source. Even length
and offset
parameters only refer to the target
ByteArray.
close()
on the other hand, is responsible for closing the previous connection
s or InputStream
s every time a new one is initiated mostly right before a call to open()
function.
getUri()
which simply returns the URI corresponding to our MediaFile.
To play our encrypted local file, we will need to create a new class that extends DataSource
and implements the aforementioned methods the same as follows.
Stream an Encrypted Local MediaFile
To stream an encrypted local media file, we’ll create a custom DataSource called FileCipherEncryptedDataSource
.
This DataSource will be responsible for reading and decrypting the content. Let’s first see how to implement our FileCipherEncryptedDataSource’s open()
function:
Here the bytesToRead
represents the entire number of bytes to be read and is updated in read()
successively. Note the FileCipherInputStram which is a subtype of CipherInputStream and its forceSkip(bytesToRead: Long): Long
function replacing the original CipherInputStream’s sikp()
function since it doesn’t work in our case. The forceSkip
ensures that we skip to a position in the encrypted file that aligns with the block size of the cipher. This is essential to avoid parsing errors when using ExoPlayer. Here is the implementation:
Since read()
function reads the stream in blocks of the same size as cipher.blockSize
we must skip to the nearest position which is divisible by cipher.blockSize
otherwise, ExoPlayer ends up with ERROR_CODE_PARSING_CONTAINER_MALFORMED
error. Also, note that there are 2 different implementations for forceSkip
since ivParameterSpec
needs to stay aligned with the upstream once it’s skipped.
Now it is time to see how read()
of FileCipherEncryptedDataSource is going to be implemented:
And last but not least, is the implementation of close()
:
Also, consider to return the uri
inside getUri()
function and let addTransferListener(transferListener: TransferListener)
be as it is.
Here we have our FileCipherEncryptedDataSource fully implemented and now it just needs to be injected inside a DataSource.Factory
.
Inject DataSource inside the ExoPlayer
Now that we have implemented our FileCipherEncryptedDataSource the easiest part is just remaining. Inject your DataSource inside the ExoPlayer’s DataSource.Factory
as follows:
Use EncryptedDataSourceFactory
to instantiate your previously created DataSource.
Use ProgressiveMediaSource.Factory(dataSourceFactory)
to inject your factory inside ExoPlayer’s MediaSource
. And that’s it! Congratulations, You can now play your encrypted MediaFile the same as a normal file.
Conclusion
In this article, we explored the use of AES encryption for secure media playback on Android with ExoPlayer. We covered the essentials of creating custom DataSources for encrypted local media files. In Part 2, we’ll dive into streaming encrypted media files from a remote server on the fly, expanding your knowledge of secure media playback. Thank you for reading!