既存アプリにおけるEncryptedSharedPreferencesへの移行方法

本記事は、[Android Advent Calendar 2021](https://qiita.com/advent-calendar/2021/android) 16日目の記事です。

# はじめに：EncryptedSharedPreferencesとは
[Android Developers - データをより安全に取り扱う](https://developer.android.com/topic/security/data?hl=ja)  

> SharedPreferences クラスをラップし、2 つの方式を使用してキーと値を自動的に暗号化します。
> -  キーは、暗号化された状態でも検索できるように、決定性暗号化アルゴリズムを使用して暗号化されます。
> - 値は、AES-256 GCM を使用して暗号化されます。これは非決定性です。

2021年4月にStable版ver1.0.0がリリースされた、Jetpackのライブラリです。
このライブラリを用いると、煩雑なコードなしで暗号化したSharedPreferencesのデータを読み書きできます。

リリースしているアプリでこのライブラリを使うには、既に暗号化して保存しているデータを復号&再度暗号化して保存する処理が必要になります。
この記事では、その具体的な処理について解説します。

# 1. 導入

[Android Developersのページ](https://developer.android.com/jetpack/androidx/releases/security#version_100_2)を参考に、ライブラリの依存関係を追加します。

```gradle:app/build.gradle
dependencies {
    implementation "androidx.security:security-crypto:1.0.0"
}
```

# 2. 初期化処理
SharedPreferencesの操作を行うクラスで、暗号化用のeditorを取得します。
アプリのローンチ時等であれば、encryptedPreferencesNameはそのまま `context.packageName` を用いて問題ありませんが、既に同じ名前のファイルにデータが保存されている場合は、適当なsuffixを付ける必要があります。

```kotlin
    private val encryptedPreferencesName = context.packageName + "_encrypted_preferences"
    private val prefs: SharedPreferences = EncryptedSharedPreferences.create(
        encryptedPreferencesName,
        MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
        context,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )
    private val editor = prefs.edit()
```

また、ライブラリ導入前の既存のSharedPreferencesアクセス用のeditorも取得しておきます。

```kotlin
    private val unEncryptedPrefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
    private val unEncryptedEditor = unEncryptedPrefs.edit()
```

# 3. データ移管処理
データの移管処理はアプリの起動時に行う必要があるため、ApplicationクラスのonCreate()のタイミングで以下の処理を実行します。
ポイントごとに解説します。

```kotlin
    fun initialize(keys: List<Pair<String, Boolean>>) { // ポイント①：移管対象の項目を指定する
        val unEncryptedKeys: List<String> = unEncryptedPrefs.all.keys.map { it.toString() }

        keys.forEach {
            val key = it.first
            val isEncrypted = it.second

            unEncryptedKeys.find { it == key }?.let {
                unEncryptedPrefs.all.entries.find { it.key == key }?.let { entry ->
                    if (isEncrypted) {
                        putString(key, getStringWithDecryption(key))
                    } else {
                        when (val value = entry.value) {
                            is String -> putString(key, value)
                            is Int -> putInt(key, value)
                            is Long -> putLong(key, value)
                            is Boolean -> putBoolean(key, value)
                        }
                    }
                    unEncryptedEditor.remove(key).apply() // ポイント②：移管した項目を削除する
                }
            }
        }
    }
```

## ポイント①：移管対象の項目を指定する
※この処理は必ずしも必要ではありません
自分の担当しているアプリの場合、使用しているSDKがデフォルトのSharedPreferencesを使ってデータの読み書きをしています。
このため、ここで明示的に「（SDKではなく）アプリ側で管理している項目」を移管対象として指定する必要がありました。
SDKの管理する項目まで一括でEncryptedSharedPreferencesで管理すると、SDK側で復号できずエラーが発生するためです。
こういった場合は、移管対象をリスト化してやるのがベターです。

```kotlin
    private val keys = listOf(
        Pair("testKey1", false), // キーと暗号化済みの項目か？をPairでまとめる
        Pair("testKey2", true)
    )
```

## ポイント②：移管した項目を削除する
既存のSharedPreferencesからEncryptedSharedPreferencesへ移管し終わった項目については、既存のSharedPreferencesからデータを消しておきます。
こうすることで、次回アプリ起動時にはこの移管処理が走らなくなります。

# まとめ
EncryptedSharedPreferencesを用いると、アプリ側でAndroidKeyStoreManagerを用いることなく、キーと値を暗号化することができます。
これからローンチするアプリにおいては、最初からこの方式で読み書きを行えば良いですが、既存のアプリの場合は旧Verとの整合性を取る必要があるため、本記事に記載したような移管処理が必要になります。
実務においてはこのようなケースが大半と思いますので、どなたかのお役に立てれば幸いです :tada: 

# 参考記事
- [Android Developers](https://developer.android.com/topic/security/data)
- [How to Migrate to Encrypted Shared Preferences](https://medium.com/att-israel/how-to-migrate-to-encrypted-shared-preferences-cc4105c03518)