Skip to content

Commit

Permalink
[APT-10658] Better EncryptedSharedPreference Resilience
Browse files Browse the repository at this point in the history
The earlier failsafe does not protect against the app crashing on startup. This time, the exception is caught and the SecureStorage can handle the SharedPref being null.
  • Loading branch information
kabliz committed Oct 23, 2024
1 parent 9e70e2b commit 1ad0659
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 59 deletions.
6 changes: 6 additions & 0 deletions Armadillo/src/main/java/com/scribd/armadillo/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"
Expand Down
56 changes: 13 additions & 43 deletions Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
}

Expand All @@ -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<String, DrmDownload> {
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()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1ad0659

Please sign in to comment.