From 93cb0a6b766f4ebc54a3bf855a37c5fa2d3eb636 Mon Sep 17 00:00:00 2001 From: Katherine Blizard <414924+kabliz@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:36:14 -0700 Subject: [PATCH 1/3] [APT-10344] Use EncryptedSharedPreferences Encrypts widevine drm keys. The Android minSDK has been changed to version 23 in order to do this. Old DRM storage contents are migrated to the new one in SecureStorage The DRM storage backup files are excluded from syncing with Google Backup. --- Armadillo/build.gradle | 4 ++- .../java/com/scribd/armadillo/Constants.kt | 1 + .../com/scribd/armadillo/di/DownloadModule.kt | 27 +++++++++++++++++++ .../armadillo/encryption/SecureStorage.kt | 26 +++++++++++++----- RELEASE.md | 3 +++ TestApp/build.gradle | 2 +- TestApp/src/main/res/xml/backup.xml | 2 ++ gradle.properties | 2 +- 8 files changed, 58 insertions(+), 9 deletions(-) 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..22224c4 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt @@ -43,6 +43,7 @@ object Constants { const val STANDARD_STORAGE = "standard_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..347cebd 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,26 @@ internal class DownloadModule { fun drmDownloadStorage(context: Context): SharedPreferences = context.getSharedPreferences("armadillo.download.drm", Context.MODE_PRIVATE) + @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..3bc1b35 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt @@ -29,6 +29,7 @@ internal interface SecureStorage { 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.DRM_SECURE_STORAGE) private val secureDrmStorage: SharedPreferences ) : SecureStorage { companion object { const val DOWNLOAD_KEY = "download_key" @@ -59,13 +60,23 @@ 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 && drmDownloadStorage.contains(alias)) { + //migrate old storage to secure storage + val downloadValue = drmDownloadStorage.getString(alias, null) + download = downloadValue?.decodeToDrmDownload() + secureDrmStorage.edit().putString(alias, downloadValue).apply() + drmDownloadStorage.edit().remove(alias).apply() + } + return download + } override fun getAllDrmDownloads(context: Context): Map = drmDownloadStorage.all.keys.mapNotNull { key -> @@ -75,11 +86,14 @@ internal class ArmadilloSecureStorage @Inject constructor( }.toMap() override fun removeDrmDownload(context: Context, audioUrl: String, drmType: DrmType) { - drmDownloadStorage.edit().remove(getDrmDownloadKey(audioUrl, drmType)).apply() + val alias = getDrmDownloadAlias(audioUrl, drmType) + drmDownloadStorage.edit().remove(alias).apply() + secureDrmStorage.edit().remove(alias).apply() } override fun removeDrmDownload(context: Context, key: String) { drmDownloadStorage.edit().remove(key).apply() + secureDrmStorage.edit().remove(key).apply() } private val String.toSecretByteArray: ByteArray @@ -91,7 +105,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..af66237 100644 --- a/TestApp/src/main/res/xml/backup.xml +++ b/TestApp/src/main/res/xml/backup.xml @@ -1,4 +1,6 @@ + + \ 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 From 26be2af516afa1c716b5cfa1756d1f18507b93dc Mon Sep 17 00:00:00 2001 From: Katherine Blizard <414924+kabliz@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:34:26 -0700 Subject: [PATCH 2/3] [APT-10344] Use EncryptedSharedPreferences Encrypts widevine drm keys. The Android minSDK has been changed to version 23 in order to do this. Old DRM storage contents are migrated to the new one in SecureStorage The DRM storage backup files are excluded from syncing with Google Backup. [APT-10344] Use EncryptedSharedPreferences Encrypts widevine drm keys. The Android minSDK has been changed to version 23 in order to do this. Old DRM storage contents are migrated to the new one in SecureStorage The DRM storage backup files are excluded from syncing with Google Backup. --- .../java/com/scribd/armadillo/Constants.kt | 1 + .../com/scribd/armadillo/di/DownloadModule.kt | 20 +++++++++++ .../armadillo/encryption/SecureStorage.kt | 35 ++++++++++++------- TestApp/src/main/res/xml/backup.xml | 2 ++ 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt b/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt index 22224c4..4c89af1 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt @@ -42,6 +42,7 @@ 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" } 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 347cebd..8c8270f 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt @@ -118,6 +118,26 @@ 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) 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 3bc1b35..c919e75 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,9 @@ 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 { @@ -40,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 } } @@ -68,31 +79,31 @@ internal class ArmadilloSecureStorage @Inject constructor( 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 && drmDownloadStorage.contains(alias)) { + if (download == null && legacyDrmStorage.contains(alias)) { //migrate old storage to secure storage - val downloadValue = drmDownloadStorage.getString(alias, null) + val downloadValue = legacyDrmStorage.getString(alias, null) download = downloadValue?.decodeToDrmDownload() secureDrmStorage.edit().putString(alias, downloadValue).apply() - drmDownloadStorage.edit().remove(alias).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 -> + legacyDrmStorage.all.keys.mapNotNull { key -> + legacyDrmStorage.getString(key, null)?.let { drmResult -> key to drmResult.decodeToDrmDownload() } }.toMap() override fun removeDrmDownload(context: Context, audioUrl: String, drmType: DrmType) { val alias = getDrmDownloadAlias(audioUrl, drmType) - drmDownloadStorage.edit().remove(alias).apply() + 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() } diff --git a/TestApp/src/main/res/xml/backup.xml b/TestApp/src/main/res/xml/backup.xml index af66237..8cf4588 100644 --- a/TestApp/src/main/res/xml/backup.xml +++ b/TestApp/src/main/res/xml/backup.xml @@ -1,6 +1,8 @@ + + \ No newline at end of file From d7a6d864b65873825952cf3ea622fbcac3119663 Mon Sep 17 00:00:00 2001 From: Katherine Blizard <414924+kabliz@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:03:32 -0700 Subject: [PATCH 3/3] [APT-10344] Use EncryptedSharedPreferences Encrypts widevine drm keys. The Android minSDK has been changed to version 23 in order to do this. Old DRM storage contents are migrated to the new one in SecureStorage The DRM storage backup files are excluded from syncing with Google Backup. --- .../scribd/armadillo/encryption/SecureStorage.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 c919e75..140543d 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt @@ -89,12 +89,20 @@ internal class ArmadilloSecureStorage @Inject constructor( return download } - override fun getAllDrmDownloads(context: Context): Map = - legacyDrmStorage.all.keys.mapNotNull { key -> - legacyDrmStorage.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) { val alias = getDrmDownloadAlias(audioUrl, drmType)