diff --git a/Armadillo/build.gradle b/Armadillo/build.gradle index 62a81ca..d564ec8 100644 --- a/Armadillo/build.gradle +++ b/Armadillo/build.gradle @@ -27,7 +27,7 @@ android { compileSdk 34 defaultConfig { - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 34 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" consumerProguardFiles 'proguard-rules.pro' @@ -54,6 +54,8 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' + implementation "androidx.security:security-crypto:1.0.0" + compileOnly files('../libs/exoplayer-core-release.aar') implementation "com.google.android.exoplayer:exoplayer-common:${EXOPLAYER_VERSION}" implementation ("com.google.android.exoplayer:exoplayer-hls:${EXOPLAYER_VERSION}") { diff --git a/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt b/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt index 12f47c1..4c89af1 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt @@ -42,7 +42,9 @@ object Constants { const val GLOBAL_SCOPE = "global_scope" const val STANDARD_STORAGE = "standard_storage" + const val STANDARD_SECURE_STORAGE = "standard_secure_storage" const val DRM_DOWNLOAD_STORAGE = "drm_download_storage" + const val DRM_SECURE_STORAGE = "drm_secure_storage" } internal object Exoplayer { 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 96c3036..8c8270f 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt @@ -2,6 +2,13 @@ package com.scribd.armadillo.di import android.content.Context import android.content.SharedPreferences +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties.BLOCK_MODE_GCM +import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE +import android.security.keystore.KeyProperties.PURPOSE_DECRYPT +import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys import com.google.android.exoplayer2.offline.DownloadManager import com.google.android.exoplayer2.offline.DownloadService import com.google.android.exoplayer2.offline.DownloaderFactory @@ -111,6 +118,46 @@ internal class DownloadModule { fun drmDownloadStorage(context: Context): SharedPreferences = context.getSharedPreferences("armadillo.download.drm", Context.MODE_PRIVATE) + @Singleton + @Provides + @Named(Constants.DI.STANDARD_SECURE_STORAGE) + fun standardSecureStorage(context: Context): SharedPreferences { + val keys = MasterKeys.getOrCreate( + KeyGenParameterSpec.Builder("armadilloStandard", PURPOSE_ENCRYPT or PURPOSE_DECRYPT) + .setKeySize(256) + .setBlockModes(BLOCK_MODE_GCM) + .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) + .build() + ) + return EncryptedSharedPreferences.create( + "armadillo.standard.secure", + keys, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + @Singleton + @Provides + @Named(Constants.DI.DRM_SECURE_STORAGE) + fun drmSecureStorage(context: Context): SharedPreferences { + val keys = MasterKeys.getOrCreate( + KeyGenParameterSpec.Builder("armadillo", PURPOSE_ENCRYPT or PURPOSE_DECRYPT) + .setKeySize(256) + .setBlockModes(BLOCK_MODE_GCM) + .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) + .build() + ) + return EncryptedSharedPreferences.create( + "armadillo.download.secure", + keys, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + @Singleton @Provides fun downloadManagerFactory(downloadManagerFactory: ArmadilloDownloadManagerFactory): DownloadManagerFactory = downloadManagerFactory 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 3f45e67..140543d 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt @@ -27,8 +27,10 @@ internal interface SecureStorage { @Singleton internal class ArmadilloSecureStorage @Inject constructor( - @Named(Constants.DI.STANDARD_STORAGE) private val standardStorage: SharedPreferences, - @Named(Constants.DI.DRM_DOWNLOAD_STORAGE) private val drmDownloadStorage: SharedPreferences, + @Named(Constants.DI.STANDARD_STORAGE) private val legacyStandardStorage: SharedPreferences, + @Named(Constants.DI.STANDARD_SECURE_STORAGE) private val secureStandardStorage: SharedPreferences, + @Named(Constants.DI.DRM_DOWNLOAD_STORAGE) private val legacyDrmStorage: SharedPreferences, + @Named(Constants.DI.DRM_SECURE_STORAGE) private val secureDrmStorage: SharedPreferences ) : SecureStorage { companion object { const val DOWNLOAD_KEY = "download_key" @@ -39,15 +41,25 @@ internal class ArmadilloSecureStorage @Inject constructor( } override fun downloadSecretKey(context: Context): ByteArray { - return if (standardStorage.contains(DOWNLOAD_KEY)) { - val storedKey = standardStorage.getString(DOWNLOAD_KEY, DEFAULT)!! + return if (secureStandardStorage.contains(DOWNLOAD_KEY)) { + val storedKey = secureDrmStorage.getString(DOWNLOAD_KEY, DEFAULT)!! if (storedKey == DEFAULT) { Log.e(TAG, "Storage Is Out of Alignment") } storedKey.toSecretByteArray + } else if(legacyStandardStorage.contains(DOWNLOAD_KEY)) { + //migrate to secured version + val storedKey = legacyStandardStorage.getString(DOWNLOAD_KEY, DEFAULT)!! + if (storedKey == DEFAULT) { + Log.e(TAG, "Storage Is Out of Alignment") + } + secureStandardStorage.edit().putString(DOWNLOAD_KEY, storedKey).apply() + legacyStandardStorage.edit().remove(DOWNLOAD_KEY).apply() + storedKey.toSecretByteArray } else { + //no key exists anywhere yet createRandomString().also { - standardStorage.edit().putString(DOWNLOAD_KEY, it).apply() + secureStandardStorage.edit().putString(DOWNLOAD_KEY, it).apply() }.toSecretByteArray } } @@ -59,27 +71,48 @@ internal class ArmadilloSecureStorage @Inject constructor( } override fun saveDrmDownload(context: Context, audioUrl: String, drmDownload: DrmDownload) { - val key = getDrmDownloadKey(audioUrl, drmDownload.drmType) + val alias = getDrmDownloadAlias(audioUrl, drmDownload.drmType) val value = Base64.encodeToString(Json.encodeToString(drmDownload).toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP) - drmDownloadStorage.edit().putString(key, value).apply() + secureDrmStorage.edit().putString(alias, value).apply() } - override fun getDrmDownload(context: Context, audioUrl: String, drmType: DrmType): DrmDownload? = - drmDownloadStorage.getString(getDrmDownloadKey(audioUrl, drmType), null)?.decodeToDrmDownload() + override fun getDrmDownload(context: Context, audioUrl: String, drmType: DrmType): DrmDownload? { + val alias = getDrmDownloadAlias(audioUrl, drmType) + var download = secureDrmStorage.getString(alias, null)?.decodeToDrmDownload() + if (download == null && legacyDrmStorage.contains(alias)) { + //migrate old storage to secure storage + val downloadValue = legacyDrmStorage.getString(alias, null) + download = downloadValue?.decodeToDrmDownload() + secureDrmStorage.edit().putString(alias, downloadValue).apply() + legacyDrmStorage.edit().remove(alias).apply() + } + return download + } - override fun getAllDrmDownloads(context: Context): Map = - drmDownloadStorage.all.keys.mapNotNull { key -> - drmDownloadStorage.getString(key, null)?.let { drmResult -> - key to drmResult.decodeToDrmDownload() + override fun getAllDrmDownloads(context: Context): Map { + val drmDownloads = secureDrmStorage.all.keys.mapNotNull { alias -> + secureDrmStorage.getString(alias, null)?.let { drmResult -> + alias to drmResult.decodeToDrmDownload() + } + }.toMap() + val legacyDownloads = legacyDrmStorage.all.keys.mapNotNull { alias -> + legacyDrmStorage.getString(alias, null)?.let { drmResult -> + alias to drmResult.decodeToDrmDownload() } }.toMap() + return drmDownloads.plus(legacyDownloads) + } + override fun removeDrmDownload(context: Context, audioUrl: String, drmType: DrmType) { - drmDownloadStorage.edit().remove(getDrmDownloadKey(audioUrl, drmType)).apply() + val alias = getDrmDownloadAlias(audioUrl, drmType) + legacyDrmStorage.edit().remove(alias).apply() + secureDrmStorage.edit().remove(alias).apply() } override fun removeDrmDownload(context: Context, key: String) { - drmDownloadStorage.edit().remove(key).apply() + legacyDrmStorage.edit().remove(key).apply() + secureDrmStorage.edit().remove(key).apply() } private val String.toSecretByteArray: ByteArray @@ -91,7 +124,7 @@ internal class ArmadilloSecureStorage @Inject constructor( return keyBytes } - private fun getDrmDownloadKey(audioUrl: String, drmType: DrmType) = + private fun getDrmDownloadAlias(audioUrl: String, drmType: DrmType) = Base64.encodeToString(audioUrl.toSecretByteArray + drmType.name.toSecretByteArray, Base64.NO_WRAP) private fun String.decodeToDrmDownload(): DrmDownload = diff --git a/RELEASE.md b/RELEASE.md index 3f7e8b0..278b5ea 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,8 @@ # Project Armadillo Release Notes +## 1.6.0 +- Encrypts widevine drm keys. The Android minSDK has been changed to version 23 in order to support this feature. + ## 1.5.4 - Ensured that ArmadilloPlayer.armadilloStateObservable has a state as soon as the player is initialized - Fixed UnknownHostException being mapped to a HTTP status code issue rather than a Connectivity issue. diff --git a/TestApp/build.gradle b/TestApp/build.gradle index 8e86df7..4e26a5a 100644 --- a/TestApp/build.gradle +++ b/TestApp/build.gradle @@ -17,7 +17,7 @@ android { defaultConfig { applicationId "com.scribd.armadillotestapp" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 34 versionCode 1 versionName "1.0" diff --git a/TestApp/src/main/res/xml/backup.xml b/TestApp/src/main/res/xml/backup.xml index e5be8ed..8cf4588 100644 --- a/TestApp/src/main/res/xml/backup.xml +++ b/TestApp/src/main/res/xml/backup.xml @@ -1,4 +1,8 @@ + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7cdf70e..c2bd107 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true PACKAGE_NAME=com.scribd.armadillo GRADLE_PLUGIN_VERSION=7.2.0 -LIBRARY_VERSION=1.5.4 +LIBRARY_VERSION=1.6.0 EXOPLAYER_VERSION=2.19.1 RXJAVA_VERSION=2.2.4 RXANDROID_VERSION=2.0.1