Answers:
通常,SharedPreferences是存储首选项的最佳选择,因此,一般而言,我建议使用这种方法来保存应用程序和用户设置。
这里唯一需要关注的是您要保存的内容。密码存储总是一件棘手的事情,我特别要警惕以明文形式存储它们。Android架构使您的应用程序的SharedPreferences被沙盒化,以防止其他应用程序能够访问这些值,因此那里有一些安全性,但是对手机的物理访问可能会允许访问这些值。
如果可能的话,我会考虑修改服务器以使用协商的令牌来提供访问权限,例如OAuth。或者,您可能需要构造某种加密存储,尽管这并非易事。至少,在将密码写入磁盘之前,请确保已对密码进行了加密。
MODE_PRIVATE
混淆本地存储数据的有效性而言,与SharedPreferences一起使用是否等同于对内部存储上创建的文件进行相同的处理?
我同意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活动中存储单个首选项的最简单方法是执行以下操作:
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。
好的; 由于答案有点混杂,已经有一段时间了,但这是一些常见的答案。我像疯了一样研究了这个问题,很难找到一个好的答案
如果您假设用户未root设备,则通常认为MODE_PRIVATE方法是安全的。您的数据以纯文本格式存储在文件系统的一部分中,该文件系统只能由原始程序访问。这样一来,便可以轻松在具有根目录的设备上使用另一个应用程序获取密码。再说一次,您是否要支持有根设备?
AES仍然是您可以做的最好的加密。自从我发布此代码以来已经有一段时间了,请记住如果您要启动一个新的实现,请仔细阅读。最大的问题是“如何处理加密密钥?”
因此,现在我们处于“如何使用钥匙?” 一部分。这是困难的部分。拿到钥匙还算不错。您可以使用密钥派生功能来获取一些密码,并使之成为相当安全的密钥。您确实遇到了诸如“您对PKFDF2做多少遍?”的问题,但这是另一个主题。
理想情况下,将AES密钥存储在设备之外。您必须找出一种安全,可靠和安全地从服务器检索密钥的好方法
您有某种登录顺序(甚至是您用于远程访问的原始登录顺序)。您可以使用相同的密码运行两次密钥生成器。这是如何工作的,您需要使用新的盐和新的安全初始化向量来两次导出密钥。您将这些生成的密码之一存储在设备上,并将第二个密码用作AES密钥。
登录时,可以重新派生本地登录名上的密钥,并将其与存储的密钥进行比较。完成后,将派生密钥2用于AES。
您可以对它们进行很多变化。例如,您可以执行快速PIN(派生)来代替完整的登录序列。快速PIN可能不像完整的登录顺序一样安全,但是它比纯文本安全很多倍
我知道这有点死法,但是您应该使用Android AccountManager。它是为这种情况而专门设计的。这有点麻烦,但是它的作用之一是,如果SIM卡发生更改,会使本地凭据失效,因此,如果有人轻扫您的手机并向其中扔新的SIM卡,您的凭据将不会受到损害。
这也为用户提供了一种快速简便的方法,使他们可以从一个地方访问(并可能删除)他们在设备上拥有的任何帐户的存储凭据。
SampleSyncAdapter是使用存储的帐户凭据的示例。
我会大声疾呼,只是为了谈论一般在Android上保护密码的问题。在Android上,应将设备二进制文件视为已泄露-对于任何直接由用户控制的最终应用程序都是相同的。从概念上讲,黑客可以使用对二进制文件的必要访问权来对其进行反编译,并根除您的加密密码等。
因此,如果您非常关注安全性,那么我想提出两个建议:
1)不要存储实际密码。存储授予的访问令牌,并使用访问令牌和电话的签名来验证会话服务器端。这样做的好处是,您可以使令牌具有有限的持续时间,不会破坏原始密码,并且具有良好的签名,可用于以后与流量相关联(例如,检查入侵尝试并使密码无效)。令牌使其变得无用)。
2)利用2要素验证。这可能更令人烦恼和干扰,但对于某些合规性情况是不可避免的。
您也可以签出这个小库,其中包含您提到的功能。
https://github.com/kovmarci86/android-secure-preferences
它与此处的其他一些方法类似。希望可以帮助:)
对于那些根据问题标题到达这里的人(像我一样),这是一个补充性答案,不需要处理与保存密码有关的安全性问题。
用户设置通常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);
如果找不到密钥,则使用默认值。
该答案基于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);
}
}
这显示了如何使用它-这是驱动首选项显示的“项目”文件。请注意,它包含三个普通的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/使用一些代码,尽管它们的值不同键和初始化向量。
首先,我认为不应将用户的数据存储在手机上,如果必须将数据存储在手机上的某处,则应在应用程序的私有数据中对其进行加密。用户凭据的安全性应该是应用程序的优先级。
敏感数据应安全存储或完全不存储。如果设备丢失或感染了恶意软件,则不安全存储的数据可能会受到威胁。
我使用Android KeyStore在ECB模式下使用RSA加密密码,然后将其保存在SharedPreferences中。
当我想找回密码时,我从SharedPreferences中读取了加密的密码,并使用KeyStore对其进行了解密。
使用此方法,您可以生成一个公用/专用密钥对,其中专用密钥对由Android安全存储和管理。
这是有关如何执行此操作的链接:Android KeyStore教程
共享首选项是存储我们的应用程序数据的最简单方法。但是任何人都有可能通过应用程序管理器清除我们的共享首选项数据。所以我认为这对于我们的应用程序不是完全安全的。
您需要使用安全的sqlite存储密码。这是存储密码的最佳示例-passwordsafe。这是来源和说明的链接-http: //code.google.com/p/android-passwordsafe/