How to play an encrypted Video or Audio file from a remote server or a local file on Android (Part 1)

Mohsen Mousavi
4 min readOct 26, 2023

--

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 own DataSource 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 CipherInputStreamas 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 connections or InputStreams 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!

--

--