[Android][Kotlin] API23前後が共存するKeyStoreを使った暗号処理

業務でAndroid5系以上をサポートしているアプリでKeyStoreを使った暗号化処理を実装する必要があり、結構ハマったので備忘を兼ねて記事にします。
尚、パフォーマンスを考慮してAPI22以前はRSA方式、API23以降はAES方式としています。

# 開発環境
Android Studio 3.2.1
Kotlin 1.3.11

# 仕上がり
こんな感じです。

###API22(RSA)
![Screenshot_1545841975.png](https://qiita-image-store.s3.amazonaws.com/0/172991/b543637f-8880-64c7-bc83-7e081e654c63.png) ![Screenshot_1545841795.png](https://qiita-image-store.s3.amazonaws.com/0/172991/71c8b4b1-e541-b264-2848-b305552c18bf.png)

###API28(AES)
![Screenshot_1545840678.png](https://qiita-image-store.s3.amazonaws.com/0/172991/45c08a69-2133-41c6-9ee2-555ad94c7614.png) ![Screenshot_1545840683.png](https://qiita-image-store.s3.amazonaws.com/0/172991/78b6dc64-aa47-47e2-4278-652c12707294.png)

#コード
###レイアウト
```xml:activity_main
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <EditText
            android:id="@+id/editText"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginTop="50dp"
            android:layout_marginStart="25dp"
            android:layout_marginEnd="25dp"/>

    <LinearLayout
            android:id="@+id/buttonLayout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintTop_toBottomOf="@+id/editText"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_margin="25dp">

        <Button
                android:id="@+id/encryptButton"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="暗号化"/>

        <Button
                android:id="@+id/decryptButton"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="復号"/>

    </LinearLayout>

    <TextView
            android:id="@+id/encryptTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@+id/buttonLayout"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_margin="25dp"/>

</android.support.constraint.ConstraintLayout>
```

###Activity
```kotlin:MainActivity
import android.content.Context
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val PREF_NAME = "sample_pref"
    private val SAVED_KEY = "encrypted_text"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val pref = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
        encryptButton.setOnClickListener {
            if (editText.text.isNullOrEmpty()) return@setOnClickListener

            try {
                encrypt(this, editText.text.toString())?.let { str ->
                    encryptTextView.text = str
                    editText.text = null
                }
            } catch (e: Exception) {
                println(e.localizedMessage)
            }

        }

        decryptButton.setOnClickListener {
            if (encryptTextView.text.isNullOrEmpty()) return@setOnClickListener

            try {
                decrypt(encryptTextView.text.toString())?.let { str ->
                    editText.setText(str)
                    encryptTextView.text = null
                }
            } catch (e: Exception) {
                println(e.localizedMessage)
            }
        }
    }
}
```

###暗号処理ファイル
kotlinファイルにトップレベル関数で実装しています。

```kotlin:Encryption.kt
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.math.BigInteger
import java.nio.charset.Charset
import java.security.Key
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.KeyStoreException
import java.util.*
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec
import javax.security.auth.x500.X500Principal

// 共通定義
private const val PROVIDER = "AndroidKeyStore"
private const val KEY_STORE_ALIAS = "this_apps_alias"

// API22以下で利用
private const val ALGORITHM = "RSA"
private const val CIPHER_TRANSFORMATION_RSA = "RSA/ECB/PKCS1Padding"

// API23以上で利用
@TargetApi(23)
private const val CIPHER_TRANSFORMATION_AES = "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}"

// APIレベルで分岐して暗号化
fun encrypt(context: Context, plainText: String): String? {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        encryptAES(plainText)
    } else {
        encryptRSA(context, plainText)
    }
}

// APIレベルで分岐して復号
fun decrypt(encryptedText: String): String? {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        decryptAES(encryptedText)
    } else {
        decryptRSA(encryptedText)
    }
}

// API22以下用暗号化メソッド
private fun encryptRSA(context: Context, plainText: String): String? {

    val start = Calendar.getInstance()
    val end = Calendar.getInstance()
    end.add(Calendar.YEAR, 100)

    val keyStore = KeyStore.getInstance(PROVIDER).apply {
        load(null)
    }.also {
        if (!it.containsAlias(KEY_STORE_ALIAS)) {
            KeyPairGenerator.getInstance(ALGORITHM, PROVIDER).apply {
                initialize(
                    KeyPairGeneratorSpec.Builder(context)
                        .setAlias(KEY_STORE_ALIAS)
                        .setSubject(X500Principal("CN=$KEY_STORE_ALIAS"))
                        .setSerialNumber(BigInteger.ONE)
                        .setStartDate(start.time)
                        .setEndDate(end.time)
                        .build()
                )
            }.run {
                generateKeyPair()
            }
        }
    }
    val key = keyStore.getCertificate(KEY_STORE_ALIAS).publicKey

    return Cipher.getInstance(CIPHER_TRANSFORMATION_RSA).apply {
        init(Cipher.ENCRYPT_MODE, key)
    }.run {
        this.doFinal(plainText.toByteArray(Charset.defaultCharset()))
    }.let {
        Base64.encodeToString(it, Base64.DEFAULT)
    }
}

// API22以下用復号メソッド
private fun decryptRSA(encryptedText: String): String? {

    val keyStore = KeyStore.getInstance(PROVIDER).apply {
        load(null)
    }.also {
        if (!it.containsAlias(KEY_STORE_ALIAS)) return null
    }

    val privateKey = keyStore.getKey(KEY_STORE_ALIAS, null)
    return Cipher.getInstance(CIPHER_TRANSFORMATION_RSA).apply {
        init(Cipher.DECRYPT_MODE, privateKey)
    }.run {
        this.doFinal(Base64.decode(encryptedText, Base64.DEFAULT))
    }?.let {
        String(it)
    }
}

// API23以上用暗号化メソッド
@TargetApi(23)
private fun encryptAES(plainText: String): String? {
    val keyStore = KeyStore.getInstance(PROVIDER).apply {
        load(null)
    }

    val key = getKey(keyStore)
    val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION_AES).apply {
        init(Cipher.ENCRYPT_MODE, key)
    }
    val ivStr = Base64.encodeToString(cipher.iv, Base64.DEFAULT)
    val textBytes = cipher.doFinal(plainText.toByteArray(Charset.defaultCharset()))
    return ivStr + Base64.encodeToString(textBytes, Base64.DEFAULT)
}

// API23以上用復号メソッド
@TargetApi(23)
private fun decryptAES(encryptedText: String): String? {
    val keyStore = KeyStore.getInstance(PROVIDER).apply {
        load(null)
    }.also {
        if (!it.containsAlias(KEY_STORE_ALIAS)) return null
    }

    val splitText = encryptedText.split("\n").also {
        if (it.size < 2) return null
    }
    val ivStr = splitText[0]
    val encryptedBytes = Base64.decode(splitText[1], Base64.DEFAULT)
    val key = keyStore.getKey(KEY_STORE_ALIAS, null)
    val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION_AES).apply {
        init(Cipher.DECRYPT_MODE, key, IvParameterSpec(Base64.decode(ivStr, Base64.DEFAULT)))
    }
    val decryptedBytes = cipher.doFinal(encryptedBytes)
    return String(decryptedBytes)
}

// API23以上用共通鍵取得メソッド
@TargetApi(23)
@Throws(KeyStoreException::class)
private fun getKey(keyStore: KeyStore): Key {
    // 既に鍵を所有していたらRSAか否かで処理を分岐
    if (keyStore.containsAlias(KEY_STORE_ALIAS)) {
        keyStore.getKey(KEY_STORE_ALIAS, null).let {
            if (it.algorithm == ALGORITHM) {
                // RSAだったら削除して例外をスロー
                keyStore.deleteEntry(KEY_STORE_ALIAS)
                throw KeyStoreException("Mismatch key")
            } else {
                // AESだったら鍵を返す
                return it
            }
        }
    }

    // 鍵が無ければ作って返す
    KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, PROVIDER).apply {
        init(
            KeyGenParameterSpec.Builder(KEY_STORE_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
                .setCertificateSubject(X500Principal("CN=$KEY_STORE_ALIAS"))
                .setCertificateSerialNumber(BigInteger.ONE)
                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                .build()
        )
    }.run {
        generateKey()
    }

    return keyStore.getKey(KEY_STORE_ALIAS, null)
}
```

#最後に
暗号処理の専門ではないので、ロジックおかしいなどあればどんどんアドバイスお願い致します！

###以下参考にさせていただきました
https://qiita.com/asksaito/items/1793b8d8b3069b0b8d68
http://kei-sakaki.jp/2013/08/09/encryption-and-decryption/
https://proandroiddev.com/secure-data-in-android-initialization-vector-6ca1c659762c
https://qiita.com/sekitaka_1214/items/1942621118bba78ddf5b
https://developer.android.com/reference/javax/crypto/Cipher
http://www.nttdata.com/jp/ja/insights/blog/20180313.html
https://qiita.com/wakwak/items/e25b0c0a8d2d5148e429
https://qiita.com/KashikomaSweet/items/0b746e44b33f541d09dc
https://paonejp.github.io/2017/11/04/making_kotlin_appauth_android_demo_application.html