在Android应用程序中存储用户设置的最合适的方式是什么

我正在创建一个使用用户名/密码连接到服务器的应用程序,我想启用“保存密码”选项,这样用户就不必在每次应用程序启动时输入密码。

我试图用共享偏好来做,但不确定这是否是最好的解决方案。

我很感激任何关于如何在Android应用程序中存储用户值/设置的建议。

157515 次浏览

一般来说,SharedPreferences是存储首选项的最佳选择,所以一般来说,我推荐使用这种方法来保存应用程序和用户设置。

这里唯一需要关注的是你节省了什么。存储密码总是一件很棘手的事情,我对以明文形式存储密码特别谨慎。Android架构是这样的,你的应用程序的SharedPreferences是沙盒的,以防止其他应用程序能够访问这些值,所以有一些安全性,但对手机的物理访问可能允许访问这些值。

如果可能的话,我会考虑修改服务器以使用协商令牌来提供访问,例如OAuth。或者,您可能需要构造某种加密存储,尽管这不是简单的。至少,要确保在将密码写入磁盘之前对其进行了加密。

你需要使用sqlite,安全apit来存储密码。 下面是最好的例子,它存储密码——passwordsafe。 这是来源和解释的链接 http://code.google.com/p/android-passwordsafe/ < / p >

关于在Android Activity中存储单个首选项的最简单的方法是这样做:

Editor e = this.getPreferences(Context.MODE_PRIVATE).edit();
e.putString("password", mPassword);
e.commit();

如果您担心这些密码的安全性,那么您可以在存储密码之前对其进行加密。

使用Richard提供的代码片段,您可以在保存密码之前对其进行加密。然而,首选项API并没有提供一种简单的方法来拦截值并加密它-你可以通过OnPreferenceChange监听器阻止它被保存,理论上你可以通过preferenceChangeListener修改它,但这会导致一个无休止的循环。

我之前建议添加一个“隐藏的”首选项来完成这个任务。这绝对不是最好的方法。我将提出另外两个我认为更可行的选择。

首先,最简单的,是在preferenceChangeListener中,你可以抓取输入的值,加密,然后保存到另一个首选项文件中:

  public boolean onPreferenceChange(Preference preference, Object newValue) {
// get our "secure" shared preferences file.
SharedPreferences secure = context.getSharedPreferences(
"SECURE",
Context.MODE_PRIVATE
);
String encryptedText = null;
// encrypt and set the preference.
try {
encryptedText = SimpleCrypto.encrypt(Preferences.SEED,(String)newValue);


Editor editor = secure.getEditor();
editor.putString("encryptedPassword",encryptedText);
editor.commit();
}
catch (Exception e) {
e.printStackTrace();
}
// always return false.
return false;
}

第二种方式,也是我现在更喜欢的方式,是创建自己的自定义首选项,扩展EditTextPreference, @Override setText()getText()方法,以便setText()加密密码,而getText()返回null。

我同意Reto和fiXedd。客观地说,在SharedPreferences中投入大量的时间和精力来加密密码并没有多大意义,因为任何可以访问您的首选项文件的攻击者都很可能也可以访问您的应用程序的二进制文件,因此也可以访问解密密码的密钥。

然而,话虽如此,似乎确实有一种宣传活动正在进行,即识别在SharedPreferences中以明文存储密码的移动应用程序,并对这些应用程序进行不利的报道。有关一些例子,请参见http://blogs.wsj.com/digits/2011/06/08/some-top-apps-put-data-at-risk/http://viaforensics.com/appwatchdog

虽然我们总体上需要更多地关注安全问题,但我认为,这种对这一特定问题的关注实际上并不能显著提高我们的整体安全。然而,鉴于人们的看法,这里有一个解决方案来加密您放置在SharedPreferences中的数据。

只需将您自己的SharedPreferences对象包装在这个对象中,您读/写的任何数据都将自动加密和解密。如。

final SharedPreferences prefs = new ObscuredSharedPreferences(
this, this.getSharedPreferences(MY_PREFS_FILE_NAME, Context.MODE_PRIVATE) );


// eg.
prefs.edit().putString("foo","bar").commit();
prefs.getString("foo", null);

下面是这个类的代码:

/**
* Warning, this gives a false sense of security.  If an attacker has enough access to
* acquire your password store, then he almost certainly has enough access to acquire your
* source binary and figure out your encryption key.  However, it will prevent casual
* investigators from acquiring passwords, and thereby may prevent undesired negative
* publicity.
*/
public class ObscuredSharedPreferences implements SharedPreferences {
protected static final String UTF8 = "utf-8";
private static final char[] SEKRIT = ... ; // INSERT A RANDOM PASSWORD HERE.
// Don't use anything you wouldn't want to
// get out there if someone decompiled
// your app.




protected SharedPreferences delegate;
protected Context context;


public ObscuredSharedPreferences(Context context, SharedPreferences delegate) {
this.delegate = delegate;
this.context = context;
}


public class Editor implements SharedPreferences.Editor {
protected SharedPreferences.Editor delegate;


public Editor() {
this.delegate = ObscuredSharedPreferences.this.delegate.edit();
}


@Override
public Editor putBoolean(String key, boolean value) {
delegate.putString(key, encrypt(Boolean.toString(value)));
return this;
}


@Override
public Editor putFloat(String key, float value) {
delegate.putString(key, encrypt(Float.toString(value)));
return this;
}


@Override
public Editor putInt(String key, int value) {
delegate.putString(key, encrypt(Integer.toString(value)));
return this;
}


@Override
public Editor putLong(String key, long value) {
delegate.putString(key, encrypt(Long.toString(value)));
return this;
}


@Override
public Editor putString(String key, String value) {
delegate.putString(key, encrypt(value));
return this;
}


@Override
public void apply() {
delegate.apply();
}


@Override
public Editor clear() {
delegate.clear();
return this;
}


@Override
public boolean commit() {
return delegate.commit();
}


@Override
public Editor remove(String s) {
delegate.remove(s);
return this;
}
}


public Editor edit() {
return new Editor();
}




@Override
public Map<String, ?> getAll() {
throw new UnsupportedOperationException(); // left as an exercise to the reader
}


@Override
public boolean getBoolean(String key, boolean defValue) {
final String v = delegate.getString(key, null);
return v!=null ? Boolean.parseBoolean(decrypt(v)) : defValue;
}


@Override
public float getFloat(String key, float defValue) {
final String v = delegate.getString(key, null);
return v!=null ? Float.parseFloat(decrypt(v)) : defValue;
}


@Override
public int getInt(String key, int defValue) {
final String v = delegate.getString(key, null);
return v!=null ? Integer.parseInt(decrypt(v)) : defValue;
}


@Override
public long getLong(String key, long defValue) {
final String v = delegate.getString(key, null);
return v!=null ? Long.parseLong(decrypt(v)) : defValue;
}


@Override
public String getString(String key, String defValue) {
final String v = delegate.getString(key, null);
return v != null ? decrypt(v) : defValue;
}


@Override
public boolean contains(String s) {
return delegate.contains(s);
}


@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
delegate.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
}


@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
delegate.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
}








protected String encrypt( String value ) {


try {
final byte[] bytes = value!=null ? value.getBytes(UTF8) : new byte[0];
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(SEKRIT));
Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
pbeCipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID).getBytes(UTF8), 20));
return new String(Base64.encode(pbeCipher.doFinal(bytes), Base64.NO_WRAP),UTF8);


} catch( Exception e ) {
throw new RuntimeException(e);
}


}


protected String decrypt(String value){
try {
final byte[] bytes = value!=null ? Base64.decode(value,Base64.DEFAULT) : new byte[0];
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(SEKRIT));
Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
pbeCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID).getBytes(UTF8), 20));
return new String(pbeCipher.doFinal(bytes),UTF8);


} catch( Exception e) {
throw new RuntimeException(e);
}
}


}

我知道这有点死灵,但你应该使用Android AccountManager。它是专门为这个场景构建的。这有点麻烦,但它所做的一件事是,如果SIM卡发生变化,本地凭据就会失效,所以如果有人刷你的手机,把一个新的SIM卡扔进去,你的凭据不会被泄露。

这也为用户提供了一种快速简单的方法,可以从一个地方访问(可能还会删除)他们在设备上的任何帐户的存储凭据。

SampleSyncAdapter是一个使用存储帐户凭据的例子。

好吧;有一段时间了,答案有点复杂,但这里有一些常见的答案。我疯狂地研究了这个问题,很难找到一个好的答案

  1. MODE_PRIVATE方法通常被认为是安全的,如果您假设用户没有对设备进行根操作的话。您的数据以纯文本形式存储在文件系统的某个部分中,该部分只能由原始程序访问。这使得在根设备上使用另一个应用程序获取密码很容易。那么,您是否希望支持根设备?

  2. AES仍然是你能做的最好的加密。如果你正在开始一个新的实现,如果距离我发布这篇文章已经有一段时间了,请记得查看一下。最大的问题是“如何处理加密密钥?”

所以,现在我们到了“如何处理密钥?”的部分。这是最难的部分。拿到钥匙其实也没那么糟。您可以使用密钥派生函数获取某个密码,并使其成为相当安全的密钥。你确实会遇到诸如“你对PKFDF2进行了多少次传递?”之类的问题,但这是另一个话题

  1. 理想情况下,您可以在设备上存储AES密钥。但是,您必须找到一种好方法来安全、可靠、安全地从服务器检索密钥

  2. 您有某种类型的登录序列(甚至是用于远程访问的原始登录序列)。您可以对同一个密码执行两次密钥生成器。它的工作原理是使用一个新的盐和一个新的安全初始化向量派生两次密钥。您可以在设备上存储一个生成的密码,并使用第二个密码作为AES密钥。

登录时,在本地登录时重新派生密钥,并将其与存储的密钥进行比较。完成此操作后,您将使用派生键#2用于AES。

  1. 使用“一般安全”的方法,使用AES加密数据并将密钥存储在MODE_PRIVATE中。这是最近一篇Android博客文章推荐的。不是很安全,但对一些人来说,纯文本要好得多

你可以做很多变化。例如,您可以使用一个快速的PIN(派生的),而不是完整的登录序列。快速PIN可能不像完整的登录序列那么安全,但它比纯文本安全很多倍

这个答案是基于Mark建议的方法。创建EditTextPreference类的自定义版本,它在视图中看到的纯文本和存储在首选项存储中的密码的加密版本之间来回转换。

正如大多数在这篇文章中回答问题的人所指出的,这不是一种非常安全的技术,尽管安全程度在一定程度上取决于所使用的加密/解密代码。但它相当简单和方便,并将阻止大多数随意窥探。

下面是自定义EditTextPreference类的代码:

package com.Merlinia.OutBack_Client;


import android.content.Context;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.util.Base64;


import com.Merlinia.MEncryption_Main.MEncryptionUserPassword;




/**
* This class extends the EditTextPreference view, providing encryption and decryption services for
* OutBack user passwords. The passwords in the preferences store are first encrypted using the
* MEncryption classes and then converted to string using Base64 since the preferences store can not
* store byte arrays.
*
* This is largely copied from this article, except for the encryption/decryption parts:
* https://groups.google.com/forum/#!topic/android-developers/pMYNEVXMa6M
*/
public class EditPasswordPreference  extends EditTextPreference {


// Constructor - needed despite what compiler says, otherwise app crashes
public EditPasswordPreference(Context context) {
super(context);
}




// Constructor - needed despite what compiler says, otherwise app crashes
public EditPasswordPreference(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}




// Constructor - needed despite what compiler says, otherwise app crashes
public EditPasswordPreference(Context context, AttributeSet attributeSet, int defaultStyle) {
super(context, attributeSet, defaultStyle);
}




/**
* Override the method that gets a preference from the preferences storage, for display by the
* EditText view. This gets the base64 password, converts it to a byte array, and then decrypts
* it so it can be displayed in plain text.
* @return  OutBack user password in plain text
*/
@Override
public String getText() {
String decryptedPassword;


try {
decryptedPassword = MEncryptionUserPassword.aesDecrypt(
Base64.decode(getSharedPreferences().getString(getKey(), ""), Base64.DEFAULT));
} catch (Exception e) {
e.printStackTrace();
decryptedPassword = "";
}


return decryptedPassword;
}




/**
* Override the method that gets a text string from the EditText view and stores the value in
* the preferences storage. This encrypts the password into a byte array and then encodes that
* in base64 format.
* @param passwordText  OutBack user password in plain text
*/
@Override
public void setText(String passwordText) {
byte[] encryptedPassword;


try {
encryptedPassword = MEncryptionUserPassword.aesEncrypt(passwordText);
} catch (Exception e) {
e.printStackTrace();
encryptedPassword = new byte[0];
}


getSharedPreferences().edit().putString(getKey(),
Base64.encodeToString(encryptedPassword, Base64.DEFAULT))
.commit();
}




@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
if (restoreValue)
getEditText().setText(getText());
else
super.onSetInitialValue(restoreValue, defaultValue);
}
}

这显示了如何使用它-这是驱动首选项显示的“items”文件。注意,它包含三个普通EditTextPreference视图和一个自定义EditPasswordPreference视图。

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">


<EditTextPreference
android:key="@string/useraccountname_key"
android:title="@string/useraccountname_title"
android:summary="@string/useraccountname_summary"
android:defaultValue="@string/useraccountname_default"
/>


<com.Merlinia.OutBack_Client.EditPasswordPreference
android:key="@string/useraccountpassword_key"
android:title="@string/useraccountpassword_title"
android:summary="@string/useraccountpassword_summary"
android:defaultValue="@string/useraccountpassword_default"
/>


<EditTextPreference
android:key="@string/outbackserverip_key"
android:title="@string/outbackserverip_title"
android:summary="@string/outbackserverip_summary"
android:defaultValue="@string/outbackserverip_default"
/>


<EditTextPreference
android:key="@string/outbackserverport_key"
android:title="@string/outbackserverport_title"
android:summary="@string/outbackserverport_summary"
android:defaultValue="@string/outbackserverport_default"
/>


</PreferenceScreen>

至于实际的加密/解密,留给读者练习。我目前正在使用一些基于本文http://zenu.wordpress.com/2011/09/21/aes-128bit-cross-platform-java-and-c-encryption-compatibility/的代码,尽管键和初始化向量的值不同。

您还可以查看这个小库,其中包含您提到的功能。

https://github.com/kovmarci86/android-secure-preferences

它与这里的其他一些方法类似。希望会有所帮助:)

共享首选项是存储应用程序数据的最简单方法。但任何人都可以通过应用程序管理器清除我们共享的首选项数据。所以我不认为它对我们的应用是完全安全的。

首先,我认为用户的数据不应该存储在手机上,如果必须将数据存储在手机上的某个地方,它应该在应用程序的私有数据中加密。用户凭证的安全性应该是应用程序的优先级。

敏感数据必须安全存储,否则就不存储。在设备丢失或恶意软件感染的情况下,不安全存储的数据可能会受到损害。

我将加入这个圈子,只是为了谈谈在Android上保护密码的一般问题。在Android上,设备二进制文件应该被认为是被破坏的——这对于任何终端应用程序都是一样的,因为它是直接由用户控制的。从概念上讲,黑客可以使用对二进制文件的必要访问来反编译它,并找出你的加密密码等等。

因此,如果安全是你主要关注的问题,我想提出两个建议:

1)不要保存实际密码。存储已授予的访问令牌,并使用该访问令牌和话机的签名对会话服务器端进行认证。这样做的好处是,您可以使令牌有一个有限的持续时间,您不会损害原始密码,并且您有一个很好的签名,您可以使用它来关联以后的流量(例如检查入侵尝试和使令牌无效,使其无用)。

2)利用2因素认证。这可能更令人讨厌和侵入,但在某些合规情况下是不可避免的。

这是对那些根据问题标题来这里的人的补充回答(就像我做的那样),不需要处理与保存密码相关的安全问题。

如何使用共享首选项

用户设置通常使用带有键值对的SharedPreferences保存在Android本地。使用String键保存或查找相关值。

写入共享首选项

String key = "myInt";
int valueToSave = 10;


SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(key, valueToSave).commit();

使用apply()而不是commit()在后台保存,而不是立即保存。

从共享首选项读取

String key = "myInt";
int defaultValue = 0;


SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
int savedValue = sharedPref.getInt(key, defaultValue);

如果没有找到键,则使用默认值。

笔记

  • 与其像上面那样在多个地方使用本地键String,不如在单个位置使用常量。你可以在你的设置活动的顶部使用这样的东西:

      final static String PREF_MY_INT_KEY = "myInt";
    
  • 我在我的例子中使用了int,但你也可以使用putString()putBoolean()getString()getBoolean()等。

  • 参见文档获取更多详细信息。

  • 有多种方法获得SharedPreferences。查看这个答案了解需要注意什么。

我使用Android KeyStore在ECB模式下使用RSA加密密码,然后将其保存在SharedPreferences中。

当我想要回密码时,我从SharedPreferences中读取加密的密码,并使用KeyStore对其解密。

使用这个方法,你可以生成一个公共/私有密钥对,其中私有密钥对由Android安全存储和管理。

下面是一个关于如何做到这一点的链接:Android KeyStore教程

正如其他人已经指出的,你可以使用SharedPreferences一般,但如果你想存储加密的数据有点不方便。幸运的是,现在有一种更简单、更快速的方法来加密数据,因为有一个SharedPreferences的实现可以加密密钥和值。你可以在Android JetPack Security中使用EncryptedSharedPreferences

只需在build.gradle中添加AndroidX Security:

implementation 'androidx.security:security-crypto:1.0.0-rc01'

你可以这样使用它:

String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);


SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
"secret_shared_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);


// use the shared preferences and editor as you normally would
SharedPreferences.Editor editor = sharedPreferences.edit();

查看更多详细信息:https://android-developers.googleblog.com/2020/02/data-encryption-on-android-with-jetpack.html

官方文档:https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences

我就是这么做的。

这不会在严格模式下给出错误。

public class UserPreferenceUtil
{


private static final String  THEME = "THEME";
private static final String  LANGUAGE = "LANGUAGE";


public static String getLanguagePreference(Context context)
{


String lang = getPreferenceByKey(context,LANGUAGE);


if( lang==null || "System".equalsIgnoreCase(lang))
{
return null;
}




return lang;
}


public static void saveLanguagePreference(Context context,String value)
{
savePreferenceKeyValue(context, LANGUAGE,value);
}


public static String getThemePreference(Context context)
{


return getPreferenceByKey(context,THEME);
}


public static void saveThemePreference(Context context, String value)
{
savePreferenceKeyValue(context,THEME,value);


}


public static String getPreferenceByKey(Context context, String preferenceKey )
{
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);


String value = sharedPreferences.getString(preferenceKey, null);


return value;
}


private static void savePreferenceKeyValue(Context context, String preferenceKey, String value)
{
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(preferenceKey,value);
editor.apply();


}




}

我的应用程序不需要密码。但是,我不会保存密码或加密密码,而是保存单向散列。当用户登录时,我将以同样的方式哈希输入,并将其与存储的哈希匹配。