ユーザーの新規登録→ログインをアプリで実装していたのですが、途中でパスワードを安全に保存する必要があることに気づいて調べました。
公式ドキュメント に情報がありますが、Android 6.x以上でしか使えない情報でした。
本エントリではAndroid 4.3以降でパスワードを暗号化し保存/復号化して取得する方法について説明します。
コード
暗号化に必要なコードは次の通り
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に保存して、次のような関数を使って保存/取得します。
/**
* パスワードを暗号化して保存する
*/
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