diff --git a/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt b/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt index 4c89af1..7bf1d13 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/Constants.kt @@ -30,6 +30,7 @@ object Constants { internal object Keys { const val KEY_ARMADILLO_CONFIG = "armadillo_config" const val KEY_AUDIO_PLAYABLE = "audio_playable" + const val ANDROID_KEYSTORE_NAME= "AndroidKeyStore" } internal object DI { @@ -41,6 +42,11 @@ object Constants { const val GLOBAL_SCOPE = "global_scope" + const val DOWNLOAD_STORE_ALIAS="armadillo" + const val DOWNLOAD_STORE_FILENAME="armadillo.download.secure" + const val STANDARD_STORE_ALIAS="armadilloStandard" + const val STANDARD_STORE_FILENAME="armadillo.standard.secure" + const val STANDARD_STORAGE = "standard_storage" const val STANDARD_SECURE_STORAGE = "standard_secure_storage" const val DRM_DOWNLOAD_STORAGE = "drm_download_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 26c6dcb..dc67ff6 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt @@ -2,13 +2,6 @@ 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 @@ -17,6 +10,10 @@ import com.google.android.exoplayer2.upstream.cache.Cache import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.scribd.armadillo.Constants +import com.scribd.armadillo.Constants.DI.DOWNLOAD_STORE_ALIAS +import com.scribd.armadillo.Constants.DI.DOWNLOAD_STORE_FILENAME +import com.scribd.armadillo.Constants.DI.STANDARD_STORE_ALIAS +import com.scribd.armadillo.Constants.DI.STANDARD_STORE_FILENAME import com.scribd.armadillo.download.ArmadilloDatabaseProvider import com.scribd.armadillo.download.ArmadilloDatabaseProviderImpl import com.scribd.armadillo.download.ArmadilloDownloadManagerFactory @@ -35,10 +32,10 @@ import com.scribd.armadillo.encryption.ExoplayerEncryption import com.scribd.armadillo.encryption.ExoplayerEncryptionImpl import com.scribd.armadillo.encryption.SecureStorage import com.scribd.armadillo.exoplayerExternalDirectory +import com.scribd.armadillo.extensions.createEncryptedSharedPrefKeyStoreWithRetry import dagger.Module import dagger.Provides import java.io.File -import java.security.KeyStore import javax.inject.Named import javax.inject.Qualifier import javax.inject.Singleton @@ -122,46 +119,19 @@ internal class DownloadModule { @Singleton @Provides @Named(Constants.DI.STANDARD_SECURE_STORAGE) - fun standardSecureStorage(context: Context): SharedPreferences { - val keystoreAlias = "armadilloStandard" - val fileName = "armadillo.standard.secure" - return createEncryptedSharedPrefsKeyStore(context = context, fileName = fileName, keystoreAlias = keystoreAlias) + fun standardSecureStorage(context: Context): SharedPreferences? { + val keystoreAlias = STANDARD_STORE_ALIAS + val fileName = STANDARD_STORE_FILENAME + return createEncryptedSharedPrefKeyStoreWithRetry(context = context, fileName = fileName, keystoreAlias = keystoreAlias) } @Singleton @Provides @Named(Constants.DI.DRM_SECURE_STORAGE) - fun drmSecureStorage(context: Context): SharedPreferences { - val keystoreAlias = "armadillo" - val fileName = "armadillo.download.secure" - return createEncryptedSharedPrefsKeyStore(context = context, fileName = fileName, keystoreAlias = keystoreAlias) - } - - private fun createEncryptedSharedPrefsKeyStore(context: Context, fileName: String, keystoreAlias: String) - : SharedPreferences { - val keySpec = KeyGenParameterSpec.Builder(keystoreAlias, PURPOSE_ENCRYPT or PURPOSE_DECRYPT) - .setKeySize(256) - .setBlockModes(BLOCK_MODE_GCM) - .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) - .build() - - val keys = try { - MasterKeys.getOrCreate(keySpec) - } catch (ex: Exception) { - //clear corrupted store, contents will be lost - val keyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null) - keyStore.deleteEntry(keystoreAlias) - context.getSharedPreferences(fileName, Context.MODE_PRIVATE).edit().clear().apply() - MasterKeys.getOrCreate(keySpec) - } - return EncryptedSharedPreferences.create( - fileName, - keys, - context, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) + fun drmSecureStorage(context: Context): SharedPreferences? { + val keystoreAlias = DOWNLOAD_STORE_ALIAS + val fileName = DOWNLOAD_STORE_FILENAME + return createEncryptedSharedPrefKeyStoreWithRetry(context = context, fileName = fileName, keystoreAlias = keystoreAlias) } @Singleton 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 8bfa1b5..e2ad4da 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt @@ -28,9 +28,9 @@ internal interface SecureStorage { @Singleton internal class ArmadilloSecureStorage @Inject constructor( @Named(Constants.DI.STANDARD_STORAGE) private val legacyStandardStorage: SharedPreferences, - @Named(Constants.DI.STANDARD_SECURE_STORAGE) private val secureStandardStorage: 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 + @Named(Constants.DI.DRM_SECURE_STORAGE) private val secureDrmStorage: SharedPreferences? ) : SecureStorage { companion object { const val DOWNLOAD_KEY = "download_key" @@ -41,26 +41,31 @@ internal class ArmadilloSecureStorage @Inject constructor( } override fun downloadSecretKey(context: Context): ByteArray { - return if (secureStandardStorage.contains(DOWNLOAD_KEY)) { + return if (secureStandardStorage?.contains(DOWNLOAD_KEY) == true) { val storedKey = secureStandardStorage.getString(DOWNLOAD_KEY, DEFAULT) ?: DEFAULT if (storedKey == DEFAULT) { Log.e(TAG, "Storage Is Out of Alignment") } storedKey.toSecretByteArray - } else if(legacyStandardStorage.contains(DOWNLOAD_KEY)) { + } else if (legacyStandardStorage.contains(DOWNLOAD_KEY)) { //migrate to secured version val storedKey = legacyStandardStorage.getString(DOWNLOAD_KEY, DEFAULT) ?: 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() + if (secureStandardStorage != null) { + secureStandardStorage.edit().putString(DOWNLOAD_KEY, storedKey).apply() + legacyStandardStorage.edit().remove(DOWNLOAD_KEY).apply() + } storedKey.toSecretByteArray - } else { + } else if (secureStandardStorage != null) { //no key exists anywhere yet createRandomString().also { secureStandardStorage.edit().putString(DOWNLOAD_KEY, it).apply() }.toSecretByteArray + } else { + "".toSecretByteArray + //we've attempted to create 2 sharedPrefs by this point, so this shouldn't happen. Let exoplayer fail to decrypt } } @@ -73,28 +78,30 @@ internal class ArmadilloSecureStorage @Inject constructor( override fun saveDrmDownload(context: Context, id: String, drmDownload: DrmDownload) { val alias = getDrmDownloadAlias(id, drmDownload.drmType) val value = Base64.encodeToString(Json.encodeToString(drmDownload).toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP) - secureDrmStorage.edit().putString(alias, value).apply() + secureDrmStorage?.edit()?.putString(alias, value)?.apply() } override fun getDrmDownload(context: Context, id: String, drmType: DrmType): DrmDownload? { val alias = getDrmDownloadAlias(id, drmType) - var download = secureDrmStorage.getString(alias, null)?.decodeToDrmDownload() + 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() + if (secureDrmStorage != null) { + secureDrmStorage.edit().putString(alias, downloadValue).apply() + legacyDrmStorage.edit().remove(alias).apply() + } } return download } override fun getAllDrmDownloads(context: Context): Map { - val drmDownloads = secureDrmStorage.all.keys.mapNotNull { alias -> + val drmDownloads = secureDrmStorage?.all?.keys?.mapNotNull { alias -> secureDrmStorage.getString(alias, null)?.let { drmResult -> alias to drmResult.decodeToDrmDownload() } - }.toMap() + }?.toMap() ?: emptyMap() val legacyDownloads = legacyDrmStorage.all.keys.mapNotNull { alias -> legacyDrmStorage.getString(alias, null)?.let { drmResult -> alias to drmResult.decodeToDrmDownload() @@ -107,12 +114,12 @@ internal class ArmadilloSecureStorage @Inject constructor( override fun removeDrmDownload(context: Context, id: String, drmType: DrmType) { val alias = getDrmDownloadAlias(id, drmType) legacyDrmStorage.edit().remove(alias).apply() - secureDrmStorage.edit().remove(alias).apply() + secureDrmStorage?.edit()?.remove(alias)?.apply() } override fun removeDrmDownload(context: Context, key: String) { legacyDrmStorage.edit().remove(key).apply() - secureDrmStorage.edit().remove(key).apply() + secureDrmStorage?.edit()?.remove(key)?.apply() } private val String.toSecretByteArray: ByteArray diff --git a/Armadillo/src/main/java/com/scribd/armadillo/extensions/SharedPrefExt.kt b/Armadillo/src/main/java/com/scribd/armadillo/extensions/SharedPrefExt.kt new file mode 100644 index 0000000..af0c2f0 --- /dev/null +++ b/Armadillo/src/main/java/com/scribd/armadillo/extensions/SharedPrefExt.kt @@ -0,0 +1,85 @@ +package com.scribd.armadillo.extensions + +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 android.util.Log +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys +import com.scribd.armadillo.Constants.DI.STANDARD_STORE_FILENAME +import com.scribd.armadillo.Constants.Keys.ANDROID_KEYSTORE_NAME +import java.io.File +import java.security.KeyStore + +fun SharedPreferences.deleteSharedPreference(context: Context, filename: String, keystoreAlias: String) { + val tag = "DeletingSharedPrefs" + try { + val sharedPrefsFile = File( + (context.filesDir.getParent()?.plus("/shared_prefs/")) + filename + ".xml" + ) + + edit().clear().commit() + + if (sharedPrefsFile.exists()) { + val deleted = sharedPrefsFile.delete() + Log.d(tag, "resetStorage() Shared prefs file deleted: $deleted; path: ${sharedPrefsFile.absolutePath}") + } else { + Log.d(tag,"resetStorage() Shared prefs file non-existent; path: ${sharedPrefsFile.absolutePath}") + } + + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE_NAME) + keyStore.load(null) + keyStore.deleteEntry(keystoreAlias) + } catch (e: Exception) { + Log.e(tag, "Error occurred while trying to reset shared prefs", e) + } +} + +fun createEncryptedSharedPrefKeyStoreWithRetry(context: Context, fileName: String, keystoreAlias: String): SharedPreferences? { + val firstAttempt = createEncryptedSharedPrefsKeyStore(context = context, fileName = fileName, keystoreAlias = keystoreAlias) + return if(firstAttempt != null) { + firstAttempt + } else { + context.getSharedPreferences(fileName, Context.MODE_PRIVATE).deleteSharedPreference( + context = context, + filename = fileName, + keystoreAlias = keystoreAlias + ) + createEncryptedSharedPrefsKeyStore(context = context, fileName = fileName, keystoreAlias = keystoreAlias) + } +} + +fun createEncryptedSharedPrefsKeyStore(context: Context, fileName: String, keystoreAlias: String) + : SharedPreferences? { + val keySpec = KeyGenParameterSpec.Builder(keystoreAlias, PURPOSE_ENCRYPT or PURPOSE_DECRYPT) + .setKeySize(256) + .setBlockModes(BLOCK_MODE_GCM) + .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) + .build() + + val keys = try { + MasterKeys.getOrCreate(keySpec) + } catch (ex: Exception) { + //clear corrupted store, contents will be lost + context.getSharedPreferences(fileName, Context.MODE_PRIVATE).deleteSharedPreference( + context = context, + filename = fileName, + keystoreAlias = keystoreAlias ) + MasterKeys.getOrCreate(keySpec) + } + return try { + EncryptedSharedPreferences.create( + fileName, + keys, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } catch(ex: Exception) { + null + } +} \ No newline at end of file diff --git a/RELEASE.md b/RELEASE.md index a377c37..2e7d6ec 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,8 @@ # Project Armadillo Release Notes +## 1.6.8 +- Fixes an app startup crash to EncryptedSharedPreference faults. + ## 1.6.7 - Adds additional data in audio player errors: HttpResponseCodeException, DownloadFailed - Add new ParsingException for internal ParserException diff --git a/gradle.properties b/gradle.properties index 4b3e1a1..5508bfd 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.6.7 +LIBRARY_VERSION=1.6.8 EXOPLAYER_VERSION=2.19.1 RXJAVA_VERSION=2.2.4 RXANDROID_VERSION=2.0.1