この記事は DeNA Advent Calendar 2020 の15日目の Android 開発者向けの小ネタ記事です。
PreferenceFragmentCompat を EncryptedSharedPreferences を利用して暗号化する方法を紹介します。下にサンプルコードのリンクがあります。
PreferenceFragmentCompat & SharedPreferences
PreferenceFragmentCompat を利用した設定等の変更は SharedPreferences を利用した実装で保存され、そのデータは /data/data/<package-name>/shared_prefs/<package-name>_preferences.xml に下ような感じでプレーンテキストで保存されています。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="edit_text_preference_1">Hello world!</string>
<boolean name="check_box_preference_1" value="true" />
</map>
通常ではこのファイルを覗くことはできないとはいえ、保存する内容によっては好ましくない場合があります。Jetpack Security の一環として EncryptedSharedPreferences の登場で暗号化した SharedPreferences を利用できるようになったので、PreferenceFramgnetCompat が持つ SharedPreferences を EncryptedSharedPreferences に入れ替えてみます。
※EncryptedSharedPreferences の利用には API 23 (6.0) 以上が必要です。
データ保存の実装
PreferenceFragmentCompat でのデータ保存は、PreferenceFragmentCompat → PreferenceManager → PreferenceDataStore に実装されています。この PreferenceDataStore の実装をカスタムの実装と入れ替えることによって独自のデータ保存を実現することができます。
preferenceManager.preferenceDataStore = MyPrefDataStore()
カスタム PreferenceDataStore の実装
PreferenceDataStore は抽象クラスで各データ型に対する put、get の空メソッドが定義されているだけで、データを保存する手段自体は何も提供しません。ここで EncryptedSharedPreferences にデータ保存するように実装することで、PreferenceFragmentCompat のSharedPreferences を利用した実装との換装を行い暗号化を行います。
実装はシンプルで、EncryptedSharedPreferences のインスタンスを生成し、putString や putBoolean 等の put/get メソッドをオーバーライドして、生成したインスタンスに対して保存を行うようにします。
class EncryptedPreferencesDataStore(context: Context) : PreferenceDataStore() {
private val prefs: SharedPreferences by lazy {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
override fun putString(key: String, value: String?) {
prefs.edit().putString(key, value).apply()
}
override fun getString(key: String, defValue: String?): String? {
return prefs.getString(key, defValue)
}
...
}
※暗号の強度は仕様に合わせて変更してください。
PreferenceDataStore の換装
EncryptedSharedPreferences で実装した PreferenceDataStore を onCreatePreferences のタイミングで入れ替えます。
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = EncryptedPreferencesDataStore(requireContext())
setPreferencesFromResource(R.xml.prefs, rootKey)
}
この実装例で冒頭の例と同じデータを保存するとこんな感じになります。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a901ecfc8590896f0b3a69727a0e19f67acda5a2bb4267156ac94f35d4685183be5fbfc4228b3c62e04936d6881563b0df5097782d7dc503e8661e216e4e68a68451e655939f94f6ba26591add85fc9fb74da88eed8d8df60ea3229d1891f2f886445f1552f5888755301f7d493d4f847a9d3061715b89667ce9ca5b3c8accfe5f6e83452827e150aaaf12981bbb8a963b412bf21910683c2ec30631a16379748e59b9a976a3ce98d030021a440883e7deec07123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b657910011883e7deec072001</string>
<string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">12880103f090de87cb4f94fb82642cfb13b0f6ccc70fbe838ed2b78599b46095c6f493448aba4710860dc306580aa54af683aaa6d5075cbdb252f4f2a4baf95d560aac7116fb688811e22ad4d9b6ef57ac768513a5f9d837f915c1c9d6bf2c7af050b0c9ad218e1eed2e67352102d07031670386da2838c2555ca58a402045a20b130557e721bf5a0062621a440887f68c8f06123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b657910011887f68c8f062001</string>
<string name="AX2Xs4MjUnzmBfZpNwtRywtkZyQ5w13ia24bJ4bJ4G/quWS5qbpjxg+6eQ==">AWHjOweIiD0hzFxNpelQuTRcyuCalpHSx6/7vBGB2O1xt3x51JY=</string>
<string name="AX2Xs4PB8fS5GHuUeucYQ5OVVZxJ6rc5HVTC74SU4zufESyyM8RPnrgDZg==">AWHjOwfZO6IY3zm24LinKL60d0lA2S9PJaDkJZHL1ljq9BlOGC8slkKukNiXZrLX9OpAKKcz</string>
</map>
なんだか分からないものに変わっています。簡単に暗号化対応できました。
バックアップ
Account credentials or other sensitive information. Consider asking the user to reauthenticate the first time they launch a restored app rather than allowing for storage of such information in the backup.
Jetpack Security は端末に依存した鍵を利用しているのでシステムの自動バックアップでのデータ引き継ぎができません。センシティブな情報は引き継がずに端末ごとにユーザに再入力を促す方針です。
すべてのプリファレンス周りの情報が引き継がれないのも問題なので、センシティブなものとそうでないものに分ける、そもそもクライアント側に保存する必要があるかの検討の上での設定設計が良いでしょう。勿論、Jetpack Security に頼らず独自に実装すれば如何様にでも可能です。
分ける場合の保存するものしないものの仕分けはマニフェストの <android:fullBackupContent> にバックアップのルールを記述します。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.encryptedpreferencefragmentcompat">
<application
android:allowBackup="true"
android:fullBackupContent="@xml/my_backup_rules"
...
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include
domain="sharedpref"
path="." />
<exclude
domain="sharedpref"
path="secret_shared_prefs.xml" />
</full-backup-content>
終わりに
PreferenceDataStore の実装には EncryptedSharedPreferences でなくとも何でも構わないので、暗号化レイヤーを追加したデータベースや、暗号化サポートが追加されたら Jetpack DataStore を利用することも(たぶん)可能です。ユーザの情報がそのまま保存されているのが気になっている人、EncryptedSharedPreferences だけで全部暗号化したつもりだった人()は実装してみてはいかがでしょうか。
GitHub にサンプルコードを上げています。
また DeNA 公式 Twitter アカウント @DeNAxTech では色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい!