LoginSignup
4
3

More than 5 years have passed since last update.

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

Posted at

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

開発環境

Android Studio 3.2.1
Kotlin 1.3.11

仕上がり

こんな感じです。

API22(RSA)

Screenshot_1545841975.png Screenshot_1545841795.png

API28(AES)

Screenshot_1545840678.png Screenshot_1545840683.png

コード

レイアウト

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

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ファイルにトップレベル関数で実装しています。

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3