Что является наиболее подходящим способом для сохранения пользовательских настроек в Android-приложение


Я создаю приложение, которое подключается к серверу с помощью имени пользователя/пароля, и я хотел бы включить опцию "Сохранить пароль", чтобы пользователю не приходилось вводить пароль при каждом запуске приложения.

Я пытался сделать это с общими предпочтениями, но не уверен, что это лучшее решение.

Я был бы признателен за любые предложения о том, как хранить пользовательские значения/настройки в Android-приложении.

13 285

13 ответов:

в целом 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 в этом, и любые данные, которые Вы читаете/записываете, будут автоматически зашифрованы и расшифрованы. например.

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 деятельности, чтобы сделать что-то вроде этого:

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

Если вы беспокоитесь о безопасности этих, то вы всегда можете зашифровать пароль перед его сохранением.

используя фрагмент кода, предоставленный Ричардом, вы можете зашифровать пароль перед его сохранением. Однако 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, @ Override'ING setText() и getText() методы, так что setText() шифрует пароль, и getText() возвращать null.

Я знаю, что это немного некромантии, но вы должны использовать Android AccountManager. Он специально создан для этого сценария. Это немного громоздко, но одна из вещей, которые он делает, - это аннулирование локальных учетных данных, если SIM-карта изменится, поэтому, если кто-то проведет ваш телефон и бросит в него новую SIM-карту, ваши учетные данные не будут скомпрометированы.

Это также дает пользователю быстрый и простой способ доступа (и потенциально удалить) сохраненные учетные данные для любой учетной записи они имеют на устройстве, все из одного места.

SampleSyncAdapter пример, который использует сохраненные учетные данные.

хорошо; прошло некоторое время с тех пор, как ответ был смешанным, но вот несколько общих ответов. Я исследовал это как сумасшедший, и было трудно построить хороший ответ

  1. метод MODE_PRIVATE считается в целом безопасным, если вы предполагаете, что пользователь не запустил устройство. Ваши данные хранятся в виде обычного текста в той части файловой системы, доступ к которой может получить только исходная программа. Это делает захват пароля с другим приложением на корневом устройстве простой. Опять же, вы хотите поддерживать корневые устройства?

  2. AES по-прежнему является лучшим шифрованием, которое вы можете сделать. Не забудьте посмотреть это, если вы начинаете новую реализацию, если прошло некоторое время с тех пор, как я опубликовал это. Самая большая проблема с этим - "что делать с ключом шифрования?"

Итак, теперь мы на "что делать с ключом?" доля. Это самая трудная часть. Получение ключа оказывается не так уж и плохо. Вы можете использовать ключ функция деривации, чтобы взять какой-то пароль и сделать его довольно безопасным ключом. Вы попадаете в такие вопросы, как "сколько проходов вы делаете с PKFDF2?", но это уже другая тема

  1. В идеале, вы храните ключ AES от устройства. Вы должны найти хороший способ получить ключ с сервера безопасно, надежно и надежно, хотя

  2. У вас есть какая-то последовательность входа (даже исходная последовательность входа, которую вы делаете для удаленного доступа). Вы можете выполните два запуска вашего генератора ключей на одном пароле. Как это работает, вы получаете ключ дважды с новой солью и новым вектором безопасной инициализации. Вы храните один из этих сгенерированных паролей на устройстве, и вы используете второй пароль в качестве ключа AES.

когда вы входите в систему, вы повторно производите ключ на локальном входе в систему и сравниваете его с сохраненным ключом. Как только это будет сделано, вы используете производный ключ #2 для AES.

  1. использование ", как правило, безопасны" подход, вы шифруете данные с помощью AES и храните ключ в MODE_PRIVATE. Это рекомендуется недавним сообщением в блоге Android. Не невероятно безопасный, но способ лучше для некоторых людей по простому тексту

вы можете сделать много вариантов из них. Например, вместо полной последовательности входа, вы можете сделать быстрый контакт (производные). Быстрый PIN-код может быть не так безопасен, как полная последовательность входа в систему, но он во много раз более безопасен, чем обычный текст

Я брошу свою шляпу в кольцо, чтобы просто поговорить о защите паролей в целом на Android. На Android устройство binary следует считать скомпрометированным-это то же самое для любого конечного приложения, которое находится под прямым контролем пользователя. Концептуально, хакер может использовать необходимый доступ к двоичному файлу, чтобы декомпилировать его и выкорчевать ваши зашифрованные пароли и т. д.

таким образом, есть два предложения, которые я хотел бы бросить там, если безопасность является серьезной проблемой для ты:

1) Не храните пароль. Сохраните предоставленный маркер доступа и используйте маркер доступа и подпись телефона для аутентификации на стороне сервера сеанса. Преимущество этого заключается в том, что вы можете сделать токен ограниченным по времени, вы не ставите под угрозу исходный пароль, и у вас есть хорошая подпись, которую вы можете использовать для корреляции с трафиком позже (например, для проверки попыток вторжения и недействительности токена, делающего его бесполезным).

2) Используйте 2-х факторную аутентификацию. Это может быть более раздражающим и навязчивым, но для некоторых ситуаций соответствия неизбежно.

вы также можете проверить этот маленький lib, содержащий функциональность, которую вы упомянули.

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

Это похоже на некоторые из других приближений здесь. Надежда помогает:)

это дополнительный ответ для тех, кто прибывает сюда на основе названия вопроса (как и я) и не нужно заниматься вопросами безопасности, связанными с сохранением паролей.

как использовать общие настройки

настройки пользователя сохраняются локально в Android с помощью SharedPreferences С парой ключ-значение. Вы используете 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. Смотрите ответ на что обратить внимание.

этот ответ основан на предложенном Марком подходе. Создается пользовательская версия класса 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/, хотя и с разными значениями для ключа и вектора инициализации.

прежде всего, я думаю, что данные пользователя не должны храниться на телефоне, и если он должен хранить данные где-то на телефоне, он должен быть зашифрован в личных данных приложений. Безопасность учетных данных пользователей должна быть приоритетом приложения.

конфиденциальные данные должны храниться надежно или не на всех. В случае утери устройства или заражения вредоносным ПО данные, хранящиеся небезопасно, могут быть скомпрометированы.

вам нужно использовать sqlite, security apit для хранения паролей. вот лучший пример, который хранит пароли, -- пишет. вот ссылка на источник и пояснение -- http://code.google.com/p/android-passwordsafe/

общие настройки-это самый простой способ хранения данных нашего приложения. но вполне возможно, что кто-то может очистить наши общие данные предпочтений через приложение manager.so я не думаю, что это полностью безопасно для нашего приложения.