diff --git a/Armadillo/build.gradle b/Armadillo/build.gradle index 94a98b8..ba2179e 100644 --- a/Armadillo/build.gradle +++ b/Armadillo/build.gradle @@ -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 { @@ -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") @@ -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 @@ -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" diff --git a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt index f07089a..170f50f 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt @@ -14,7 +14,6 @@ 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 @@ -22,8 +21,11 @@ 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 @@ -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 diff --git a/Armadillo/src/main/java/com/scribd/armadillo/encryption/ExoplayerEncryption.kt b/Armadillo/src/main/java/com/scribd/armadillo/encryption/ExoplayerEncryption.kt index 5cf6cff..648961c 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/ExoplayerEncryption.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/ExoplayerEncryption.kt @@ -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 { @@ -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()) } } \ No newline at end of file diff --git a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt index 5cb6a4d..d4400f8 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt @@ -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 + 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(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 = + context.getSharedPreferences(DOWNLOAD_FILENAME, Context.MODE_PRIVATE).let { sharedPrefs -> + sharedPrefs.all.mapNotNull { + sharedPrefs.getString(it.key, null)?.let { drmResult -> + it.key to Json.decodeFromString(drmResult) + } + }.toMap() } private val String.toSecretByteArray: ByteArray diff --git a/Armadillo/src/main/java/com/scribd/armadillo/models/Models.kt b/Armadillo/src/main/java/com/scribd/armadillo/models/Models.kt index 0d95498..dcfe0ab 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/models/Models.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/models/Models.kt @@ -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 @@ -143,6 +144,36 @@ data class Chapter( data class DrmInfo(val drmType: DrmType, val licenseServer: String, val drmHeaders: Map = 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;