在Android应用程序中存储用户设置的最合适的方式是什么?
我正在创build一个使用用户名/密码连接到服务器的应用程序,我想启用“保存密码”选项,这样用户在每次启动应用程序时都不必input密码。
我正在尝试使用共享首选项,但不知道这是否是最佳解决scheme。
我将不胜感激任何有关如何在Android应用程序中存储用户值/设置的build议。
一般来说,SharedPreferences是存储首选项的最佳select,所以通常我会推荐这种方法来保存应用程序和用户设置。
这里唯一值得关注的是你正在保存的东西。 密码始终是一个棘手的事情要存储,我会特别谨慎的存储他们作为明文。 Android体系结构是这样的,你的应用程序的SharedPreferences被沙箱化,以防止其他应用程序访问这些值,所以这里有一些安全性,但是物理访问手机可能允许访问这些值。
如果可能的话,我会考虑修改服务器以使用协商令牌来提供访问权限,如OAuth 。 另外,您可能需要构build某种types的encryption存储,尽pipe这不是微不足道的。 至less,确保在将密码写入磁盘之前对密码进行encryption。
我同意Reto和fiXedd。 客观地说,在SharedPreferences中对密码进行encryption没有太多的意义,因为任何攻击者都可以访问你的应用程序的二进制文件,因此也可以使用这些密钥来解密密码。
然而,话虽如此,似乎有一个宣传倡议继续确定移动应用程序,在SharedPreferences明文存储他们的密码,并对这些应用程序的光照不利。 有关示例,请参阅http://blogs.wsj.com/digits/2011/06/08/some-top-apps-put-data-at-risk/和http://viaforensics.com/appwatchdog 。
虽然我们需要更多地关注安全问题,但我认为对这一问题的这种关注实际上并没有显着增加我们的整体安全。 然而,感觉是一样的,这里有一个解决scheme来encryption你放置在SharedPreferences中的数据。
只需将自己的SharedPreferences对象封装在这个对象中,任何读取/写入的数据都将被自动encryption和解密。 例如。
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 Activity中存储单个首选项的最简单的方法是做这样的事情:
Editor e = this.getPreferences(Context.MODE_PRIVATE).edit(); e.putString("password", mPassword); e.commit();
如果您担心这些密码的安全性,那么在存储密码之前,您总是可以encryption密码。
使用Richard提供的代码片段,您可以在保存密码之前encryption密码。 然而,首选项API并不提供简单的方法来截取值并对其进行encryption – 您可以通过OnPreferenceChange侦听器来阻止它被保存,理论上可以通过preferenceChangeListener对其进行修改,但是会导致无限循环。
我曾经build议增加一个“隐藏”的偏好,以完成这一点。 这绝对不是最好的方法。 我将介绍另外两个我认为更可行的选项。
首先,最简单的是,在preferenceChangeListener中,可以获取input的值,对其进行encryption,然后将其保存到备用首选项文件中:
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; }
第二种方法,我现在更喜欢的方式是创build自己的自定义首选项,扩展EditTextPreference,@覆盖setText()
和getText()
方法,以便setText()
encryption密码, getText()
返回空值。
好的; 已经有一段时间了,因为答案是混合的,但是这里有一些常见的答案。 我研究这个很疯狂,很难build立一个好的答案
-
如果您假定用户没有root设备,MODE_PRIVATE方法通常是安全的。 您的数据以纯文本forms存储在只能由原始程序访问的文件系统的一部分中。 这种方法可以很容易地在另一个应用程序上抓取密码。 那么,你想支持植根设备吗?
-
AES仍然是你可以做的最好的encryption。 请记住,如果你正在开始一个新的实现,如果自从发布了这一段时间以来已经有一段时间了。 最大的问题是“如何处理encryption密钥?”
所以,现在我们在“关键是怎么做?” 一部分。 这是困难的部分。 获得钥匙原来并不是那么糟糕。 您可以使用密钥派生函数来获取一些密码,并使其成为一个非常安全的密钥。 你确实遇到了“你用PKFDF2做多less次传球?”这样的问题,但这是另一个话题
-
理想情况下,您将AES密钥存储在设备上。 你必须找出一个好的方法来安全,可靠,安全地从服务器上检索密钥
-
你有一个login序列(甚至是你远程访问的原始login序列)。 您可以使用相同的密码对密钥生成器执行两次运行。 这是如何工作的,你用新的salt和一个新的安全初始化向量来导出密钥两次。 您将其中一个生成的密码存储在设备上,并使用第二个密码作为AES密钥。
login时,您将重新派生本地login的密钥并将其与存储的密钥进行比较。 一旦完成,您使用派生密钥#2进行AES。
- 使用“通常安全”的方法,您使用AESencryption数据并将密钥存储在MODE_PRIVATE中。 这是最近Android的博客文章推荐的。 不是非常安全,但对于纯文本的某些人更好
你可以做很多这些变化。 例如,而不是一个完整的login序列,你可以做一个快速的PIN(派生)。 快速PIN可能不像完整的login序列那么安全,但是它比纯文本多得多
我会把我的帽子扔进戒指,只是为了谈论在Android安全密码。 在Android上,设备二进制文件应该被认为是被攻破的 – 对于直接用户控制的任何terminal应用程序来说,这是相同的。 从概念上讲,黑客可以使用对二进制文件的必要访问来对其进行反编译并根据您的encryption密码等进行反编译。
因此,如果安全性是您的主要担忧,那么我想要抛出两条build议:
1)不要存储实际的密码。 存储授予的访问令牌,并使用访问令牌和电话的签名来validation会话服务器端。 这样做的好处是,您可以使令牌的持续时间有限,不会损害原始密码,并且您可以使用以后用于关联stream量的良好签名(例如,检查入侵企图并使无效令牌渲染它没用)。
2)利用2因素authentication。 这可能更令人讨厌和侵入,但对于一些遵守情况是不可避免的。
我知道这有一点necromancy,但你应该使用Android AccountManager 。 这是专门为这种情况build立的。 这有点麻烦,但是它所做的一件事情是,如果SIM卡改变,则本地凭证无效,所以如果有人在你的手机上刷了一个新的SIM,那么你的凭证就不会受到威胁。
这也为用户提供了一种快速而简单的方法,可以从一个地方访问(并可能删除)他们在设备上拥有的任何帐户的存储凭据。
SampleSyncAdapter是一个使用存储帐户凭证的示例。
你也可以看看这个小小的库,包含你提到的function。
https://github.com/kovmarci86/android-secure-preferences
这和其他的一些问题类似。 希望帮助:)
这个答案是基于马克build议的方法。 创buildEditTextPreference类的自定义版本,该视图在视图中显示的纯文本与存储在首选项存储中的密码的encryption版本之间来回转换。
正如大多数人在这个线程中已经指出的那样,这不是一个非常安全的技术,虽然安全程度部分取决于所使用的encryption/解密代码。 但它相当简单和方便,并将阻止大多数随意窥探。
以下是自定义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); } }
这显示了如何使用它 – 这是驱动首选项显示的“项目”文件。 注意它包含三个普通的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>
至于实际的encryption/解密,这是留给读者的一个练习。 我目前正在使用一些基于这篇文章的代码http://zenu.wordpress.com/2011/09/21/aes-128bit-cross-platform-java-and-c-encryption-compatibility/ ,虽然有不同的值为密钥和初始化向量。
首先,我认为用户的数据不应该存储在手机上,如果必须在手机上某处存储数据,则应该使用应用程序的私人数据进行encryption。 用户证书的安全性应该是应用程序的优先级。
敏感数据应该安全地存储或根本不存储。 在设备丢失或恶意软件感染的情况下,不安全地存储的数据可能被损害。
对于那些根据问题标题来到这里的人来说,这是一个补充的答案(就像我做的那样),不需要处理与保存密码相关的安全问题。
如何使用共享首选项
用户设置通常使用带有键值对的SharedPreferences
在本地保存。 您使用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);
如果未find密钥,则使用默认值。
笔记
-
而不是像上面所做的那样在多个地方使用本地密钥string,最好是在一个位置使用常量。 您可以在设置活动的顶部使用类似这样的内容:
final static String PREF_MY_INT_KEY = "myInt";
-
我在我的例子中使用了一个
int
,但是也可以使用putBoolean()
,putBoolean()
,getString()
,getBoolean()
等。 - 请参阅文档了解更多详情。
- 有多种方法可以获得SharedPreferences。 看到这个答案要注意什么。
你需要使用sqlite,安全apit来存储密码。 这里是最好的例子,它存储密码, – 密码保护。 这里是链接的来源和解释 – http://code.google.com/p/android-passwordsafe/
共享首选项是存储我们的应用程序数据的最简单方法。 但有可能任何人都可以通过应用程序pipe理器清除我们的共享偏好数据。所以我不认为这对我们的应用程序是完全安全的。