首页 文章

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

提问于
浏览
287

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

我试图使用共享偏好设置,但不确定这是否是最佳解决方案 .

我将不胜感激任何有关如何在Android应用程序中存储用户值/设置的建议 .

14 回答

  • 2

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

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

    使用此方法,您可以生成公钥/私钥对,其中私有密钥对由Android安全存储和管理 .

    以下是如何执行此操作的链接:Android KeyStore Tutorial

  • 28

    你需要使用sqlite,security apit来存储密码 . 这是最好的例子,它存储密码, - passwordsafe . 这里是源和解释的链接 - http://code.google.com/p/android-passwordsafe/

  • 0

    通常,SharedPreferences是存储首选项的最佳选择,因此通常我建议使用这种方法来保存应用程序和用户设置 .

    这里唯一关注的领域是你正在拯救的东西 . 密码总是存储起来很棘手,我特别担心将它们存储为明文 . Android体系结构使得应用程序的SharedPreferences被沙箱化,以防止其他应用程序访问这些值,因此存在一些安全性,但对手机的物理访问可能允许访问这些值 .

    如果可能的话,我会考虑修改服务器以使用协商令牌来提供访问,例如OAuth . 或者,您可能需要构建某种加密存储,尽管's non-trivial. At the very least, make sure you'在将密码写入磁盘之前对其进行加密 .

  • 1

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

    关于在Android Activity中存储单个首选项的最简单方法是执行以下操作:

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

    如果您担心这些安全性,那么您可以在存储之前始终加密密码 .

  • 226

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

  • 5

    使用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,@覆盖 setText()getText() 方法,以便 setText() 加密密码, getText() 返回null .

  • 1

    我知道这有点神秘,但你应该使用Android AccountManager . 它有点麻烦,但如果SIM卡发生变化,它所做的一件事就是使本地凭证失效,所以如果有人刷你的手机并在其中抛出新的SIM卡,你的凭证就不会受到损害 .

    这也为用户提供了一种快速简便的方法,可以从一个位置访问(并可能删除)他们在设备上拥有的任何帐户的存储凭据 .

    SampleSyncAdapter是一个使用存储帐户凭据的示例 .

  • 202

    好的;已经有一段时间了,因为答案有点混乱,但这里有一些常见的答案 . 我研究这个就像疯了一样,很难 Build 一个好的答案

    • 如果,MODE_PRIVATE方法通常被认为是安全的您假设用户没有root设备 . 您的数据以纯文本格式存储在文件系统的一部分中,只能由原始程序访问 . 这个makings很容易在root设备上使用另一个应用程序获取密码 . 那么,你想支持root设备吗?

    • AES仍然是您可以做的最佳加密 . 如果你开始一个新的实现,如果我发布这个已经有一段时间了,请记得查看这个 . 最大的问题是“如何处理加密密钥?”

    那么,现在我们正处于“如何处理密钥?”一部分 . 这是困难的部分 . 获得密钥并没有那么糟糕 . 您可以使用密钥派生函数来获取一些密码并使其成为非常安全的密钥 . 您确实会遇到诸如“您使用PKFDF2传递多少次?”等问题,但这是另一个主题

    • 理想情况下,您将AES密钥存储在设备上 . 您必须找到一种安全,可靠和安全地从服务器检索密钥的好方法

    • 您有某种登录顺序(甚至是您为远程访问所做的原始登录顺序) . 您可以使用相同的密码执行两次密钥生成器运行 . 这是如何工作的,您使用新的盐和新的安全初始化向量导出密钥两次 . 您将其中一个生成的密码存储在设备上,并使用第二个密码作为AES密钥 .

    登录时,您将在本地登录名上重新导出密钥并将其与存储的密钥进行比较 . 完成后,您将使用派生密钥#2进行AES .

    • 使用"generally safe"方法,使用AES加密数据并将密钥存储在MODE_PRIVATE中 . 这是最近推出的Android博客文章推荐的 . 不是非常安全,但对于一些人来说比普通文本更好

    你可以做很多这些变化 . 例如,您可以执行快速PIN(派生),而不是完整登录序列 . 快速PIN可能不如完整登录序列安全,但它比纯文本安全多倍

  • -2

    我会把我的帽子扔进戒指,只是为了谈论在Android上保护密码 . 在Android上,设备二进制文件应被视为已泄露 - 对于任何直接用户控制的终端应用程序而言,这都是相同的 . 从概念上讲,黑客可以使用对二进制文件的必要访问来对其进行反编译并根除您的加密密码等 .

    因此,如果安全性是您的主要关注点,那么我有两条建议可以抛弃:

    1)不要存储实际密码 . 存储授予的访问令牌,并使用访问令牌和电话签名对会话服务器端进行身份验证 . 这样做的好处是,您可以使令牌具有有限的持续时间,您不会破坏原始密码,并且您有一个很好的签名,您可以使用该签名以便稍后与流量相关联(例如,检查入侵尝试并使其无效)令牌呈现它无用) .

    2)利用2因素认证 . 这可能更令人讨厌和侵扰,但对于某些合规情况是不可避免的 .

  • 9

    您还可以查看这个包含您提到的功能的小lib .

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

    它类似于其他一些方法 . 希望有帮助:)

  • 2

    这是基于问题 Headers 到达此处的人的补充答案(就像我一样),并且不需要处理与保存密码相关的安全问题 .

    如何使用共享首选项

    用户设置通常使用带有键值对的 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);
    

    如果未找到密钥,则使用默认值 .

    注意事项

    • 而不是像我上面那样在多个地方使用本地键字符串,最好在单个位置使用常量 . 您可以在设置活动的顶部使用类似的内容:
    final static String PREF_MY_INT_KEY = "myInt";
    
    • 我在我的示例中使用了 int ,但您也可以使用 putString()putBoolean()getString()getBoolean() 等 .

    • 有关详细信息,请参阅documentation .

    • 有多种方法可以获得SharedPreferences . 有关注意事项,请参阅this answer .

  • 5

    这个答案是基于马克建议的方法 . 创建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/的一些代码,尽管键和初始化向量的值不同 .

  • -2

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

    敏感数据应该安全存储或根本不存储 . 在设备丢失或恶意软件感染的情况下,不安全地存储的数据可能会受到损害 .

相关问题