有没有一种方法可以将EncryptedSharedPreference与PreferenceScreen集成?

9

我是 Android 开发的新手。目前,我想加密一个自定义命名的 Shared Preference 并将其与 PreferenceScreen 集成,但未能成功。我正在使用以下依赖项:

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

我已经尝试过搜索有关这两个功能集成的相关信息,但没有找到任何相关信息。

通过我的测试,我拥有一个现有的加密共享偏好设置,并测试了以下 API:

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

但实际上它最终仅以明文值保存了偏好设置。

我的问题:

  1. 在当前阶段是否可能将这两个功能集成在一起?
  2. 我不知道PreferenceScreen是否提供加密功能?
  3. 如果我坚持使用EncryptedSharedPreference,是否最好创建一个自定义活动,类似于偏好设置屏幕?
4个回答

5

A1: 可以实现。

A3: 您可以按照以下方式利用系统提供的设置。由于 Kotlin 已经成为首选语言一段时间了,因此我将展示 Kotlin 的方式。@Rikka 在另一个答案中给出了 Java 版本。对于 Kotlin,技巧仍然是要设置 preferencesDataSource,代码如下:

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

EncryptedPreferenceDataStore 的 Kotlin 版本:我使用 also 关键字来创建单例,类似于 Google 官方 Room 示例中在 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)
    }
}

也许通过双重同步检查,可以使其更加线程安全?


4

使用getPreferenceManager().setPreferenceDataStore(PreferenceDataStore)PreferenceDataStore提供了更改首选项如何加载/保存的能力。

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()));

运行得非常好!! - Osvel Alvarez Jacomino

2

在将EncryptedSharePreferences与AndroidX Preferences GUI集成时,我遇到了一些问题。

  • PreferenceManager无法全局设置默认的首选项数据存储,因为只能检索默认的共享首选项(未加密变体且绑定到应用程序包名称),而不能将默认共享首选项设置为加密变体。 PreferenceManager.getDefaultSharedPreferences(context); 没有相应的设置方法。
  • 只有相同库组的软件包才能创建PreferenceManager。

我创建的解决方案是不依赖SharedPreferences,而是利用PreferenceFragmentCompat写入EncryptedPreferenceDataStore。然而,这仍然存在一个问题,即默认值直到用户进入首选项屏幕才会初始化。

依赖关系

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

使用方法

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

由于mSharedPreferences不是Fragment的成员,那么你如何取消/注册OnSharedPreferenceChangeListener呢? - Marvin

1
拿到这里的答案后,我加入了一些 Kotlin 的语法糖以使代码更简洁,并避免使用未发布的库版本,最终得到了以下代码:

依赖项:

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

数据存储:

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)

使用方法:

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

备注:

  1. 对于继承 SharedPreferences.Editor 的 put... 函数,无法通过 get... 函数的技巧来实现,因为 put... 函数的返回值与 PreferenceDataStore 不同,并且需要调用 apply()
  2. 不使用单例是我对依赖参数并忽略下一次调用参数的单例的偏好。但这是一个不同的主题。我建议从 class MyApplication : Application() 中调用 prepareDataStore 一次,并将其 Context 参数更改为 Application,以使其更清晰明了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接