it-roy-ru.com

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

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

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

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

291
Niko Gamulin

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

Единственная проблемная область - это то, что вы экономите. Пароли всегда сложнее хранить, и я бы особенно остерегался хранить их в виде открытого текста. Архитектура Android такова, что SharedPreferences вашего приложения находятся в «песочнице», чтобы другие приложения не могли получить доступ к значениям, поэтому существует некоторая безопасность, но физический доступ к телефону потенциально может предоставить доступ к значениям.

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

226
Reto Meier

Я согласен с 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);
        }
    }

}
204
emmby

Самый простой способ сохранить одно предпочтение в Android Activity - сделать что-то вроде этого:

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

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

28
Jeremy Logan

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

9
Mark

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

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

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

5
Jon O

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

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

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

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

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

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

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

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

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

5
Joe Plante

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

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

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

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

5
dcgregorya

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

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

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

2
Marcell

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

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

Пользовательские настройки обычно сохраняются локально в 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);

Значение по умолчанию используется, если ключ не найден.

Заметки

  • Вместо того, чтобы использовать локальный ключ String в нескольких местах, как я делал выше, было бы лучше использовать константу в одном месте. Вы могли бы использовать что-то вроде этого в верхней части вашего действия настройки:

    final static String PREF_MY_INT_KEY = "myInt";
    
  • В моем примере я использовал int, но вы также можете использовать putString(), putBoolean(), getString(), getBoolean() и т.д.

  • Смотрите документацию для более подробной информации.
  • Есть несколько способов получить SharedPreferences. Смотрите этот ответ за что нужно следить.
2
Suragch

Этот ответ основан на предложенном подходе Марка. Создается пользовательская версия класса 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.

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

1
RenniePet

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

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

1
Gauraw Yadav

Я использую Android KeyStore для шифрования пароля с помощью RSA в режиме ECB, а затем сохраняю его в SharedPreferences.

Когда я хочу вернуть пароль, я прочитал зашифрованный из SharedPreferences и расшифровал его, используя KeyStore.

С помощью этого метода вы генерируете открытую/закрытую пару ключей, где частная надежно хранится и управляется Android.

Вот ссылка о том, как это сделать: Учебник по Android KeyStore

0
Braveblacklion