【コピペで使える!】 Android Keystoreシステムを使ってパスワードを安全に管理する


ユーザーの新規登録→ログインをアプリで実装していたのですが、途中でパスワードを安全に保存する必要があることに気づいて調べました。

[公式ドキュメント](https://developer.android.com/training/articles/keystore?hl=ja#SupportedCiphers) に情報がありますが、Android 6.x以上でしか使えない情報でした。

本エントリではAndroid 4.3以降でパスワードを暗号化し保存/復号化して取得する方法について説明します。

## コード
暗号化に必要なコードは次の通り

```kotlin
import android.content.Context
import android.security.KeyPairGeneratorSpec
import android.util.Base64
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.util.*
import javax.crypto.Cipher
import javax.security.auth.x500.X500Principal


val PROVIDER = "AndroidKeyStore"
val ALGORITHM = "RSA"
val CIPHER_TRANSFORMATION = "RSA/ECB/PKCS1Padding"

/**
 * テキストを暗号化する
 * @param context
 * @param alias キーペアを識別するためのエリアス。用途ごとに一意にする。
 * @param plainText 暗号化したいテキスト
 * @return 暗号化されBase64でラップされた文字列
 */
fun encrypt(context: Context, alias: String, plainText: String): String {
    val keyStore = KeyStore.getInstance(PROVIDER)
    keyStore.load(null)

    // キーペアがない場合生成
    if (!keyStore.containsAlias(alias)) {
        val keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER)
        keyPairGenerator.initialize(createKeyPairGeneratorSpec(context, alias))
        keyPairGenerator.generateKeyPair()
    }
    val publicKey = keyStore.getCertificate(alias).getPublicKey()
    val privateKey = keyStore.getKey(alias, null)

    // 公開鍵で暗号化
    val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
    cipher.init(Cipher.ENCRYPT_MODE, publicKey)
    val bytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))

    // SharedPreferencesに保存しやすいようにBase64でString化
    return Base64.encodeToString(bytes, Base64.DEFAULT)
}

/**
 * 暗号化されたテキストを復号化する
 * @param alias キーペアを識別するためのエリアス。用途ごとに一意にする。
 * @param encryptedText encryptで暗号化されたテキスト
 * @return 復号化された文字列
 */
fun decrypt(alias: String, encryptedText: String): String? {
    val keyStore = KeyStore.getInstance(PROVIDER)
    keyStore.load(null)
    if (!keyStore.containsAlias(alias)) {
        return null
    }

    // 秘密鍵で復号化
    val privateKey = keyStore.getKey(alias, null)
    val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
    cipher.init(Cipher.DECRYPT_MODE, privateKey)
    val bytes = Base64.decode(encryptedText, Base64.DEFAULT)

    val b = cipher.doFinal(bytes)
    return String(b)
}

/**
 * キーペアを生成する
 */
fun createKeyPairGeneratorSpec(context: Context, alias: String): KeyPairGeneratorSpec {
    val start = Calendar.getInstance()
    val end = Calendar.getInstance()
    end.add(Calendar.YEAR, 100)

    return KeyPairGeneratorSpec.Builder(context)
            .setAlias(alias)
            .setSubject(X500Principal(String.format("CN=%s", alias)))
            .setSerialNumber(BigInteger.valueOf(1000000))
            .setStartDate(start.getTime())
            .setEndDate(end.getTime())
            .build()
}

```

どうやらキーストアはキーペアの管理を安全にしてくれるだけみたいです。キーを安全に管理してアクセスできるようにしてあげるから、キーを使って好きに暗号化/復号化してねってことで、暗号化されたコンテンツの保存はキーストアの役割でないようです。
今回はSharedPreferencesに保存します。

### 使い方
暗号化されたテキストは見られても問題ない(←これほんとにホント？)のでSharedPreferencesに保存して、次のような関数を使って保存/取得します。

```kotlin
/**
 * パスワードを暗号化して保存する
 */
fun savePassword(context: Context, plainPassword: String) {
    val encryptedPassword = encrypt(context, "password", plainPassword)
    val editor = getSharedPreferences(context).edit()
    editor.putString("encrypted_password", encryptedPassword).commit()
}

/**
 * パスワードを復号化して取得する
 */
fun getPassword(context: Context): String? {
    val encryptedPassword = getSharedPreferences(context).getString("encrypted_password", null)
    if (encryptedPassword == null) {
        return encryptedPassword
    }
    val plainPassword = decrypt("password", encryptedPassword)
    return plainPassword
}
```

## 参考
https://qiita.com/f_nishio/items/485490dea126dbbb5001
https://qiita.com/Koganes/items/e8253f13ecb534ca11a1