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

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

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


当前回答

正如其他人已经指出的,你可以使用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

其他回答

我同意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);
        }
    }

}

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

The MODE_PRIVATE method is considered generally safe, if you assume that the user didn't root the device. Your data is stored in plain text in a part of the file system that can only be accessed by the original program. This makings grabbing the password with another app on a rooted device easy. Then again, do you want to support rooted devices? AES is still the best encryption you can do. Remember to look this up if you are starting a new implementation if it's been a while since I posted this. The largest issue with this is "What to do with the encryption key?"

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

Ideally, you store the AES key off the device. You have to figure out a good way to retrieve the key from the server safely, reliably, and securely though You have a login sequence of some sort (even the original login sequence you do for remote access). You can do two runs of your key generator on the same password. How this works is that you derive the key twice with a new salt and a new secure initialization vector. You store one of those generated passwords on the device, and you use the second password as the AES key.

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

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

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

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

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

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

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

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

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

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

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

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