簡體   English   中英

在Android應用程序中存儲用戶設置的最合適方法是什么

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

我正在創建一個使用用戶名/密碼連接到服務器的應用程序,我想啟用“保存密碼”選項,這樣用戶就不必在每次應用程序啟動時輸入密碼。

我試圖用 Shared Preferences 來做到這一點,但不確定這是否是最好的解決方案。

對於如何在 Android 應用程序中存儲用戶值/設置的任何建議,我將不勝感激。

一般來說,SharedPreferences 是您存儲首選項的最佳選擇,所以總的來說,我建議使用這種方法來保存應用程序和用戶設置。

這里唯一需要關注的是您要保存的內容。 存儲密碼總是一件棘手的事情,我會特別小心將它們存儲為明文。 Android 架構是這樣的,您的應用程序的 SharedPreferences 被沙箱化以防止其他應用程序能夠訪問這些值,因此那里有一些安全性,但是對手機的物理訪問可能會允許訪問這些值。

如果可能的話,我會考慮修改服務器以使用協商的令牌來提供訪問權限,例如OAuth 或者,您可能需要構建某種加密存儲,盡管這並非易事。 至少,確保在將密碼寫入磁盤之前對其進行了加密。

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

}

在 Android Activity 中存儲單個首選項的最簡單方法是執行以下操作:

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

如果您擔心這些的安全性,那么您始終可以在存儲密碼之前對其進行加密。

使用 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,@Override'ing setText()getText()方法,以便setText()加密密碼,而getText()返回無效的。

好的; 自從答案有點混雜以來已經有一段時間了,但這里有一些常見的答案。 我瘋狂地研究了這個,很難找到一個好的答案

  1. MODE_PRIVATE 方法通常被認為是安全的,如果您假設用戶沒有 root 設備。 您的數據以純文本形式存儲在文件系統的一部分中,只能由原始程序訪問。 這使得在有根設備上使用另一個應用程序輕松獲取密碼。 再說一次,你想支持有根設備嗎?

  2. AES 仍然是你能做的最好的加密。 如果你開始一個新的實現,如果我發布這個已經有一段時間了,記得查看這個。 最大的問題是“如何處理加密密鑰?”

所以,現在我們在“如何處理密鑰?” 部分。 這是困難的部分。 得到鑰匙原來不是那么糟糕。 您可以使用密鑰派生函數獲取一些密碼並使其成為非常安全的密鑰。 您確實會遇到諸如“您使用 PKFDF2 進行了多少次通過?”之類的問題,但這是另一個話題

  1. 理想情況下,您將 AES 密鑰存儲在設備之外。 您必須找到一種安全、可靠、安全地從服務器檢索密鑰的好方法

  2. 您有某種登錄序列(甚至是您為遠程訪問所做的原始登錄序列)。 您可以使用相同的密碼運行兩次密鑰生成器。 它的工作原理是您使用新的鹽和新的安全初始化向量兩次派生密鑰。 您將其中一個生成的密碼存儲在設備上,並將第二個密碼用作 AES 密鑰。

登錄時,您重新派生本地登錄的密鑰並將其與存儲的密鑰進行比較。 完成后,您可以使用 AES 的派生密鑰 #2。

  1. 使用“一般安全”的方法,您使用 AES 加密數據並將密鑰存儲在 MODE_PRIVATE 中。 這是最近的一篇 Android 博客文章推薦的。 不是非常安全,但對某些人來說比純文本更好

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

我將把我的帽子扔進戒指,只是為了談論在 Android 上保護密碼的一般性。 在 Android 上,設備二進制文件應該被認為是受損的——這對於任何直接由用戶控制的最終應用程序都是一樣的。 從概念上講,黑客可以使用對二進制文件的必要訪問來反編譯它並根除您的加密密碼等。

因此,如果安全是您的主要關注點,我想提出兩個建議:

1)不要存儲實際密碼。 存儲授予的訪問令牌並使用訪問令牌和手機的簽名來驗證會話服務器端。 這樣做的好處是您可以使令牌具有有限的持續時間,您不會破壞原始密碼並且您有一個很好的簽名,您可以使用它來關聯以后的流量(例如檢查入侵嘗試並使令牌使其無用)。

2)利用2因素身份驗證。 這可能更令人討厭和干擾,但對於某些合規情況是不可避免的。

我知道這有點死靈,但你應該使用 Android AccountManager 它是專門為這種情況而設計的。 這有點麻煩,但它所做的一件事就是在 SIM 卡發生變化時使本地憑據失效,因此,如果有人刷你的手機並將新的 SIM 卡放入其中,你的憑據不會受到損害。

這也為用戶提供了一種快速簡便的方法來訪問(並可能刪除)他們在設備上擁有的任何帳戶的存儲憑據,所有這些都可以從一個地方進行。

SampleSyncAdapter是一個使用存儲的帳戶憑據的示例。

這是根據問題標題(就像我一樣)到達這里的人的補充答案,不需要處理與保存密碼相關的安全問題。

如何使用共享首選項

用戶設置通常使用帶有鍵值對的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( putString()putBoolean()getString()getBoolean()等。

  • 有關更多詳細信息,請參閱文檔

  • 有多種方法可以獲取 SharedPreferences。 請參閱此答案以了解要注意的事項。

您還可以查看這個包含您提到的功能的小庫。

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

它類似於這里的其他一些方法。 希望有幫助:)

此答案基於 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/的一些代碼,盡管具有不同的值對於密鑰和初始化向量。

首先,我認為用戶的數據不應該存儲在手機上,如果必須將數據存儲在手機上的某個位置,則應該在應用程序的私有數據中對其進行加密。 用戶憑證的安全性應該是應用程序的優先級。

敏感數據應安全存儲或根本不存儲。 如果設備丟失或惡意軟件感染,不安全存儲的數據可能會受到損害。

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

當我想要找回密碼時,我從 SharedPreferences 中讀取加密的密碼並使用 KeyStore 對其進行解密。

使用這種方法,您可以生成一個公鑰/私鑰對,其中的私鑰由 Android 安全存儲和管理。

這是有關如何執行此操作的鏈接: Android KeyStore Tutorial

正如其他人已經指出的那樣,您通常可以使用 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

我就是這樣做的。

這不會在嚴格模式下產生錯誤。

public class UserPreferenceUtil
{

    private static final String  THEME = "THEME";
    private static final String  LANGUAGE = "LANGUAGE";

    public static String getLanguagePreference(Context context)
    {

        String lang = getPreferenceByKey(context,LANGUAGE);

        if( lang==null || "System".equalsIgnoreCase(lang))
        {
            return null;
        }


        return lang;
    }

    public static void saveLanguagePreference(Context context,String value)
    {
        savePreferenceKeyValue(context, LANGUAGE,value);
    }

    public static String getThemePreference(Context context)
    {

        return getPreferenceByKey(context,THEME);
    }

    public static void saveThemePreference(Context context, String value)
    {
        savePreferenceKeyValue(context,THEME,value);

    }

    public static String getPreferenceByKey(Context context, String preferenceKey )
    {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);

        String value = sharedPreferences.getString(preferenceKey, null);

        return value;
    }

    private static void savePreferenceKeyValue(Context context, String preferenceKey, String value)
    {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(preferenceKey,value);
        editor.apply();

    }


}

我的應用不需要密碼。 但是,我不會保存密碼或加密密碼,而是保存單向哈希。 當用戶登錄時,我將以相同的方式對輸入進行哈希處理,並將其與存儲的哈希值匹配。

共享首選項是存儲我們的應用程序數據的最簡單方法。 但是任何人都可以通過應用程序管理器清除我們共享的偏好數據。所以我認為這對我們的應用程序來說並不完全安全。

您需要使用 sqlite, security apit 來存儲密碼。 這是最好的例子,它存儲密碼,--passwordsafe。 這是來源和解釋的鏈接——http: //code.google.com/p/android-passwordsafe/

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM