317

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

我试图用 Shared Preferences 来做到这一点,但不确定这是否是最好的解决方案。

对于如何在 Android 应用程序中存储用户值/设置的任何建议,我将不胜感激。

4

15 回答 15

238

一般来说,SharedPreferences 是您存储首选项的最佳选择,所以总的来说,我建议使用这种方法来保存应用程序和用户设置。

这里唯一需要关注的是您要保存的内容。存储密码总是一件棘手的事情,我会特别小心将它们存储为明文。Android 架构是这样的,您的应用程序的 SharedPreferences 被沙箱化以防止其他应用程序能够访问这些值,因此那里有一些安全性,但是对手机的物理访问可能会允许访问这些值。

如果可能的话,我会考虑修改服务器以使用协商的令牌来提供访问权限,例如OAuth。或者,您可能需要构建某种加密存储,尽管这并非易事。至少,确保在将密码写入磁盘之前对其进行了加密。

于 2009-04-24T16:20:35.243 回答
212

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

}
于 2011-06-18T02:42:09.297 回答
29

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

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

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

于 2009-05-04T08:50:26.707 回答
10

使用 Richard 提供的代码段,您可以在保存密码之前对其进行加密。然而,preferences API 并没有提供一种简单的方法来截取值并对其进行加密——您可以阻止它通过 OnPreferenceChange 侦听器保存,并且理论上您可以通过preferenceChangeListener 对其进行修改,但这会导致无限循环。

我之前曾建议添加“隐藏”首选项以实现此目的。这绝对不是最好的方法。我将介绍另外两个我认为更可行的选择。

首先,最简单的,是在一个preferenceChangeListener中,你可以获取输入的值,加密它,然后将它保存到另一个preferences文件中:

  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、@OverridesetText()getText()方法,以便setText()加密密码并getText()返回 null。

于 2011-03-02T14:56:26.387 回答
6

好的; 自从答案有点混杂以来已经有一段时间了,但这里有一些常见的答案。我疯狂地研究了这个,很难找到一个好的答案

  1. MODE_PRIVATE 方法通常被认为是安全的,如果您假设用户没有 root 设备。您的数据以纯文本形式存储在文件系统的一部分中,只能由原始程序访问。这使得在有根设备上使用另一个应用程序轻松获取密码。再说一次,你想支持有根设备吗?

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

所以,现在我们在“如何处理密钥?” 部分。这是困难的部分。得到钥匙原来不是那么糟糕。您可以使用密钥派生函数获取一些密码并使其成为非常安全的密钥。您确实会遇到诸如“您使用 PKFDF2 进行了多少次通过?”之类的问题,但这是另一个话题

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

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

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

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

你可以做很多这些变化。例如,您可以使用快速 PIN(派生)代替完整的登录序列。快速 PIN 可能不如完整的登录序列安全,但它比纯文本安全许多倍

于 2013-05-14T13:21:53.447 回答
5

我知道这有点死灵,但你应该使用 Android AccountManager。它是专门为这种情况而设计的。这有点麻烦,但它所做的一件事就是在 SIM 卡发生变化时使本地凭据失效,因此,如果有人刷你的手机并将新的 SIM 卡放入其中,你的凭据不会受到损害。

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

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

于 2012-04-04T18:04:40.760 回答
5

我将把我的帽子扔进戒指,只是为了谈论在 Android 上保护密码的一般性。在 Android 上,设备二进制文件应该被认为是受损的——这对于任何直接由用户控制的最终应用程序都是一样的。从概念上讲,黑客可以使用对二进制文件的必要访问来反编译它并根除您的加密密码等。

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

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

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

于 2014-10-30T17:00:00.567 回答
4

这是根据问题标题(就像我一样)到达这里的人的补充答案,不需要处理与保存密码相关的安全问题。

如何使用共享首选项

用户设置通常使用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在示例中使用了 an,但您也可以使用putString(), putBoolean(), getString(),getBoolean()等。

  • 有关更多详细信息,请参阅文档

  • 有多种方法可以获取 SharedPreferences。请参阅此答案以了解要注意的事项。

于 2017-10-25T02:32:51.503 回答
2

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

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

它类似于这里的其他一些方法。希望有帮助:)

于 2013-09-21T21:51:40.363 回答
1

此答案基于 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/的一些代码,尽管具有不同的值对于密钥和初始化向量。

于 2013-09-06T05:49:10.227 回答
1

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

敏感数据应安全存储或根本不存储。如果设备丢失或恶意软件感染,不安全存储的数据可能会受到损害。

于 2013-12-03T10:40:56.973 回答
1

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

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

使用这种方法,您可以生成一个公钥/私钥对,其中的私钥由 Android 安全存储和管理。

这是有关如何执行此操作的链接:Android KeyStore Tutorial

于 2018-11-07T13:23:21.883 回答
0

正如其他人已经指出的那样,您通常可以使用 SharedPreferences 但如果您想存储加密的数据,这有点不方便。幸运的是,现在有一种更简单、更快捷的方式来加密数据,因为有一个 SharedPreferences 的实现可以加密键和值。您可以在 Android JetPack Security中使用EncryptedSharedPreferences 。

只需将 AndroidX Security 添加到您的 build.gradle 中:

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

于 2021-02-18T05:30:26.670 回答
-3

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

于 2009-04-27T06:09:44.563 回答
-3

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

于 2013-10-18T08:41:05.857 回答