AndroidX PreferenceFragmentCompat scaffolded SharedPreferences encrypted with Jetpack Security
Aug 16, 2020
Let me start by disadvising anybody from developing their own encryption algorithm, encryption protocol, or encryption schemes while trying to increase security of an application. Always use well known encryption libraries and such best practice protocols and techniques wich are advised by experts. Otherwise you can easily make a tiny mistake which could compromise your whole scheme.
This is why I’m so happy that Android Jetpack has a Security feature set which offers encryption / decryption of SharedPreferences and file streams. The fact that it’s developed by Google engineers gives a peace of mind that it’ll pass a high standard of quality and since it’s backed by Google it is safe to rely on it. The default advised usage uses latest algorithms and modes like AES256, GCM and AES-GCM-SIV. Complex scenarios can be configured using KeyGenParameterSpec, key authentication expiration can be specified and JetSec is also capable of using Titan security module which elevates the security to hardware token level (Titan basically integrates the hardware security token into the phone).
Be careful when reading blogs about JetSec because it’s still in alpha phase so the API can still change. Good example for that is the MasterKey: MasterKeys (plural) is deprecated now, so if you read stuff like MasterKeys.getOrCreate that’s an older alpha version. You’d want to look for MasterKey.Builder. This blog post won’t talk about file encryption, I want to focus on SharedPreferences encryption and specifically Kotlin - which is now the primary first class over Java citizen in the Android ecosystem. Here is a code snippet how you can easily obtain an encrypted SharedPreferences instance:
val masterKey = MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val preferences = EncryptedSharedPreferences.create(
applicationContext,
SHARED_PREFERENCES_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
Once you have that it can be used just as if it would be a regular SharedPreferences. Once you’ve changed your code the saved cleartext data won’t be compatible with the new scheme - this is not a surprise. I have to talk about PreferenceFragment
- Add implementation "androidx.security:security-crypto-ktx:1.1.0-alpha02" for JetSec to you gradle.build file.
- You need to add implementation "androidx.preference:preference:1.1.1" to you gradle.build file for PreferenceFragmentCompat and additional Jetpack preferences helper functionality.
- Define your preferences UI in a declarative way in res/xml/preferences.xml file.
- Add a small SettingsActivity stub.
- Add a small SettingsFragment stub.
- Supply the EncryptedPreferenceDataStore which provides the encrypted provider for preferences data.
Here is a snippet of the res/xml/preferences.xml:
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.SettingsFragment">
<EditTextPreference
app:key="project_id"
app:title="@string/project_id_title"
app:summary="@string/project_id_help"
android:icon="@drawable/ic_one"/>
<EditTextPreference
app:key="application_id"
app:title="@string/application_id_title"
app:summary="@string/application_id_help"
android:icon="@drawable/ic_two"/>
<!-- ... -->
</PreferenceScreen>
SettingsActivity.kt:
package your.package.path.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import your.package.path.R
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings_container, SettingsFragment())
.commit()
}
}
SettingsFragment.kt:
package your.package.path.ui
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import your.package.path.data.EncryptedPreferenceDataStore
import your.package.path.R
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// The following instruction makes the difference between the encrypted and the normal version:
preferenceManager.preferenceDataStore =
EncryptedPreferenceDataStore.getInstance(requireContext())
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}
The most important code in the last snippet. The only distinction between the encrypted and the non encrypted variation is the preferenceManager.
package your.package.path.data
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceManager
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class EncryptedPreferenceDataStore private constructor(context: Context) : PreferenceDataStore() {
companion object {
private const val SHARED_PREFERENCES_NAME = "secret_shared_preferences"
@Volatile private var INSTANCE: EncryptedPreferenceDataStore? = null
fun getInstance(context: Context): EncryptedPreferenceDataStore =
INSTANCE ?: synchronized(this) {
INSTANCE ?: EncryptedPreferenceDataStore(context).also { INSTANCE = it }
}
}
private var mSharedPreferences: SharedPreferences
private lateinit var mContext: Context
init {
try {
mContext = context
val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
mSharedPreferences = EncryptedSharedPreferences.create(
context,
SHARED_PREFERENCES_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (e: Exception) {
// Fallback, default mode is Context.MODE_PRIVATE!
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
}
}
override fun putString(key: String, value: String?) {
mSharedPreferences.edit().putString(key, value).apply()
}
override fun putStringSet(key: String, values: Set<String>?) {
mSharedPreferences.edit().putStringSet(key, values).apply()
}
override fun putInt(key: String, value: Int) {
mSharedPreferences.edit().putInt(key, value).apply()
}
override fun putLong(key: String, value: Long) {
mSharedPreferences.edit().putLong(key, value).apply()
}
override fun putFloat(key: String, value: Float) {
mSharedPreferences.edit().putFloat(key, value).apply()
}
override fun putBoolean(key: String, value: Boolean) {
mSharedPreferences.edit().putBoolean(key, value).apply()
}
override fun getString(key: String, defValue: String?): String? {
return mSharedPreferences.getString(key, defValue)
}
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? {
return mSharedPreferences.getStringSet(key, defValues)
}
override fun getInt(key: String, defValue: Int): Int {
return mSharedPreferences.getInt(key, defValue)
}
override fun getLong(key: String, defValue: Long): Long {
return mSharedPreferences.getLong(key, defValue)
}
override fun getFloat(key: String, defValue: Float): Float {
return mSharedPreferences.getFloat(key, defValue)
}
override fun getBoolean(key: String, defValue: Boolean): Boolean {
return mSharedPreferences.getBoolean(key, defValue)
}
}
For Java version please look at this StackOverflow post.