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

我正在尝试使用“共享首选项”来执行此操作,但是不确定这是否是最佳解决方案。

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

#1 楼

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

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

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

评论


您能解释一下沙盒的意思吗?

–阿比吉特
2012年7月3日在15:00

沙盒程序是指其进程和信息(例如那些共享的首选项)对其余应用程序隐藏的任何应用程序。在一个程序包中运行的android应用程序无法直接访问另一个程序包中的任何内容。这就是为什么同一个程序包(始终由您自己)中的应用程序可以访问其他程序包中的信息的原因

–霍霍利斯人
2012年7月12日在20:46

@Reto Meier我的要求是保护我正在使用的令牌的公开Web服务,将其存储在共享首选项中是否安全?我在我的应用程序中有一个启动广播接收器,如果发现设备为root用户,它将删除所有sharedpreferences数据。这足以保护我的令牌。

– pyus13
2013年3月11日19:47



根据android-developers.blogspot.com/2013/02/…,用户凭据应与MODE_PRIVATE标志一起存储并存储在内部存储设备中(关于在本地存储最终将受到攻击的任何密码的相同警告)。就是说,就混淆本地存储数据的有效性而言,是否将MODE_PRIVATE与SharedPreferences一起使用等同于对内部存储上创建的文件执行相同操作?

– qix
13-10-18在20:06



不要在共享首选项中存储密码。如果用户丢失了电话,则说明他们已经丢失了密码。它将被读取。如果他们在其他地方使用了该密码,则使用该密码的每个地方都会受到威胁。此外,您已经永久丢失了该帐户,因为使用密码可以更改您的密码。正确的方法是将密码一次发送到服务器,然后再收到一个登录令牌。将其存储在共享首选项中,并随每个请求一起发送。如果该令牌被泄露,则不会丢失任何其他内容。

–Gabe Sechan
2014年7月27日在2:31

#2 楼

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

}


评论


FYI Base64在API级别8(2.2)和更高版本中可用。对于较早的操作系统,可以使用iharder.sourceforge.net/current/java/base64或其他方式。

– emmby
11年6月20日在22:19

是的,我写了这个。随时使用,无需注明出处

– emmby
2012年9月10日下午16:21

我同意你的看法。但是,如果仅在服务器上使用密码,为什么不使用公用/专用密钥加密?保存密码时客户端的公钥。客户端将不再需要再次读取明文密码,对吗?然后,服务器可以使用私钥对其进行解密。因此,即使有人浏览了您的应用程序源代码,他们也无法获得密码,除非他们入侵了您的服务器并获得了私钥。

–帕特里克·布斯(Patrick Boos)
2012-09-24的1:43



我已向此代码添加了一些功能,并将其放在github.github.com/RightHandedMonkey/WorxForUs_Library/blob/master/src/…上。现在,它可以处理将非加密首选项迁移到加密首选项的问题。此外,它还在运行时生成密钥,因此反编译应用程序不会释放密钥。

– RightHandedMonkey
14年4月23日在15:15

后来添加了,但是@PatrickBoos的评论是个好主意。但是,与此有关的一个问题是,即使您已经加密了密码,但是盗用该密码的攻击者仍然可以登录到您的服务器,因为您的服务器进行了解密。此方法的另一项功能是将密码和时间戳一起加密。这样,您可以决定例如只允许保存最近的密码(例如将过期日期添加到“令牌”中),或者甚至要求某些用户自特定日期起添加时间戳(让您“撤消”旧的“令牌”)。

–adevine
14-10-23在21:55

#3 楼

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

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


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

评论


对于这种简单的方法,我完全同意。但是,您应该始终担心存储的密码的安全性吗?根据您的应用程序,您可能对被盗的个人信息负有责任。只需为任何试图将实际密码存储到银行帐户等重要内容的人指出这一点。我仍然投票给你。

–虽然-E
2011年6月16日4:21



您将把存储密码的密钥存储在哪里?如果其他用户可以访问共享的首选项,那么密钥也是如此。

– OrhanC1
2014年8月7日14:08

@ OrhanC1你得到答案了吗?

– eRaisedToX
17年8月19日在12:04



#4 楼

使用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。 br />

评论


我知道这已经很老了,但是请介意为自定义版本的EditTextPreference发布代码吗?

–RenniePet
2013年9月6日在2:50

没关系,我在这里groups.google.com/forum/#!topic/android-developers/pMYNEVXMa6M上找到了可用的示例,并且现在可以正常使用了。感谢您提出这种方法。

–RenniePet
2013年9月6日下午5:04

#5 楼

好的;由于答案有点混杂,已经有一段时间了,但这是一些常见的答案。我像疯了似的研究了这个问题,并且很难建立一个很好的答案。如果您假设用户没有使用设备的root权限,那么MODE_PRIVATE方法通常被认为是安全的。您的数据以纯文本格式存储在文件系统的一部分中,该文件系统只能由原始程序访问。这样一来,在有根设备上使用另一个应用程序即可轻松获取密码。再说一次,您是否要支持有根设备?
AES仍然是您可以做的最好的加密。自从我发布此代码以来已经有一段时间了,请记住如果您要启动一个新的实现,请查阅此内容。与此有关的最大问题是“如何处理加密密钥?”

因此,现在我们处于“如何处理密钥?”的问题。一部分。这是困难的部分。拿到钥匙还算不错。您可以使用密钥派生功能来获取一些密码,并将其设为非常安全的密钥。您确实遇到了诸如“您用PKFDF2进行了多少次通行证?”之类的问题,但这是另一个主题。



理想情况下,您将AES密钥存储在设备之外。您必须找出一种安全,可靠和安全地从服务器检索密钥的好方法,尽管您有某种登录顺序(甚至是用于远程访问的原始登录顺序)。您可以使用相同的密码运行两次密钥生成器。这是如何工作的,您需要使用新的盐和新的安全初始化向量来两次导出密钥。您将其中一个生成的密码存储在设备上,然后将第二个密码用作AES密钥。

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


使用“通常安全”的方法,您可以使用AES加密数据并将密钥存储在MODE_PRIVATE中。这是最近发表的Android博客文章推荐的。它不是非常安全,但是对于某些人而言,使用纯文本更好。

您可以对它们进行很多更改。例如,您可以执行快速PIN(派生)来代替完整的登录序列。快速PIN可能不像完整的登录顺序那样安全,但是它比纯文本安全很多倍

#6 楼

我知道这有点死法,但是您应该使用Android AccountManager。它是为这种情况专门设计的。这有点麻烦,但是它的作用之一是,如果SIM卡发生更改,会使本地凭据失效,因此,如果有人轻扫您的手机并向其中扔新的SIM卡,您的凭据将不会受到损害。

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

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

评论


请注意,使用AccountManager并不比上面提供的任何其他方法更安全! developer.android.com/training/id-auth/…

– Sander Versluys
2012年11月8日上午10:14

AccountManager的用例是必须在不同应用程序之间以及来自不同作者的应用程序之间共享帐户。存储密码并将其提供给任何请求的应用程序是不合适的。如果用户/密码仅用于单个应用程序,请不要使用AccountManager。

–杜尔门
2012年11月12日,0:34

@dolmen,那不是很正确。 AccountManager不会将帐户密码提供给UID与身份验证器不匹配的任何应用程序。名字,是的; auth令牌,是;密码,否。如果尝试,它将抛出SecurityException。用例远不止于此。 developer.android.com/training/id-auth/identify.html

–乔恩·奥(Jon O)
2012年11月12日15:37

#7 楼

我会大声疾呼,只是为了谈论一般在Android上保护密码的问题。在Android上,应将设备二进制文件视为已泄露-对于任何直接受用户控制的最终应用程序都是相同的。从概念上讲,黑客可以使用对二进制文件的必要访问权对其进行反编译,并清除加密的密码等。

因此,如果安全性很重要,我有两个建议可以扔掉您的担心:

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

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

#8 楼

这是针对那些根据问题标题到达此处的人的补充答案(就像我一样),并且不需要处理与保存密码有关的安全性问题。
如何使用共享首选项
用户设置通常使用带有键值对的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()等。有关更多详细信息,请参见文档。


有多种获取SharedPreferences的方法。请参阅此答案以查找所需内容。



#9 楼

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

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

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

#10 楼

该答案基于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);
    }
}


这显示了如何使用-这是“ items”文件驱动首选项显示。请注意,它包含三个普通的EditTextPreference视图和一个自定义EditPasswordPreference视图。

对于实际的加密/解密,留给读者练习。我目前正在基于本文http://zenu.wordpress.com/2011/09/21/aes-128bit-cross-platform-java-and-c-encryption-compatibility/使用一些代码,尽管它们的值不同键和初始化向量。

#11 楼

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

敏感数据应该安全存储或完全不存储。如果设备丢失或感染了恶意软件,则不安全存储的数据可能会受到威胁。

#12 楼

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

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

使用此方法,您可以生成一个公用/专用密钥对,其中专用密钥对由Android安全地存储和管理。

此处是有关如何执行此操作的链接:Android KeyStore教程

#13 楼

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

#14 楼

您需要使用sqlite的安全性存储密码。
这是存储密码的最佳示例-密码安全。
这是源代码和说明的链接-
http ://code.google.com/p/android-passwordsafe/

评论


OP需要存储一对用户名和密码。考虑为此创建一个整个数据库表是荒谬的

– HXCaine
2010年5月25日下午14:27

@HXCaine我非常不同意-我可以看到用户/密码sqlite表的至少1种其他用法。例如,如果您考虑(使用sqlite的)ACCEPTABLE的风险,除了简单的应用程序登录身份验证之外,您还可以使用该表存储多个ftp密码(如果您的应用程序使用ftp-有时是我的)。此外,创建这种操作的sqlite适配器类非常简单。

–tony gil
2012年7月22日14:40

很高兴复活了两年的评论!公平地说,我的评论是在回答之后的一年:)即使使用少量的FTP密码,在空间和编码方面,SQLite表的开销也要比SharedPreferences大得多。当然没必要

– HXCaine
2012年7月24日在21:01