Skip to content

Commit

Permalink
Prevent attempting to read backed up EncryptedSharedPreferences that …
Browse files Browse the repository at this point in the history
…are no longer readable (#2113)
  • Loading branch information
tylerjroach authored Nov 15, 2022
1 parent 610edc7 commit 5f92ff5
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,81 @@ import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV
import androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
import androidx.security.crypto.MasterKeys
import java.io.File
import java.util.UUID

internal class EncryptedKeyValueRepository(
private val context: Context,
private val sharedPreferencesName: String,
) : KeyValueRepository {

@VisibleForTesting
internal val sharedPreferences: SharedPreferences by lazy {
EncryptedSharedPreferences.create(
"$sharedPreferencesName.${getInstallationIdentifier(context, sharedPreferencesName)}",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
AES256_SIV,
AES256_GCM
)
}

@VisibleForTesting
internal val editor: SharedPreferences.Editor by lazy {
sharedPreferences.edit()
}

override fun put(dataKey: String, value: String?) {
with(getSharedPreferences().edit()) {
with(editor) {
putString(dataKey, value)
apply()
}
}

override fun get(dataKey: String): String? = getSharedPreferences().getString(dataKey, null)
override fun get(dataKey: String): String? = sharedPreferences.getString(dataKey, null)

override fun remove(dataKey: String) {
with(getSharedPreferences().edit()) {
with(editor) {
remove(dataKey)
apply()
}
}

@VisibleForTesting
fun getSharedPreferences(): SharedPreferences {
return EncryptedSharedPreferences.create(
sharedPreferencesName,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
AES256_SIV,
AES256_GCM
)
/**
* EncryptedSharedPreferences may have been backed up by the application, but will be unreadable due to the
* KeyStore record being lost. To prevent an unreadable EncryptedSharedPreferences, we append a suffix to the name
* with a UUID created in the noBackupFilesDir
*/
@Synchronized
private fun getInstallationIdentifier(context: Context, keyValueRepoID: String): String {
val identifierFile = File(context.noBackupFilesDir, "$keyValueRepoID.installationIdentifier")
val previousIdentifier = getExistingInstallationIdentifier(identifierFile)

return previousIdentifier ?: createInstallationIdentifier(identifierFile)
}

/**
* Gets the existing installation identifier (if exists)
*/
private fun getExistingInstallationIdentifier(identifierFile: File): String? {
return if (identifierFile.exists()) {
val identifier = identifierFile.readText()
identifier.ifBlank { null }
} else {
null
}
}

/**
* Creates a new installation identifier for the install
*/
private fun createInstallationIdentifier(identifierFile: File): String {
val newIdentifier = UUID.randomUUID().toString()
try {
identifierFile.writeText(newIdentifier)
} catch (e: Exception) {
// Failed to write identifier to file, session will be forced to be in memory
}
return newIdentifier
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ class EncryptedKeyValueRepositoryTest {

@Before
fun setup() {
Mockito.`when`(repository.getSharedPreferences()).thenReturn(mockPrefs)
Mockito.`when`(mockPrefs.edit()).thenReturn(mockPrefsEditor)
Mockito.`when`(repository.sharedPreferences).thenReturn(mockPrefs)
Mockito.`when`(repository.editor).thenReturn(mockPrefsEditor)
}

@Test
Expand Down

0 comments on commit 5f92ff5

Please sign in to comment.