Exoplayer: Simplify encryption and decryption of downloaded content

Created on 4 Dec 2018  路  5Comments  路  Source: google/ExoPlayer

I am facing a issue while encryption and decryption of offline video.I am using the download manager and download tracker to download the offline video like in the demo app.
My code is as follows:

 private static DataSource.Factory buildCacheDataSource(DefaultDataSourceFactory upstreamSource,
                                                           boolean useAesEncryption, Cache cache) {
        final String secretKey = "XXXXXXXXX";
        DataSource file = new FileDataSource();
        DataSource cacheReadDataSource = useAesEncryption
                ? new AesCipherDataSource(Util.getUtf8Bytes(secretKey), file) : file;

        // Sink and cipher
        CacheDataSink cacheSink = new CacheDataSink(cache, Long.MAX_VALUE);
        DataSink cacheWriteDataSink = useAesEncryption
                ? new AesCipherDataSink(Util.getUtf8Bytes(secretKey), cacheSink) : cacheSink;

        return new DataSource.Factory() {
            @Override
            public DataSource createDataSource() {
                DataSource dataSource = upstreamSource.createDataSource();
                // Wrap the DataSource from the regular factory. This will read media from the cache when
                // available, with the AesCipherDataSource element in the chain performing decryption. When
                // not available media will be read from the wrapped DataSource and written into the cache,
                // with AesCipherDataSink performing encryption.
                return new CacheDataSource(cache,
                        dataSource,
                        cacheReadDataSource,
                        cacheWriteDataSink,
                        0,
                        null); // eventListener
            }
        };

I call the above method while creating the datasource factory

public DataSource.Factory buildDataSourceFactoryCache() {
        DefaultDataSourceFactory upstreamFactory =
                new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
        return buildCacheDataSource(upstreamFactory, true,getDownloadCache());

    }

I call the method buildDataSourceFactoryCache in initdownload manager,

private synchronized void initDownloadManager() {
        if (downloadManager == null) {
            DownloaderConstructorHelper downloaderConstructorHelper =
                    new DownloaderConstructorHelper(getDownloadCache(), buildDataSourceFactoryCache());
            downloadManager =
                    new DownloadManager(
                            downloaderConstructorHelper,
                            MAX_SIMULTANEOUS_DOWNLOADS,
                            DownloadManager.DEFAULT_MIN_RETRY_COUNT,
                            new File(getDownloadFileDirectory(), DOWNLOAD_ACTION_FILE));
            downloadTracker =
                    new DownloadTracker(
                            /* context= */ this,
                            buildDataSourceFactoryCache(),
                            new File(getDownloadFileDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
            downloadManager.addListener(downloadTracker);
        }
    }

My download does not take place it shows as download failed.Can anyone tell me where i am going wrong

enhancement

Most helpful comment

One major mistake is you didn't give a buffer to AesCipherDataSink which makes CacheDataSource return encrypted data on the first read. I noticed it's quite hard to get this code right. I'll do some improvements on the library but for now here is a working code with ExoPlayer Demo app.

  private static final String SECRET_KEY = "testKey:12345678";
  private static final int ENCRYPTION_BUFFER_SIZE = 10 * 1024;

  private synchronized void initDownloadManager() {
    if (downloadManager == null) {
      Cache cache = getDownloadCache();
      DownloaderConstructorHelper downloaderConstructorHelper =
          new DownloaderConstructorHelper(
              cache,
              buildHttpDataSourceFactory(),
              getCacheReadDataSourceFactory(),
              getCacheWriteDataSinkFactory(cache),
              /* priorityTaskManager= */ null);
      downloadManager =
          new DownloadManager(
              new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
              new DefaultDownloaderFactory(downloaderConstructorHelper),
              MAX_SIMULTANEOUS_DOWNLOADS,
              DownloadManager.DEFAULT_MIN_RETRY_COUNT);
      downloadTracker =
          new DownloadTracker(
              /* context= */ this,
              buildCacheDataSource(buildHttpDataSourceFactory(), cache),
              new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
      downloadManager.addListener(downloadTracker);
    }
  }

  private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
      DefaultDataSourceFactory upstreamFactory, Cache cache) {
    return new CacheDataSourceFactory(
        cache,
        upstreamFactory,
        getCacheReadDataSourceFactory(),
        /* cacheWriteDataSinkFactory= */ null,
        CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
        /* eventListener= */ null);
  }

  private static CacheDataSourceFactory buildCacheDataSource(
      Factory upstreamFactory, Cache cache) {
    return new CacheDataSourceFactory(
        cache,
        upstreamFactory,
        getCacheReadDataSourceFactory(),
        getCacheWriteDataSinkFactory(cache),
        CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
        /* eventListener= */ null);
  }

  private static DataSource.Factory getCacheReadDataSourceFactory() {
    return () -> new AesCipherDataSource(Util.getUtf8Bytes(SECRET_KEY), new FileDataSource());
  }

  private static DataSink.Factory getCacheWriteDataSinkFactory(final Cache cache) {
    return () -> {
      CacheDataSink cacheSink =
          new CacheDataSink(cache, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
      return new AesCipherDataSink(
          Util.getUtf8Bytes(SECRET_KEY), cacheSink, new byte[ENCRYPTION_BUFFER_SIZE]);
    };
  }

All 5 comments

One major mistake is you didn't give a buffer to AesCipherDataSink which makes CacheDataSource return encrypted data on the first read. I noticed it's quite hard to get this code right. I'll do some improvements on the library but for now here is a working code with ExoPlayer Demo app.

  private static final String SECRET_KEY = "testKey:12345678";
  private static final int ENCRYPTION_BUFFER_SIZE = 10 * 1024;

  private synchronized void initDownloadManager() {
    if (downloadManager == null) {
      Cache cache = getDownloadCache();
      DownloaderConstructorHelper downloaderConstructorHelper =
          new DownloaderConstructorHelper(
              cache,
              buildHttpDataSourceFactory(),
              getCacheReadDataSourceFactory(),
              getCacheWriteDataSinkFactory(cache),
              /* priorityTaskManager= */ null);
      downloadManager =
          new DownloadManager(
              new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
              new DefaultDownloaderFactory(downloaderConstructorHelper),
              MAX_SIMULTANEOUS_DOWNLOADS,
              DownloadManager.DEFAULT_MIN_RETRY_COUNT);
      downloadTracker =
          new DownloadTracker(
              /* context= */ this,
              buildCacheDataSource(buildHttpDataSourceFactory(), cache),
              new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
      downloadManager.addListener(downloadTracker);
    }
  }

  private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
      DefaultDataSourceFactory upstreamFactory, Cache cache) {
    return new CacheDataSourceFactory(
        cache,
        upstreamFactory,
        getCacheReadDataSourceFactory(),
        /* cacheWriteDataSinkFactory= */ null,
        CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
        /* eventListener= */ null);
  }

  private static CacheDataSourceFactory buildCacheDataSource(
      Factory upstreamFactory, Cache cache) {
    return new CacheDataSourceFactory(
        cache,
        upstreamFactory,
        getCacheReadDataSourceFactory(),
        getCacheWriteDataSinkFactory(cache),
        CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
        /* eventListener= */ null);
  }

  private static DataSource.Factory getCacheReadDataSourceFactory() {
    return () -> new AesCipherDataSource(Util.getUtf8Bytes(SECRET_KEY), new FileDataSource());
  }

  private static DataSink.Factory getCacheWriteDataSinkFactory(final Cache cache) {
    return () -> {
      CacheDataSink cacheSink =
          new CacheDataSink(cache, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
      return new AesCipherDataSink(
          Util.getUtf8Bytes(SECRET_KEY), cacheSink, new byte[ENCRYPTION_BUFFER_SIZE]);
    };
  }

Thanks a ton the solution worked

I'm glad that you find it useful. Let's keep this issue to track improvements on this issue.

Can you recommend a way of creating the SECRET_KEY? I guess it is not optimal to just hardcode something random. I am thinking to either generate a key based on some installationID or use something from the jetpack security library.
I first tried

val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build();
val keyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

Then I could use the keyAlias as the secret key. But MasterKeys is deprecated and also requires API 23. Any recommendations here would be appreciated.

Can you recommend a way of creating the SECRET_KEY?

I don't think this is an ExoPlayer specific question. It's a generic question about key management. We're not experts in this area and I imagine it's probably quite a complicated topic (e.g., where you store the key after you generate it is presumably also important). I think you'll find better resources elsewhere. A few Google searches for things like ("secret keys android") suggests there's quite a lot of information online about this topic. You'll likely also find people more qualified to answer questions about this on Stackoverflow than you will here.

Was this page helpful?
0 / 5 - 0 ratings