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

What is the most appropriate way to store user settings in Android application

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

我试图用共享的首选项来完成这项工作,但不确定这是否是最好的解决方案。

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


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

这里唯一值得关注的地方就是你在保存什么。密码总是一个棘手的事情来存储,我会特别提防他们作为明文存储。Android的体系结构是这样的:应用程序的共享引用是沙盒的,以防止其他应用程序能够访问这些值,因此那里有一些安全性,但是对电话的物理访问可能允许访问这些值。

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


我同意雷托和菲泽德。客观地说,花大量时间和精力加密sharedreferences中的密码没有多大意义,因为任何有权访问您的首选项文件的攻击者也很可能访问您的应用程序的二进制文件,因此解密密码的密钥。

然而,也就是说,似乎确实有一项宣传计划正在进行中,即识别将密码以明文形式存储在共享的引用中的移动应用程序,并对这些应用程序发出不利的光。有关一些示例,请参阅http://blogs.wsj.com/digits/2011/06/08/some-top-apps-put-data-at-risk/和http://viaforensics.com/appwatchdog。

虽然我们一般需要更多地关注安全问题,但我认为这种对这一特定问题的关注实际上并没有显著提高我们的整体安全性。然而,感知是这样的,这里有一个解决方案来加密您在sharedreferences中放置的数据。

只需将您自己的sharedreferences对象包装在这个对象中,您读/写的任何数据都将自动加密和解密。如。

1
2
3
4
5
6
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);

这是班级的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/**
 * 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);
        }
    }

}


在Android活动中存储单个偏好的最简单方法是这样做:

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

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


使用Richard提供的代码片段,您可以在保存密码之前对其进行加密。然而,首选项API并不能提供一种简单的方法来截取值并对其进行加密—您可以通过onPreferenceChangeListener阻止它被保存,理论上您可以通过PreferenceChangeListener修改它,但这会导致一个无止境的循环。

我之前建议添加一个"隐藏的"偏好来完成这一点。这绝对不是最好的方法。我将提出另外两个我认为更可行的选择。

首先,最简单的方法是在PreferenceChangeListener中,您可以获取输入的值,对其进行加密,然后将其保存到另一个首选项文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  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,@override'使用setText()getText()方法,以便setText()加密密码,getText()返回空值。


好吧,已经有一段时间了,答案是混合的,但这里有几个常见的答案。我研究得很疯狂,很难找到一个好答案

  • 如果您假设用户没有根设备,那么模式私有方法通常被认为是安全的。您的数据以纯文本形式存储在文件系统的一部分中,该部分只能由原始程序访问。这使得在根设备上用另一个应用程序获取密码变得容易。然后,您想再次支持根设备吗?

  • AES仍然是最好的加密方法。如果您正在启动一个新的实现,请记住查找这个,如果我发布这个实现已经有一段时间了。最大的问题是"如何处理加密密钥?"

  • 所以,现在我们在"怎么用钥匙?"部分。这是最困难的部分。拿到钥匙并没有那么糟糕。您可以使用密钥派生函数获取一些密码,并使其成为非常安全的密钥。你会遇到这样的问题:"你用pkfdf2能通过多少次?"但这是另一个话题

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

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

  • 当您登录时,您将在本地登录上重新派生密钥,并将其与存储的密钥进行比较。完成后,您将为AES使用派生键2。

  • 使用"一般安全"方法,您可以使用AES加密数据,并将密钥存储在模式u private中。这是最近一篇ISH Android博客文章推荐的。不是非常安全,但对于某些人来说,它比纯文本更安全。
  • 你可以做很多变化。例如,您可以执行一个快速的pin(派生),而不是完整的登录序列。快速PIN可能不像完整的登录序列那样安全,但它比纯文本安全得多。


    我会把我的帽子扔到戒指里,只是为了讨论安卓系统上的密码安全问题。在Android上,设备二进制文件应该被认为是泄露的——这对于直接由用户控制的任何终端应用程序都是一样的。从概念上讲,黑客可以使用对二进制文件的必要访问来对其进行反编译,并根除加密密码等。

    因此,如果安全问题是你的主要问题,我有两个建议要放弃:

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

    2)采用2因素认证。这可能更烦人,更具侵入性,但对于某些合规情况,这是不可避免的。


    我知道这有点死灵,但你应该使用Android账户管理器。它是专门为这个场景构建的。这有点麻烦,但它所做的一件事是,如果SIM卡发生变化,本地凭证将失效,因此,如果有人刷卡并在手机中插入新的SIM卡,您的凭证将不会受到影响。

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

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


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

    如何使用共享首选项

    用户设置通常使用带键值对的SharedPreferences在Android本地保存。使用String键保存或查找相关值。

    写入共享首选项

    1
    2
    3
    4
    5
    6
    String key ="myInt";
    int valueToSave = 10;

    SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
    SharedPreferences.Editor editor = sharedPref.edit();
    editor.putInt(key, valueToSave).commit();

    使用apply()而不是commit()在后台保存,而不是立即保存。

    从共享首选项读取

    1
    2
    3
    4
    5
    String key ="myInt";
    int defaultValue = 0;

    SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
    int savedValue = sharedPref.getInt(key, defaultValue);

    如果找不到键,则使用默认值。

    笔记

    • 与其像上面那样在多个地方使用本地键字符串,不如在单个位置使用常量。您可以在设置活动的顶部使用类似的内容:

      1
      final static String PREF_MY_INT_KEY ="myInt";
    • 我在示例中使用了int,但是您也可以使用putString()putBoolean()getString()getBoolean()等。

    • 有关详细信息,请参阅文档。
    • 有多种方法可以获得共享的引用。看看这个答案,看看要注意什么。

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

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

    这和其他一些围裙很相似。希望有帮助:)


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

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


    这个答案是基于马克建议的方法。将创建EditTextPreference类的自定义版本,该版本在视图中看到的纯文本和存储在首选项存储中的密码加密版本之间来回转换。

    正如大多数在这个线程上回答的人所指出的,这不是一种非常安全的技术,尽管安全程度部分取决于所使用的加密/解密代码。但它相当简单和方便,将阻止最随便的窥探。

    以下是自定义EditTextPreference类的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    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视图。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <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/,尽管密钥和初始化向量的值不同。


    我使用android密钥库在ecb模式下使用rsa加密密码,然后将其保存在sharedreferences中。

    当我想要回密码时,我从共享的引用中读取加密的密码,然后使用密钥库对其进行解密。

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

    下面是一个关于如何做到这一点的链接:android keystore教程


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


    您需要使用sqlite、security apit来存储密码。下面是存储密码的最佳示例--passwordsafe。这是来源和解释的链接--http://code.google.com/p/android-passwordsafe/