Каков наиболее подходящий способ хранения пользовательских настроек в приложении Android
Я создаю приложение, которое подключается к серверу, используя имя пользователя / пароль, и я хотел бы включить опцию "Сохранить пароль", чтобы пользователю не приходилось вводить пароль при каждом запуске приложения.
Я пытался сделать это с помощью общих настроек, но не уверен, что это лучшее решение.
Буду признателен за любые предложения о том, как хранить пользовательские значения / настройки в приложении Android.
16 ответов
В общем, 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();
Если вас беспокоит их безопасность, вы всегда можете зашифровать пароль перед его сохранением.
Используя фрагмент, предоставленный Ричардом, вы можете зашифровать пароль перед его сохранением. 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()
возвращает ноль.
Хорошо; Прошло много времени с тех пор, как ответ несколько смешанный, но вот несколько распространенных ответов. Я исследовал это как сумасшедший, и трудно было найти хороший ответ
Метод MODE_PRIVATE считается в целом безопасным, если вы предполагаете, что пользователь не рутировал устройство. Ваши данные хранятся в виде простого текста в той части файловой системы, которая доступна только исходной программе. Это облегчает получение пароля с помощью другого приложения на рутированном устройстве. Опять же, вы хотите поддерживать рутированные устройства?
AES - все еще лучшее шифрование, которое вы можете сделать. Не забудьте поискать это, если вы начинаете новую реализацию, если это было некоторое время, так как я отправил это. Самая большая проблема с этим - "Что делать с ключом шифрования?"
Итак, теперь мы находимся на "Что делать с ключом?" часть. Это сложная часть. Получение ключа оказывается не таким уж плохим. Вы можете использовать функцию получения ключа, чтобы взять какой-нибудь пароль и сделать его довольно безопасным ключом. Вы сталкиваетесь с такими вопросами, как "сколько проходов вы делаете с PKFDF2?", Но это уже другая тема
В идеале вы должны хранить ключ AES на устройстве. Вы должны найти хороший способ получить ключ от сервера безопасно, надежно и безопасно, хотя
У вас есть какая-то последовательность входа (даже оригинальная последовательность входа, которую вы используете для удаленного доступа). Вы можете сделать два запуска вашего генератора ключей по одному и тому же паролю. Как это работает, вы дважды получаете ключ с новой солью и новым безопасным вектором инициализации. Вы сохраняете один из этих сгенерированных паролей на устройстве и используете второй пароль в качестве ключа AES.
Когда вы входите в систему, вы заново получаете ключ при локальном входе в систему и сравниваете его с сохраненным ключом. Как только это будет сделано, вы используете ключ получения #2 для AES.
- Используя "в целом безопасный" подход, вы шифруете данные с помощью AES и сохраняете ключ в MODE_PRIVATE. Это рекомендуется в недавнем блоге Android. Не невероятно безопасно, но лучше для некоторых людей по обычному тексту
Вы можете сделать много вариаций этих. Например, вместо полной последовательности входа в систему вы можете сделать быстрый PIN-код (производный). Быстрый PIN-код может быть не таким безопасным, как полная последовательность входа в систему, но во много раз безопаснее, чем обычный текст
Я знаю, что это немного некромантия, но вы должны использовать Android AccountManager. Это специально для этого сценария. Это немного громоздко, но одна из вещей, которые он делает, - делает недействительными локальные учетные данные в случае смены SIM-карты, поэтому, если кто-нибудь проведет по телефону и вставит в него новую SIM-карту, ваши учетные данные не будут скомпрометированы.
Это также дает пользователю быстрый и простой способ доступа (и, возможно, удаления) к сохраненным учетным данным для любой учетной записи, которую они имеют на устройстве, все из одного места.
SampleSyncAdapter - пример, который использует сохраненные учетные данные учетной записи.
Я брошу свою шляпу на ринг, просто чтобы поговорить о защите паролей в целом на Android. В Android двоичный файл устройства следует считать скомпрометированным - это то же самое для любого конечного приложения, которое находится под непосредственным контролем пользователя. Концептуально, хакер может использовать необходимый доступ к двоичному файлу, чтобы декомпилировать его и выкорчевать зашифрованные пароли и т. Д.
Таким образом, есть два предложения, которые я хотел бы высказать, если безопасность является серьезной проблемой для вас:
1) Не храните действительный пароль. Сохраните предоставленный токен доступа и используйте токен доступа и подпись телефона для аутентификации сеанса на стороне сервера. Преимущество этого состоит в том, что вы можете сделать токен ограниченным по продолжительности, вы не ставите под угрозу исходный пароль, и у вас есть хорошая подпись, которую вы можете использовать для корреляции с трафиком позже (например, для проверки попыток вторжения и аннулирования токен делает его бесполезным).
2) Использовать двухфакторную аутентификацию. Это может быть более раздражающим и навязчивым, но для некоторых случаев соблюдения требований неизбежно.
Это дополнительный ответ для тех, кто прибывает сюда на основе названия вопроса (как я это сделал), и ему не нужно заниматься вопросами безопасности, связанными с сохранением паролей.
Как использовать общие настройки
Пользовательские настройки обычно сохраняются локально в 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. Посмотрите этот ответ для того, что посмотреть.
Вы также можете проверить эту маленькую библиотеку, содержащую упомянутую вами функциональность.
https://github.com/kovmarci86/android-secure-preferences
Это похоже на некоторые другие подходы здесь. Надеюсь помогает:)
Как уже отмечали другие, вы можете использовать SharedPreferences в целом, но если вы хотите хранить данные в зашифрованном виде, это немного неудобно. К счастью, сейчас есть более простой и быстрый способ шифрования данных, поскольку существует реализация SharedPreferences, которая шифрует ключи и значения. Вы можете использовать EncryptedSharedPreferences в Android JetPack Security.
Просто добавьте 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
Этот ответ основан на предложенном подходе Марка. Создается пользовательская версия класса 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/, хотя и с другими значениями для ключа и вектора инициализации.
Прежде всего, я думаю, что данные пользователя не должны храниться на телефоне, и если они должны храниться где-то на телефоне, они должны быть зашифрованы в личных данных приложений. Безопасность учетных данных пользователей должна быть приоритетом приложения.
Конфиденциальные данные должны храниться надежно или не храниться вообще. В случае утери устройства или заражения вредоносным ПО данные, хранящиеся небезопасно, могут быть скомпрометированы.
Я использую Android KeyStore для шифрования пароля с помощью RSA в режиме ECB, а затем сохраняю его в SharedPreferences.
Когда я хочу вернуть пароль, я прочитал зашифрованный из SharedPreferences и расшифровал его, используя KeyStore.
С помощью этого метода вы генерируете открытую / закрытую пару ключей, где частная надежно хранится и управляется Android.
Вот ссылка о том, как это сделать: Android KeyStore Tutorial
Вот как я это делаю. Моему приложению не нужен пароль. Однако вместо сохранения паролей или зашифрованных паролей я бы сохранил односторонний хэш. Когда пользователь входит в систему, я буду хэшировать ввод таким же образом и сопоставлять его с сохраненным хэшем.
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, apit безопасности для хранения паролей. Вот лучший пример, который хранит пароли, - Passwordsafe. вот ссылка на источник и объяснение - http://code.google.com/p/android-passwordsafe/