web-dev-qa-db-ja.com

Androidアプリケーションにユーザー設定を保存する最も適切な方法は何ですか

ユーザー名/パスワードを使用してサーバーに接続するアプリケーションを作成しています。アプリケーションを起動するたびにユーザーがパスワードを入力する必要がないように、[パスワードを保存]オプションを有効にします。

私は共有設定でそれをしようとしていましたが、これが最善の解決策であるかどうかはわかりません。

Androidアプリケーションにユーザーの値/設定を保存する方法に関する提案をいただければ幸いです。

296
Niko Gamulin

一般に、SharedPreferencesは設定を保存するための最善の方法です。したがって、一般に、アプリケーションとユーザー設定を保存するためのそのアプローチをお勧めします。

ここでの唯一の懸念事項は、保存しているものです。パスワードは常に保存するのが難しいものであり、クリアテキストとして保存する場合は特に注意が必要です。 Androidアーキテクチャは、アプリケーションのSharedPreferencesがサンドボックス化され、他のアプリケーションが値にアクセスできないようにするため、セキュリティが確保されますが、電話への物理アクセスにより値へのアクセスが許可される可能性があります。

可能であれば、アクセスを提供するためにネゴシエートされたトークンを使用するようにサーバーを変更することを検討します OAuth など。あるいは、ある種の暗号化ストアを作成する必要があるかもしれませんが、それは簡単なことではありません。少なくとも、ディスクに書き込む前にパスワードを暗号化していることを確認してください。

228
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 一部の例.

セキュリティ全般に注意を払う必要がありますが、この特定の1つの問題にこのような注意を払っても、実際にセキュリティ全体が大幅に向上するわけではないと主張します。ただし、認識はそのままなので、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);
        }
    }

}
205
emmby

単一の設定をAndroidアクティビティに保存する最も簡単な方法は、次のようなことです。

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

これらのセキュリティが心配な場合は、パスワードを保存する前にいつでも暗号化できます。

28
Jeremy Logan

リチャードが提供するスニペットを使用して、保存する前にパスワードを暗号化できます。ただし、設定APIは、値をインターセプトして暗号化する簡単な方法を提供しません。OnPreferenceChangeリスナーを介して保存されるのをブロックできます。

これを実現するために、以前は「非表示」の設定を追加することを提案していました。それは間違いなく最善の方法ではありません。より実行可能と思われる他の2つのオプションを紹介します。

まず、最も単純なものは、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; 
   }

2番目の方法、そして今私が好む方法は、独自のカスタム設定を作成し、EditTextPreferenceを拡張し、setText()およびgetText()メソッドを@Override 'して、setText()がパスワードを暗号化し、getText()がnullを返すようにすることです。

10
Mark

はい;回答が少し混ざり合ってからしばらく経ちましたが、ここにいくつかの一般的な回答があります。私はこれをクレイジーに研究しましたが、良い答えを作るのは困難でした

  1. MODE_PRIVATEメソッドは、ユーザーがデバイスをルート化していないと仮定した場合、一般に安全であると見なされます。データは、元のプログラムのみがアクセスできるファイルシステムの一部にプレーンテキストで保存されます。これにより、ルート化されたデバイス上の別のアプリでパスワードを簡単に取得できます。次に、ルート化されたデバイスをサポートしますか?

  2. AESは今でもできる最高の暗号化です。私がこれを投稿してからしばらく経ってから新しい実装を開始する場合は、必ずこれを調べてください。これに関する最大の問題は、「暗号化キーをどうするか」です。

だから、今、私たちは「キーで何をすべきか?」部分。これは難しい部分です。キーを取得することはそれほど悪くないことが判明しました。キー派生関数を使用してパスワードを取得し、非常に安全なキーにすることができます。 「PKFDF2で何回パスしますか?」などの問題が発生しますが、それは別のトピックです

  1. 理想的には、デバイスからAESキーを保管します。ただし、サーバーからキーを安全、確実、安全に取得するための良い方法を見つけ出す必要があります。

  2. ある種のログインシーケンスがあります(リモートアクセスのために行う元のログインシーケンスも)。同じパスワードでキージェネレーターを2回実行できます。これがどのように機能するかは、新しいソルトと新しいセキュアな初期化ベクトルを使用してキーを2回導出することです。生成されたパスワードの1つをデバイスに保存し、2番目のパスワードをAESキーとして使用します。

ログインすると、ローカルログインでキーを再取得し、保存されているキーと比較します。それが完了したら、AESの派生キー#2を使用します。

  1. 「一般的に安全な」アプローチを使用して、AESを使用してデータを暗号化し、キーをMODE_PRIVATEに保存します。これは最近のAndroidブログ投稿で推奨されています。信じられないほど安全ではありませんが、平文よりも優れている人もいます

これらの多くのバリエーションを行うことができます。たとえば、完全なログインシーケンスの代わりに、クイックPIN(派生)を実行できます。クイックPINは完全なログインシーケンスほど安全ではないかもしれませんが、プレーンテキストよりも何倍も安全です

6
Joe Plante

これはちょっとしたネクロマンシーであることは知っていますが、Android AccountManager を使用する必要があります。これは、このシナリオ専用です。少し面倒ですが、SIMカードが変更された場合にローカルの資格情報を無効にすることの1つであるため、誰かがスマートフォンをスワイプして新しいSIMを挿入しても、資格情報が侵害されることはありません。

これにより、ユーザーは、デバイス上にあるアカウントの保存された資格情報に、すべて1か所からすばやく簡単にアクセス(および削除)することができます。

SampleSyncAdapter は、保存されたアカウント資格情報を使用する例です。

5
Jon O

Androidでの一般的なパスワードの保護について話すために、帽子を指輪に投げ込みます。 Androidでは、デバイスバイナリが侵害されていると見なされる必要があります。これは、ユーザーが直接制御するエンドアプリケーションでも同じです。概念的には、ハッカーはバイナリへの必要なアクセスを使用してそれを逆コンパイルし、暗号化されたパスワードなどを根絶することができます。

そのため、セキュリティがあなたにとって大きな懸念事項である場合は、2つの提案があります。

1)実際のパスワードを保存しないでください。許可されたアクセストークンを保存し、アクセストークンと電話の署名を使用して、セッションサーバー側を認証します。これの利点は、トークンの有効期間を制限できること、元のパスワードを危険にさらすことなく、後でトラフィックに関連付けるために使用できる適切な署名があることです(たとえば、侵入試行をチェックして、トークンは役に立たない)。

2)2要素認証を利用します。これは、より面倒で煩わしいかもしれませんが、コンプライアンスの状況によっては避けられません。

5
dcgregorya

あなたが言及した機能を含むこの小さなライブラリをチェックアウトすることもできます。

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

ここにある他のアプローチのいくつかに似ています。希望が役立ちます:)

2
Marcell

これは質問のタイトルに基づいてここに到着した人に対する補足的な回答であり(私がしたように)、パスワードの保存に関連するセキュリティの問題に対処する必要はありません。

共有設定の使用方法

ユーザー設定は、通常、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()putBoolean()getString()getBoolean()なども使用できます。

  • 詳細については documentation をご覧ください。
  • SharedPreferencesを取得する方法は複数あります。何に注意するかについては this answer をご覧ください。
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);
    }
}

これは、それがどのように使用できるかを示します-これは、設定表示を駆動する「アイテム」ファイルです。 3つの通常のEditTextPreferenceビューと1つのカスタム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を使用して、ECBモードでRSAを使用してパスワードを暗号化し、SharedPreferencesに保存します。

パスワードを元に戻したいときは、SharedPreferencesから暗号化されたパスワードを読み取り、KeyStoreを使用して暗号化を解除します。

この方法を使用すると、秘密キーがAndroidによって安全に保存および管理される公開キーと秘密キーのペアを生成できます。

これを行う方法に関するリンクを次に示します。 Android KeyStore Tutorial

0
Braveblacklion