在Android应用程序中存储用户设置的最合适方法是什么
我正在创建一个使用用户名/密码连接到服务器的应用程序,我想启用“保存密码”选项,以便用户不必在每次应用程序启动时都键入密码。
我试图使用共享首选项执行此操作,但不确定这是否是最佳解决方案。
我将不胜感激关于如何在Android应用程序中存储用户值/设置的任何建议。
我正在创建一个使用用户名/密码连接到服务器的应用程序,我想启用“保存密码”选项,以便用户不必在每次应用程序启动时都键入密码。
我试图使用共享首选项执行此操作,但不确定这是否是最佳解决方案。
我将不胜感激关于如何在Android应用程序中存储用户值/设置的任何建议。
一般来说,共享首选项是存储首选项的最佳选择,因此一般来说,我建议使用这种方法来保存应用程序和用户设置。
这里唯一需要关注的领域是您要节省的费用。密码总是一件棘手的事情,我会特别小心将它们存储为明文。Android 架构是这样的,即应用程序的 SharedPreferences 被沙盒化,以防止其他应用程序能够访问这些值,因此存在一些安全性,但对手机的物理访问可能会允许访问这些值。
如果可能的话,我会考虑修改服务器,以使用协商的令牌来提供访问权限,类似于OAuth。或者,您可能需要构建某种加密存储,尽管这并非易事。至少,请确保在将密码写入磁盘之前对其进行加密。
我同意Reto和fiXedd。客观地说,在共享首选项中投入大量时间和精力来加密密码没有多大意义,因为任何有权访问您的首选项文件的攻击者都很可能也有权访问您应用程序的二进制文件,因此密钥可以取消加密密码。
但是,话虽如此,似乎确实有一项宣传计划正在识别在共享首选项中以明文形式存储密码的移动应用程序,并在这些应用程序上显示不利的光线。有关一些示例,请参阅 http://blogs.wsj.com/digits/2011/06/08/some-top-apps-put-data-at-risk/ 和 http://viaforensics.com/appwatchdog。
虽然我们总体上需要更多地关注安全性,但我认为,对这个特定问题的这种关注实际上并没有显着提高我们的整体安全性。但是,感知是这样的,这里有一个解决方案来加密您放置在共享首选项中的数据。
只需将您自己的共享首选项对象包装在此对象中,您读取/写入的任何数据都将自动加密和解密。例如。
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);
}
}
}