Каков наиболее подходящий способ хранения пользовательских настроек в приложении Android - PullRequest
296 голосов
/ 24 апреля 2009

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

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

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

Ответы [ 14 ]

228 голосов
/ 24 апреля 2009

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

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

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

205 голосов
/ 18 июня 2011

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

}
28 голосов
/ 04 мая 2009

Проще всего сохранить один параметр в Android Activity - сделать что-то вроде этого:

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

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

10 голосов
/ 02 марта 2011

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

6 голосов
/ 14 мая 2013

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

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

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

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

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

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

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

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

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

5 голосов
/ 30 октября 2014

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

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

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

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

5 голосов
/ 04 апреля 2012

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

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

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

2 голосов
/ 25 октября 2017

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

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

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

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

Примечания * * 1023 Вместо того, чтобы использовать локальный ключ String в нескольких местах, как я делал выше, было бы лучше использовать константу в одном месте. Вы можете использовать что-то вроде этого в верхней части вашей настройки действий: final static String PREF_MY_INT_KEY = "myInt"; Я использовал int в моем примере, но вы также можете использовать putString(), putBoolean(), getString(), getBoolean() и т. Д. Подробнее см. В документации . Есть несколько способов получить SharedPreferences. См. этот ответ о том, на что обратить внимание.

2 голосов
/ 22 сентября 2013

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

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

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

1 голос
/ 03 декабря 2013

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...