Skip to content

Commit

Permalink
[APT-9577] Add DRM for downloads for offline usage : update SecureSto…
Browse files Browse the repository at this point in the history
…rage to it can store the DRM key id

Update SecureStorage to add new methods to save and fetch DRM key id.
This ID will later be used to fetch the DRM key needed to decrypt a DRM-protected content.
The ID is saved per content (per audio URL) and per DRM technology (we currently only support Widevine).

Also make SecureStorage injectable with Dagger.
  • Loading branch information
rubeus90 committed Feb 12, 2024
1 parent 5f4401b commit abb2a0f
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 22 deletions.
7 changes: 5 additions & 2 deletions Armadillo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ plugins {
// Extension of maven plugin that properly handles android dependencies
id 'digital.wup.android-maven-publish' version "${MAVEN_PUBLISH_VERSION}"
id 'org.jetbrains.dokka' version "${DOKKA_VERSION}" // Official Kotlin documentation engine - does both Kotlin and Java docs
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.0'
}

android {
Expand Down Expand Up @@ -59,6 +60,8 @@ dependencies {
implementation "com.google.dagger:dagger:${DAGGER_VERSION}"
kapt "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
implementation 'androidx.media:media:1.6.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:${SERIALIZATON_VERSION}"

testImplementation "org.robolectric:robolectric:4.5.1"
testImplementation 'junit:junit:4.13.2'
testImplementation("org.assertj:assertj-core:3.10.0")
Expand All @@ -85,7 +88,7 @@ publishing {

publications {
android.libraryVariants.all { variant ->
"${variant.name.capitalize()}Aar" (MavenPublication) {
"${variant.name.capitalize()}Aar"(MavenPublication) {
from(components[variant.name])
groupId project.PACKAGE_NAME
version project.LIBRARY_VERSION
Expand All @@ -98,7 +101,7 @@ publishing {
}

android.libraryVariants.all { variant ->
"${variant.name.capitalize()}SnapshotAar" (MavenPublication) {
"${variant.name.capitalize()}SnapshotAar"(MavenPublication) {
from(components[variant.name])
groupId project.PACKAGE_NAME
version "${project.LIBRARY_VERSION}-SNAPSHOT"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ import com.scribd.armadillo.download.ArmadilloDatabaseProviderImpl
import com.scribd.armadillo.download.ArmadilloDownloadManagerFactory
import com.scribd.armadillo.download.CacheManager
import com.scribd.armadillo.download.CacheManagerImpl
import com.scribd.armadillo.download.MaxAgeCacheEvictor
import com.scribd.armadillo.download.DefaultExoplayerDownloadService
import com.scribd.armadillo.download.DownloadEngine
import com.scribd.armadillo.download.DownloadManagerFactory
import com.scribd.armadillo.download.DownloadTracker
import com.scribd.armadillo.download.ExoplayerDownloadEngine
import com.scribd.armadillo.download.ExoplayerDownloadTracker
import com.scribd.armadillo.download.HeaderAwareDownloaderFactory
import com.scribd.armadillo.download.MaxAgeCacheEvictor
import com.scribd.armadillo.encryption.ArmadilloSecureStorage
import com.scribd.armadillo.encryption.ExoplayerEncryption
import com.scribd.armadillo.encryption.ExoplayerEncryptionImpl
import com.scribd.armadillo.encryption.SecureStorage
import com.scribd.armadillo.exoplayerExternalDirectory
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -92,6 +94,10 @@ internal class DownloadModule {
@Provides
fun exoplayerEncryption(exoplayerEncryption: ExoplayerEncryptionImpl): ExoplayerEncryption = exoplayerEncryption

@Singleton
@Provides
fun secureStorage(secureStorage: ArmadilloSecureStorage): SecureStorage = secureStorage

@Singleton
@Provides
fun downloadManagerFactory(downloadManagerFactory: ArmadilloDownloadManagerFactory): DownloadManagerFactory = downloadManagerFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ interface ExoplayerEncryption {
* This class provides the plumbing for encrypting downloaded content & then reading this encrypted content.
*/
@Singleton
internal class ExoplayerEncryptionImpl @Inject constructor(applicationContext: Context) : ExoplayerEncryption {
internal class ExoplayerEncryptionImpl @Inject constructor(applicationContext: Context,
secureStorage: SecureStorage) : ExoplayerEncryption {

private val secureStorage: SecureStorage = ArmadilloSecureStorage()
private val secret = secureStorage.downloadSecretKey(applicationContext)

override fun dataSinkFactory(downloadCache: Cache) = DataSink.Factory {
Expand All @@ -30,6 +30,6 @@ internal class ExoplayerEncryptionImpl @Inject constructor(applicationContext: C
}

override fun dataSourceFactory(upstream: DataSource.Factory) =
DataSource.Factory { AesCipherDataSource(secret, upstream.createDataSource()) }
DataSource.Factory { AesCipherDataSource(secret, upstream.createDataSource()) }

}
Original file line number Diff line number Diff line change
@@ -1,42 +1,95 @@
package com.scribd.armadillo.encryption

import android.content.Context
import android.util.Base64
import android.util.Log
import com.scribd.armadillo.models.DrmDownload
import com.scribd.armadillo.models.DrmType
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import javax.inject.Inject
import javax.inject.Singleton

internal interface SecureStorage {
fun downloadSecretKey(context: Context): ByteArray
fun saveDrmDownload(context: Context, audioUrl: String, drmDownload: DrmDownload)
fun getDrmDownload(context: Context, audioUrl: String, drmType: DrmType): DrmDownload?
fun getAllDrmDownloads(context: Context): Map<String, DrmDownload>
fun removeDrmDownload(context: Context, audioUrl: String, drmType: DrmType)
fun removeDrmDownload(context: Context, key: String)
}

internal class ArmadilloSecureStorage : SecureStorage {
@Singleton
internal class ArmadilloSecureStorage @Inject constructor() : SecureStorage {
private companion object {
const val DOWNLOAD_KEY = "download_key"
const val STRING_LENGTH = 20
const val ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789"
const val DEFAULT = "82YEDKqPBEqA2qAb4bUU"
const val LOCATION = "armadillo.storage"
const val STANDARD_STORAGE_FILENAME = "armadillo.storage"
const val DOWNLOAD_FILENAME = "armadillo.download"
const val TAG = "SecureStorage"
}

override fun downloadSecretKey(context: Context): ByteArray {
val sharedPreferences = context.getSharedPreferences(LOCATION, Context.MODE_PRIVATE)
return if (sharedPreferences.contains(DOWNLOAD_KEY)) {
val storedKey = sharedPreferences.getString(DOWNLOAD_KEY, DEFAULT)!!
if(storedKey == DEFAULT){
Log.e(TAG, "Storage Is Out of Alignment")
}
storedKey.toSecretByteArray
} else {
createRandomString().also {
sharedPreferences.edit().putString(DOWNLOAD_KEY, it).apply()
}.toSecretByteArray
val sharedPreferences = context.getSharedPreferences(STANDARD_STORAGE_FILENAME, Context.MODE_PRIVATE)
return if (sharedPreferences.contains(DOWNLOAD_KEY)) {
val storedKey = sharedPreferences.getString(DOWNLOAD_KEY, DEFAULT)!!
if (storedKey == DEFAULT) {
Log.e(TAG, "Storage Is Out of Alignment")
}
storedKey.toSecretByteArray
} else {
createRandomString().also {
sharedPreferences.edit().putString(DOWNLOAD_KEY, it).apply()
}.toSecretByteArray
}
}

private fun createRandomString(): String {
return (1..STRING_LENGTH)
.map { ALLOWED_CHARS.random() }
.joinToString("")
return (1..STRING_LENGTH)
.map { ALLOWED_CHARS.random() }
.joinToString("")
}

override fun saveDrmDownload(context: Context, audioUrl: String, drmDownload: DrmDownload) {
context.getSharedPreferences(DOWNLOAD_FILENAME, Context.MODE_PRIVATE).also { sharedPrefs ->
val key = Base64.encodeToString(audioUrl.toSecretByteArray + drmDownload.drmType.name.toSecretByteArray, Base64.NO_WRAP)
val value = Base64.encodeToString(Json.encodeToString(drmDownload).toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP)
sharedPrefs.edit().putString(key, value).apply()
}
}

override fun getDrmDownload(context: Context, audioUrl: String, drmType: DrmType): DrmDownload? =
context.getSharedPreferences(DOWNLOAD_FILENAME, Context.MODE_PRIVATE).let { sharedPrefs ->
val key = Base64.encodeToString(audioUrl.toSecretByteArray + drmType.name.toSecretByteArray, Base64.NO_WRAP)
sharedPrefs.getString(key, null)?.let { drmResult ->
Json.decodeFromString<DrmDownload>(drmResult)
}
}

override fun removeDrmDownload(context: Context, audioUrl: String, drmType: DrmType) {
context.getSharedPreferences(DOWNLOAD_FILENAME, Context.MODE_PRIVATE).also { sharedPrefs ->
val key = Base64.encodeToString(audioUrl.toSecretByteArray + drmType.name.toSecretByteArray, Base64.NO_WRAP)
sharedPrefs.edit().remove(key).apply()
}
}

override fun removeDrmDownload(context: Context, key: String) {
context.getSharedPreferences(DOWNLOAD_FILENAME, Context.MODE_PRIVATE).also { sharedPrefs ->
sharedPrefs.edit().remove(key).apply()
}
}

override fun getAllDrmDownloads(context: Context): Map<String, DrmDownload> =
context.getSharedPreferences(DOWNLOAD_FILENAME, Context.MODE_PRIVATE).let { sharedPrefs ->
sharedPrefs.all.mapNotNull {
sharedPrefs.getString(it.key, null)?.let { drmResult ->
it.key to Json.decodeFromString<DrmDownload>(drmResult)
}
}.toMap()
}

private val String.toSecretByteArray: ByteArray
Expand Down
31 changes: 31 additions & 0 deletions Armadillo/src/main/java/com/scribd/armadillo/models/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.scribd.armadillo.models
import android.os.Parcel
import android.os.Parcelable
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.C.ContentType
import com.scribd.armadillo.Milliseconds
import com.scribd.armadillo.extensions.toPrint
import com.scribd.armadillo.time.milliseconds
Expand Down Expand Up @@ -143,6 +144,36 @@ data class Chapter(

data class DrmInfo(val drmType: DrmType, val licenseServer: String, val drmHeaders: Map<String, String> = emptyMap()) : Serializable

@kotlinx.serialization.Serializable
data class DrmDownload(
val drmKeyId: ByteArray,
val drmType: DrmType,
val licenseServer: String,
@ContentType val audioType: Int,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as DrmDownload

if (!drmKeyId.contentEquals(other.drmKeyId)) return false
if (drmType != other.drmType) return false
if (licenseServer != other.licenseServer) return false
if (audioType != other.audioType) return false

return true
}

override fun hashCode(): Int {
var result = drmKeyId.contentHashCode()
result = 31 * result + drmType.hashCode()
result = 31 * result + licenseServer.hashCode()
result = 31 * result + audioType
return result
}
}

enum class DrmType {
WIDEVINE;

Expand Down

0 comments on commit abb2a0f

Please sign in to comment.