ユーザー名/パスワードを使用してサーバーに接続するアプリケーションを作成しています。アプリケーションを起動するたびにユーザーがパスワードを入力する必要がないように、「パスワードを保存する」オプションを有効にしたいと考えています。
私は Shared Preferences でそれをやろうとしていましたが、これが最善の解決策であるかどうかはわかりません.
ユーザーの値/設定を Android アプリケーションに保存する方法についての提案をいただければ幸いです。
ユーザー名/パスワードを使用してサーバーに接続するアプリケーションを作成しています。アプリケーションを起動するたびにユーザーがパスワードを入力する必要がないように、「パスワードを保存する」オプションを有効にしたいと考えています。
私は Shared Preferences でそれをやろうとしていましたが、これが最善の解決策であるかどうかはわかりません.
ユーザーの値/設定を Android アプリケーションに保存する方法についての提案をいただければ幸いです。
一般に、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 アクティビティに 1 つの設定を保存する最も簡単な方法は、次のようにすることです。
Editor e = this.getPreferences(Context.MODE_PRIVATE).edit();
e.putString("password", mPassword);
e.commit();
これらのセキュリティが心配な場合は、パスワードを保存する前にいつでも暗号化できます。
Richard が提供するスニペットを使用すると、保存する前にパスワードを暗号化できます。ただし、設定 API は、値を傍受して暗号化する簡単な方法を提供しません。OnPreferenceChange リスナーを介して保存されるのをブロックできます。また、理論的には、preferenceChangeListener を介して値を変更することもできますが、その結果、無限ループが発生します。
これを実現するために、「非表示」設定を追加することを以前に提案しました。それは間違いなく最善の方法ではありません。より実行可能と思われる他の 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 を返すことです。
わかった; 答えが混ざり合ってからしばらく経ちましたが、ここにいくつかの一般的な答えがあります. 私はこれを狂ったように研究しましたが、良い答えを作るのは困難でした
ユーザーがデバイスをルート化していないと仮定した場合、MODE_PRIVATE メソッドは一般的に安全であると見なされます。データは、元のプログラムのみがアクセスできるファイル システムの一部にプレーン テキストで保存されます。これにより、ルート化されたデバイス上の別のアプリでパスワードを簡単に取得できます. 繰り返しになりますが、ルート化されたデバイスをサポートしますか?
AES は、今でも実行できる最高の暗号化です。これを投稿してからしばらく経っている場合、新しい実装を開始する場合は、忘れずにこれを調べてください。これの最大の問題は「暗号鍵をどうするか」です。
では、「キーをどうするか」です。部分。これは難しい部分です。キーを取得することはそれほど悪くないことがわかりました。キー派生関数を使用して、パスワードを取得し、それを非常に安全なキーにすることができます。「PKFDF2で何回パスするの?」みたいな話になるけど、それはまた別の話
理想的には、AES キーをデバイスの外に保存します。ただし、サーバーからキーを安全、確実、かつ安全に取得するための適切な方法を見つけ出す必要があります。
ある種のログイン シーケンスがあります (リモート アクセス用の元のログイン シーケンスでさえ)。同じパスワードでキー ジェネレーターを 2 回実行できます。これがどのように機能するかは、新しいソルトと新しい安全な初期化ベクトルを使用してキーを 2 回導出することです。生成されたパスワードの 1 つをデバイスに保存し、2 番目のパスワードを AES キーとして使用します。
ログイン時に、ローカル ログインでキーを再取得し、保存されているキーと比較します。それが完了したら、AES の派生キー #2 を使用します。
これらの多くのバリエーションを行うことができます。たとえば、完全なログイン シーケンスの代わりに、クイック PIN (派生) を実行できます。クイック PIN は、完全なログイン シーケンスほど安全ではないかもしれませんが、プレーン テキストよりも何倍も安全です。
これが少しネクロマンシーであることはわかっていますが、Android AccountManagerを使用する必要があります。これは、このシナリオ専用に作成されています。少し面倒ですが、SIM カードが変更された場合にローカルの資格情報が無効になるため、誰かが電話をスワイプして新しい SIM を挿入しても、資格情報が侵害されることはありません。
これにより、ユーザーはデバイスに保存されているすべてのアカウントの資格情報に、すべて 1 か所からすばやく簡単にアクセス (場合によっては削除) することもできます。
SampleSyncAdapterは、保存されたアカウント資格情報を利用する例です。
Android で一般的にパスワードを保護することについて話すために、リングに脱帽します。Android では、デバイス バイナリが侵害されていると見なす必要があります。これは、ユーザーが直接制御するエンド アプリケーションでも同じです。概念的には、ハッカーはバイナリへの必要なアクセスを使用してバイナリを逆コンパイルし、暗号化されたパスワードなどを根絶する可能性があります。
そのため、セキュリティがあなたにとって大きな懸念事項である場合に、私が提案したい 2 つの提案があります。
1) 実際のパスワードを保存しないでください。付与されたアクセス トークンを保存し、アクセス トークンと電話の署名を使用してセッション サーバー側を認証します。これの利点は、トークンの有効期間を制限できること、元のパスワードを危険にさらすことがないこと、および後でトラフィックに関連付けるために使用できる適切な署名があることです (たとえば、侵入の試みをチェックし、パスワードを無効にするため)。トークンは役に立たない)。
2) 2要素認証を利用する。これは煩わしくて押し付けがましいかもしれませんが、コンプライアンスの状況によっては避けられない場合があります。
これは、質問のタイトルに基づいてここに到着した場合の補足的な回答であり (私が行ったように)、パスワードの保存に関連するセキュリティの問題に対処する必要はありません。
ユーザー設定は通常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);
キーが見つからない場合は、デフォルト値が使用されます。
上記のように複数の場所でローカル キー String を使用するよりも、単一の場所で定数を使用する方がよいでしょう。設定アクティビティの上部で次のようなものを使用できます。
final static String PREF_MY_INT_KEY = "myInt";
この例では を使用しましたが、、、、 などint
も使用できます。putString()
putBoolean()
getString()
getBoolean()
詳細については、ドキュメントを参照してください。
SharedPreferences を取得する方法は複数あります。注意すべき点については、この回答を参照してください。
あなたが言及した機能を含むこの小さなライブラリをチェックアウトすることもできます。
https://github.com/kovmarci86/android-secure-preferences
ここでの他のアプローチのいくつかに似ています。希望が役立ちます:)
この回答は、Mark によって提案されたアプローチに基づいています。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」ファイルです。3 つの通常の EditTextPreference ビューと、カスタム EditPasswordPreference ビューの 1 つが含まれていることに注意してください。
<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 セキュリティ apit を使用する必要があります。ここにパスワードを保存する最良の例があります -- passwordsafe。ソースと説明のリンクは次のとおりです- http://code.google.com/p/android-passwordsafe/