暗号

[Kotlin] AES-GCM 暗号化/復号 サンプルコード

この記事では、KotlinでAES GCMモードの暗号化と復号を行うサンプルコードを紹介します。
このサンプルコードは以下の機能を持ちます。

  • バイナリデータの暗号化/復号
  • Base64テキストの暗号化/復号
  • Hexテキストの暗号化/復号

実際に利用できるクラスのソースコード全文と一般的な使用方法を記載していますので、ご自身のプロジェクトでご活用いただければ幸いです。
また、暗号の結果が正しいことを検証するために使用したテストコードも載せています。

ここでは AES-GCM 自体の解説は行いません。
GCMモードの仕組みについて興味がある方は以下の記事も一緒にご覧下さい。

AES-GCM 仕組みの解説とJavaサンプルコード

この記事では、共通鍵暗号のデファクトスタンダードとなっている、AES-GCMモードについての解説をしています。 AES-GCMとは、近年様々な用途で用いられているAESの暗号モードの一つです。主な利用 ...

続きを見る

ゴイチ

それでは行ってみましょう!

暗号化/復号クラス

公開メソッド機能
encryptバイナリデータの暗号化
decryptバイナリデータの復号
encryptAsBase64Base64テキストの暗号化
decryptAsBase64Base64テキストの復号
encryptAsHexHex(16進)テキストの暗号化
decryptAsHexHex(16進)テキストの復号
import org.apache.commons.codec.binary.Hex
import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

class AesGcmCipher {
    private val GCM_CIPHER_MODE = "AES/GCM/NoPadding" // Cipher mode
    private val GCM_NONCE_LENGTH = 12 // Nonce length
    private val GCM_AAD_MAX_LENGTH = 64 * 1024 // AAD Max length

    private val key: SecretKey
    private val tagBitLen: Int
    private val aad: ByteArray?
    private val random = SecureRandom()

    constructor(key: ByteArray, tagBitLen: Int = 128, aad: ByteArray? = null) {
        assert(listOf(128, 192, 256).contains(key.size * 8)) // Must be one of {128, 192, 256} bit len
        assert(listOf(128, 120, 112, 104, 96).contains(tagBitLen)) // Must be one of {128, 120, 112, 104, 96} bit len
        aad?.let { assert(it.size < GCM_AAD_MAX_LENGTH) } // Must be less than 64KiB

        this.key = SecretKeySpec(key, "AES")
        this.tagBitLen = tagBitLen
        this.aad = aad
    }

    fun encrypt(plainData: ByteArray): ByteArray {
        // Generate Cipher Instance
        val cipher = generateCipher(Cipher.ENCRYPT_MODE)

        // Perform Encryption
        val encryptData = cipher.doFinal(plainData)

        // Return nonce + Encrypt Data
        return cipher.iv + encryptData
    }

    fun decrypt(cipherData: ByteArray): ByteArray {
        val nonce = cipherData.copyOfRange(0, GCM_NONCE_LENGTH)
        val encryptData = cipherData.copyOfRange(GCM_NONCE_LENGTH, cipherData.size)

        // Generate Cipher Instance
        val cipher = generateCipher(Cipher.DECRYPT_MODE, nonce)

        // Perform Decryption
        return cipher.doFinal(encryptData)
    }

    fun encryptAsBase64(plainTextBase64: String): String {
        return Base64.getEncoder().encodeToString(
            encrypt(Base64.getDecoder().decode(plainTextBase64))
        )
    }

    fun decryptAsBase64(cipherTextBase64: String): String {
        return Base64.getEncoder().encodeToString(
            decrypt(Base64.getDecoder().decode(cipherTextBase64))
        )
    }

    fun encryptAsHex(plainTextHex: String): String {
        return Hex.encodeHexString(
            encrypt(Hex.decodeHex(plainTextHex))
        )
    }

    fun decryptAsHex(cipherTextHex: String): String {
        return Hex.encodeHexString(
            decrypt(Hex.decodeHex(cipherTextHex))
        )
    }

    private fun generateCipher(mode: Int, nonceToDecrypt: ByteArray? = null): Cipher {
        // Get Cipher Instance
        val cipher = Cipher.getInstance(GCM_CIPHER_MODE)

        // Get nonce
        val nonce = when (mode) {
            Cipher.ENCRYPT_MODE -> {
                // Generate nonce
                val nonceToEncrypt = ByteArray(GCM_NONCE_LENGTH)
                random.nextBytes(nonceToEncrypt)
                nonceToEncrypt
            }
            Cipher.DECRYPT_MODE -> {
                nonceToDecrypt ?: throw IllegalArgumentException()
            }
            else -> throw IllegalArgumentException()
        }

        // Create GCMParameterSpec
        val gcmParameterSpec = GCMParameterSpec(tagBitLen, nonce)

        // Initialize Cipher with mode/key/iv
        cipher.init(mode, key, gcmParameterSpec)
        aad?.let {
            // Update AAD to do additional auth (Optional)
            cipher.updateAAD(it)
        }

        return cipher
    }
}

使用方法

val plainText = "This is a plain text."
val keyBase64 = "XkbPC5uQWTF6UWFx/FeRjlZPaqQtQqRKLt6lbZsbQf4="

// Generate Cipher instance
val key = Base64.getDecoder().decode(keyBase64) // key as binary data
val cipher = AesGcmCipher(key) // tagLen is default 128bit, no AAD

// Encryption
val encryptData = cipher.encrypt(plainText.toByteArray())

// Decryption
val decryptData = cipher.decrypt(encryptData)

println("Decrypt Text: ${String(decryptData)}")
// Decrypt Text: This is a plain text.

テストコード

import org.apache.commons.codec.binary.Hex
import java.security.SecureRandom
import java.util.*
import kotlin.test.Test

class AesGcmCipherTest {
    private val secureRandom = SecureRandom()

    @Test
    fun test() {
        repeat(IntRange(1, 100).count()) {
            // Randomize key size
            val key = ByteArray(intArrayOf(16, 24, 32).random())
            // Make random key
            secureRandom.nextBytes(key)
            // Make cipher instance
            val aesGcmCipher = AesGcmCipher(key)

            // Make random plain
            val plain = ByteArray(secureRandom.nextInt(65535))
            secureRandom.nextBytes(plain)

            // encrypt
            val encrypted = aesGcmCipher.encrypt(plain)
            val decrypted = aesGcmCipher.decrypt(encrypted)
            assert(plain contentEquals decrypted)

            // encrypt as Base64
            val plainBase64 = Base64.getEncoder().encodeToString(plain)
            val encryptedBase64 = aesGcmCipher.encryptAsBase64(plainBase64)
            val decryptedBase64 = aesGcmCipher.decryptAsBase64(encryptedBase64)
            assert(plainBase64 contentEquals decryptedBase64)

            // encrypt as Hex string
            val plainHex = Hex.encodeHexString(plain)
            val encryptedHex = aesGcmCipher.encryptAsHex(plainHex)
            val decryptedHex = aesGcmCipher.decryptAsHex(encryptedHex)
            assert(plainHex contentEquals decryptedHex)
        }
    }

    @Test
    fun test2() {
        repeat(IntRange(1, 100).count()) {
            // Randomize key size
            val key = ByteArray(intArrayOf(16, 24, 32).random())
            // Make random key
            secureRandom.nextBytes(key)

            // Randomize tag length
            val tagBitLen = listOf(128, 120, 112, 104, 96).random()
            // Randomize AAD
            val aad = ByteArray(secureRandom.nextInt(64 * 1024))
            secureRandom.nextBytes(aad)

            // Make cipher instance
            val aesGcmCipher = AesGcmCipher(key, tagBitLen, aad)

            // Make random plain
            val plain = ByteArray(secureRandom.nextInt(65535))
            secureRandom.nextBytes(plain)

            // encrypt
            val encrypted = aesGcmCipher.encrypt(plain)
            val decrypted = aesGcmCipher.decrypt(encrypted)
            assert(plain contentEquals decrypted)
        }
    }
}

まとめ

Kotlinで AES GCMモード 暗号化する例を紹介しました。

GCMモードは、IVを毎回必ず乱数にしなければならないという仕様があり、今回のサンプルコードにも盛り込んでいます。
また、TagやAADなど従来の暗号モードには無い機能が追加されており、仕組みを知らないと理解するのが少し難しいと思います。

以下の記事には、GCMモードのNonce、TagやAADに関しても詳しく解説していますので、より深く知りたい方は是非ご覧下さい。

AES-GCM 仕組みの解説とJavaサンプルコード

この記事では、共通鍵暗号のデファクトスタンダードとなっている、AES-GCMモードについての解説をしています。 AES-GCMとは、近年様々な用途で用いられているAESの暗号モードの一つです。主な利用 ...

続きを見る

本ブログでは、他にも 暗号に関する記事 を多数投稿していますので、興味があれば読んでみて下さい。

それでは、また他の記事でお会いしましょう!

この記事は役に立ちましたか?

  • この記事を書いた人
アバター画像

ゴイチ

ソフトウェアエンジニア歴20年。 C/C++、Java、C#、Kotlinが得意で、組込系からAndroidアプリ、大規模なWebサービスなど幅広いプログラミング経験があります。 現在は某SNSの会社でWebエンジニアをしています。

-暗号
-,