Skeiths Chew
Skeiths Chew

Reputation: 161

Is there a way to integrate EncryptedSharedPreference with PreferenceScreen?

I am new to android development. Currently, I would like to encrypt a custom named Shared Preference and integrate with PreferenceScreen but failed to do so. I am using dependencies:

  1. androidx.security:security-crypto:1.0.0-alpha02 [EncryptedSharedPreference]
  2. androidx.preference:preference:1.1.0 [PreferenceScreen]

I had tried to research the related information about integration of these 2 features but no related information found.

From my testing, I had an existing encrypted shared preference and tested the following API:

getPreferenceManager().setSharedPreferencesName("MyShared"); //MyShared Is custom named preference.

But it ended up save the preference in plain value.

My Questions:

  1. Is it possible to integrate these 2 features together in current stage?
  2. Does PreferenceScreen provide encrypted feature as I am not aware of?
  3. If I am insist to use EncryptedSharedPreference, will it be better that I create a custom activity look like preference screen?

Upvotes: 9

Views: 2254

Answers (4)

Doron Ben-Ari
Doron Ben-Ari

Reputation: 572

Took the answers here, added a touch of Kotlin sugar to make it shorter, avoided using unreleased library versions, and here is what came out:

Dependencies:

dependencies {
    implementation "androidx.security:security-crypto:1.0.0"
    implementation 'androidx.preference:preference-ktx:1.2.0'
}

DataStore:

lateinit var dataStore: PreferenceDataStore
lateinit var globalPreferences: SharedPreferences

private const val SHARED_PREFERENCES_NAME = "secret_shared_preferences"

fun prepareDataStore(context: Context) {

    val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

    globalPreferences = EncryptedSharedPreferences.create(
        SHARED_PREFERENCES_NAME,
        masterKey,
        context,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    dataStore = object :
        PreferenceDataStore(), SharedPreferences by globalPreferences {

        private val editor by lazy { globalPreferences.edit() }

        override fun putString(key: String, value: String?) =
            editor.putString(key, value).apply()

        override fun putStringSet(key: String, values: Set<String>?) =
            editor.putStringSet(key, values).apply()

        override fun putInt(key: String, value: Int) =
            editor.putInt(key, value).apply()

        override fun putLong(key: String, value: Long) =
            editor.putLong(key, value).apply()

        override fun putFloat(key: String, value: Float) =
            editor.putFloat(key, value).apply()

        override fun putBoolean(key: String, value: Boolean) =
            editor.putBoolean(key, value).apply()

    }
}

PreferencesFragment:

    class PreferencesFragment: PreferenceFragmentCompat() {

        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            preferenceManager.preferenceDataStore = dataStore
            setPreferencesFromResource(R.xml.root_preferences, rootKey)

Usage:

    boolean bIsXXX = globalPreferences.getBoolean(getString(R.string.pref_access_xxx), true);

Remarks:

  1. The trick for the get... functions could not work for the put... functions by inheriting SharedPreferences.Editor, because the put... functions have different return values than those of PreferenceDataStore and also require a call to apply().
  2. Not using singleton is my taste about singletons which depend on arguments and ignore the arguments on next calls. But that is a different subject. I suggest calling prepareDataStore ones from class MyApplication : Application() and change its Context parameter to be Application to make it clear.

Upvotes: 1

Csaba Toth
Csaba Toth

Reputation: 10699

A1: Yes it's possible.

A3: You can take advantage of system provided settings the following way.

Since Kotlin is the preferred first class citizen for a while now I'll show it in Kotlin, @Rikka has a Java version in another answer. For Kotlin the trick is to still set the preferencesDataSource, it goes this way:

class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        preferenceManager.preferenceDataStore =
            EncryptedPreferenceDataStore.getInstance(requireContext())

        // Load the preferences from an XML resource
        setPreferencesFromResource(R.xml.preferences, rootKey)
    }

The Kotlin version of the EncryptedPreferenceDataStore: I'm using the also keyword for the singleton, similarly as the Google source code related Room example in Singleton with parameter in Kotlin

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)
    }
}

Maybe it could be even more thread safe by double synchronized checking?

Upvotes: 5

JaLooNz
JaLooNz

Reputation: 55

There are a few issues that I encounter with integrating EncryptedSharePreferences with AndroidX Preferences GUI.

  • PreferenceManager is not able to set the default preference data store globally, because it is only possible to retrieve the default shared preferences (un-encrypted variant and tied to application package name) and not set the default shared preferences to the encrypted variant. PreferenceManager.getDefaultSharedPreferences(context); does not have a corresponding set method.
  • PreferenceManager can only be created by packages with the same library group.

The solution I have created is to not depend on SharedPreferences, but to utilise the PreferenceFragmentCompat to write to EncryptedPreferenceDataStore. However, this still comes with the issue that the default values are not initialised until the user enters the Preference screen.

Dependencies

dependencies {
    implementation 'androidx.preference:preference:1.1.1'
    implementation 'androidx.security:security-crypto:1.0.0-rc01'
}

PreferencesFragment

import android.app.Activity;
import android.app.ActivityManager;
import android.os.Bundle;

import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;

public class PreferencesFragment extends PreferenceFragmentCompat {

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        PreferenceManager preferenceManager = getPreferenceManager();
        preferenceManager.setPreferenceDataStore(EncryptedPreferenceDataStore.getInstance(getContext()));

        // Load the preferences from an XML resource
        setPreferencesFromResource(R.xml.preferences, rootKey);
    }

EncryptedPreferenceDataStore

import android.content.Context;
import android.content.SharedPreferences;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceDataStore;
import androidx.security.crypto.EncryptedSharedPreferences;

import java.util.Set;

public class EncryptedPreferenceDataStore extends PreferenceDataStore {

    private static final String CONFIG_FILE_NAME = "FileName";
    private static final String CONFIG_MASTER_KEY_ALIAS = "KeyAlias";
    private static EncryptedPreferenceDataStore mInstance;
    private SharedPreferences mSharedPreferences;
    private Context mContext;

    private EncryptedPreferenceDataStore(Context context) {
        try {
            mContext = context;
            mSharedPreferences = EncryptedSharedPreferences.create(
                    CONFIG_FILE_NAME,
                    CONFIG_MASTER_KEY_ALIAS,
                    context,
                    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, //for encrypting Keys
                    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ////for encrypting Values
            );
        } catch (Exception e) {
            // Fallback
            mSharedPreferences = context.getSharedPreferences(CONFIG_FILE_NAME, Context.MODE_PRIVATE);
        }
    }

    @Override
    public void putString(String key, @Nullable String value) {
        mSharedPreferences.edit().putString(key, value).apply();
    }

    @Override
    public void putStringSet(String key, @Nullable Set<String> values) {
        mSharedPreferences.edit().putStringSet(key, values).apply();
    }

    @Override
    public void putInt(String key, int value) {
        mSharedPreferences.edit().putInt(key, value).apply();
    }

    @Override
    public void putLong(String key, long value) {
        mSharedPreferences.edit().putLong(key, value).apply();
    }

    @Override
    public void putFloat(String key, float value) {
        mSharedPreferences.edit().putFloat(key, value).apply();
    }

    @Override
    public void putBoolean(String key, boolean value) {
        mSharedPreferences.edit().putBoolean(key, value).apply();
    }

    @Nullable
    @Override
    public String getString(String key, @Nullable String defValue) {
        return mSharedPreferences.getString(key, defValue);
    }

    @Nullable
    @Override
    public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
        return mSharedPreferences.getStringSet(key, defValues);
    }

    @Override
    public int getInt(String key, int defValue) {
        return mSharedPreferences.getInt(key, defValue);
    }

    @Override
    public long getLong(String key, long defValue) {
        return mSharedPreferences.getLong(key, defValue);
    }

    @Override
    public float getFloat(String key, float defValue) {
        return mSharedPreferences.getFloat(key, defValue);
    }

    @Override
    public boolean getBoolean(String key, boolean defValue) {
        return mSharedPreferences.getBoolean(key, defValue);
    }
}

Usage

    EncryptedPreferenceDataStore prefs = EncryptedPreferenceDataStore.getInstance(getContext());
    boolean bIsXXX = prefs.getBoolean(getString(R.string.pref_access_xxx), true);

Upvotes: 2

Rikka
Rikka

Reputation: 471

Use getPreferenceManager().setPreferenceDataStore(PreferenceDataStore). PreferenceDataStore provides the ability to change how preference is loaded/saved.

A simple implementation of PreferenceDataStore:

public class SharedPreferenceDataStore extends PreferenceDataStore {

    private final SharedPreferences mSharedPreferences;

    public SharedPreferenceDataStore(@NonNull SharedPreferences sharedPreferences) {
        mSharedPreferences = sharedPreferences;
    }

    @NonNull
    public SharedPreferences getSharedPreferences() {
        return mSharedPreferences;
    }

    @Override
    public void putString(String key, @Nullable String value) {
        mSharedPreferences.edit().putString(key, value).apply();
    }

    @Override
    public void putStringSet(String key, @Nullable Set<String> values) {
        mSharedPreferences.edit().putStringSet(key, values).apply();
    }

    @Override
    public void putInt(String key, int value) {
        mSharedPreferences.edit().putInt(key, value).apply();
    }

    @Override
    public void putLong(String key, long value) {
        mSharedPreferences.edit().putLong(key, value).apply();
    }

    @Override
    public void putFloat(String key, float value) {
        mSharedPreferences.edit().putFloat(key, value).apply();
    }

    @Override
    public void putBoolean(String key, boolean value) {
        mSharedPreferences.edit().putBoolean(key, value).apply();
    }

    @Nullable
    @Override
    public String getString(String key, @Nullable String defValue) {
        return mSharedPreferences.getString(key, defValue);
    }

    @Nullable
    @Override
    public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
        return mSharedPreferences.getStringSet(key, defValues);
    }

    @Override
    public int getInt(String key, int defValue) {
        return mSharedPreferences.getInt(key, defValue);
    }

    @Override
    public long getLong(String key, long defValue) {
        return mSharedPreferences.getLong(key, defValue);
    }

    @Override
    public float getFloat(String key, float defValue) {
        return mSharedPreferences.getFloat(key, defValue);
    }

    @Override
    public boolean getBoolean(String key, boolean defValue) {
        return mSharedPreferences.getBoolean(key, defValue);
    }
}
getPreferenceManager().setPreferenceDataStore(new SharedPreferenceDataStore(EncryptedSharedPreferences.create()));

Upvotes: 4

Related Questions