google / ExoPlayer

This project is deprecated and stale. The latest ExoPlayer code is available in https://github.com/androidx/media
https://developer.android.com/media/media3/exoplayer
Apache License 2.0
21.7k stars 6.01k forks source link

Simplify encryption and decryption of downloaded content #5193

Open meenukrishnamurthy opened 5 years ago

meenukrishnamurthy commented 5 years ago

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

erdemguven commented 5 years ago

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]);
    };
  }
meenukrishnamurthy commented 5 years ago

Thanks a ton the solution worked

erdemguven commented 5 years ago

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

finneapps commented 4 years ago

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.

ojw28 commented 4 years ago

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.

abrarahmadraza commented 3 years ago

@ojw28 and @erdemguven Thanks a lot for your responses.

    private static final int ENCRYPTION_BUFFER_SIZE = 10 * 1024;

    private synchronized void initDownloadManager() {
        if (downloadManager == null) {
            DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
            upgradeActionFile(
                    DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
            upgradeActionFile(
                    DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
            downloadManager = new DownloadManager(
                    this,
                    getDatabaseProvider(),
                    getDownloadCache(),
                    buildCacheDataSourceFactory(),
                    Executors.newFixedThreadPool(6)
            );
            downloadTracker =
                    new DownloadTracker(/* context= */ this, buildReadOnlyCacheDataSourceFactory(), downloadManager);
        }
    }

    public DataSource.Factory buildReadOnlyCacheDataSourceFactory() {
        DefaultDataSourceFactory upstreamFactory =
                new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
        return new CacheDataSource.Factory()
                .setCache(getDownloadCache())
                .setUpstreamDataSourceFactory(upstreamFactory)
                .setCacheReadDataSourceFactory(getCacheReadDataSourceFactory())
                .setCacheWriteDataSinkFactory(null)
                .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
    }

    public CacheDataSource.Factory buildCacheDataSourceFactory() {
        DefaultDataSourceFactory upstreamFactory =
                new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
        return new CacheDataSource.Factory()
                .setCache(getDownloadCache())
                .setCacheKeyFactory(CacheKeyFactory.DEFAULT)
                .setCacheWriteDataSinkFactory(getCacheWriteDataSinkFactory(getDownloadCache()))
                .setCacheReadDataSourceFactory(getCacheReadDataSourceFactory())
                .setUpstreamDataSourceFactory(upstreamFactory)
                .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
    }

   public HttpDataSource.Factory buildHttpDataSourceFactory()  {
        return new DefaultHttpDataSource.Factory().setUserAgent(userAgent);
    }

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

    private static DataSink.Factory getCacheWriteDataSinkFactory(final Cache cache) {
        return () -> {
                CacheDataSink cacheSink =
                        new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE, CacheDataSink.DEFAULT_BUFFER_SIZE);
                return new AesCipherDataSink(
                        Util.getUtf8Bytes("secretkey1234"), cacheSink, new byte[ENCRYPTION_BUFFER_SIZE]);
        };
    }
abrarahmadraza commented 3 years ago

@meenukrishnamurthy Hey Thanks for your question, that helped a lot.

ojw28 commented 3 years ago

@abrarahmadraza - I think the problem is where you're doing:

downloadManager = new DownloadManager(
    this,
    getDatabaseProvider(),
    getDownloadCache(),
    buildCacheDataSourceFactory(),
    Executors.newFixedThreadPool(6)

the fourth argument is supposed to be a DataSource for requesting from upstream, rather than one that writes to the cache. If you look at what happens in that constructor (here), you'll see that it's building a CacheDataSource.Factory internally to write to the cache, and it doesn't encrypt. Since your code is passing a CacheDataSource.Factory in as upstream, you end up with one wrapping the other, so everything is written to the cache twice, and the one that writes last is the internal one that doesn't encrypt.

To fix this, you need to use the DownloadManager constructor that takes Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory. Something like:

downloadManager =
    new DownloadManager(
       context,
       new DefaultDownloadIndex(getDatabaseProvider(context)),
       new DefaultDownloaderFactory(
           buildCacheDataSourceFactory(),
           Executors.newFixedThreadPool(/* nThreads= */ 6)));