【Kotlin/Android】Google Tink Cryptoの使い方!暗号化/複合化の方法

この記事からわかること
- Android Studio/KotlinのGoogle Tink Cryptoの使い方
- 暗号化・複合化を実装する方法
- AEAD/MACとは?
- AES-GCMを利用して暗号化するには?
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
Google Tink Crypto
Source:Google Tink Crypto
公式リファレンス:Tink Cryptographic Library
「Google Tink Crypto」はGoogleが暗号化やデジタル署名などのセキュリティ機能を提供するライブラリです。オープンソースかつクロスプラットフォームで活用することができるのでAndroidアプリだけでなくiOSアプリでも機能を利用することができます。
AndroidのKey StoreやiOSのKeychainを利用した鍵の暗号化や保存機能も提供してくれているので複雑な暗号化や鍵の取り扱いを少ないコードで実装することができるようになっています。
Tinkでは「AEAD」などの暗号化方式や「MAC」などの整合性を検証する機能をサポートしています。
AEADとは
AEAD(Authenticated Encryption with Associated Data)とはデータの機密性と完全性、だけでなく改ざん検出のための認証性も提供する暗号化方式です。データの暗号化だけでなく暗号文に認証情報を付加し、データの改ざんを検出できるようになっています。
AEADでは「AES:共通鍵暗号方式(Advanced Encryption Standard)」や「ChaCha20-Poly1305」などの暗号化アルゴリズムを使用します。
暗号化や複合化についてや暗号化アルゴリズムの種類については以下記事も参考にしてください。
おすすめ記事:暗号化と複合化について
MACとは
MAC(Message Authentication Code)はメッセージの整合性を検証するための認証タグを生成する技術です。具体的にはメッセージに対してMACアルゴリズム(HMACなど)を使用して生成された固定長のタグ(MAC)を算出しmメッセージが改ざんされていないことを確認することができます。
導入方法
AndroidアプリにTinkを導入するには「build.gradle」に以下のように導入します。
dependencies {
implementation 'com.google.crypto.tink:tink-android:1.7.0'
}
TinkでAEADのAES-GCMを利用して文字列を暗号化・複合化する方法
Tinkを使用して「AEADのAES-GCMを利用して文字列を暗号化・複合化する方法」をまとめていきます。暗号化・複合化を行う専用クラスを実装していきます。
- Tinkを初期化
- AndroidKeysetManagerを使用してKeysetを管理
- 暗号化
- 複合化
1.Tinkを初期化
暗号化・複合化を行う機能を保持するTinkManager
を定義します。まずはイニシャライザの中でAeadConfig
を初期化します。
class TinkManager(context: Context) {
init {
// Tinkを初期化
AeadConfig.register()
}
}
2.AndroidKeysetManagerを使用してKeysetを管理
続いてTinkに定義されているAndroidKeysetManager
クラスを使用して暗号化鍵を安全に保存する処理を実装します。キーセットはSharedPreferences
に暗号鍵自体はKeyStore
に保存することができるようです。
class TinkManager(context: Context) {
/** 暗号鍵セットを管理用 */
private val keysetHandle: KeysetHandle
/** AEADプリミティブを保持 */
private val aead: Aead
init {
// Tinkを初期化
AeadConfig.register()
// AndroidKeysetManagerクラスで暗号鍵を安全に保存
// 保存先にはSharedPreferencesを活用
// 暗号化アルゴリズムをAES256-GCMに指定
keysetHandle = AndroidKeysetManager.Builder()
// 保存先ファイル名とキーを指定
.withSharedPref(context, "master_keyset", "master_key_preference")
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
.withMasterKeyUri("android-keystore://master_key")
.build()
.keysetHandle
// AEADプリミティブを取得
aead = keysetHandle.getPrimitive(Aead::class.java)
}
}
3.暗号化
生成したAEADプリミティブを使用して文字列の暗号化を行います。暗号化はaead.encrypt
で行います。暗号化する際に認証情報を渡すことでよりデータの整合性を確保することができますがこのデータは暗号化されないようです。
/**
* 暗号化
* @param plaintext
* 暗号化対象のテキスト
* @param associatedData
* 認証用データ
* @return
* 暗号化されたバイト配列
*/
public fun encrypt(text: String, associatedData: String? = null): ByteArray {
val plaintextBytes = text.toByteArray(StandardCharsets.UTF_8)
val associatedDataBytes = associatedData?.toByteArray(StandardCharsets.UTF_8)
return aead.encrypt(plaintextBytes, associatedDataBytes)
}
4.複合化
生成したAEADプリミティブを使用して暗号化された文字列の複合化を行います。暗号化はaead.decrypt
で行います。暗号化する際に認証情報を渡している場合は同じ認証情報文字列を渡します。
/**
* 複合化
* @param ciphertext
* 複合化対象のテキスト(暗号化された文字列)
* @param associatedData
* 認証用データ
* @return
* 複合化した文字列
*/
public fun decrypt(ciphertext: ByteArray, associatedData: String? = null): String {
val associatedDataBytes = associatedData?.toByteArray(StandardCharsets.UTF_8)
val decryptedBytes = aead.decrypt(ciphertext, associatedDataBytes)
return String(decryptedBytes, StandardCharsets.UTF_8)
}
使用方法
button.setOnClickListener{
// TinkManagerインスタンスを生成
val tinkManager = TinkManager(this)
// データを暗号化
val text = "Hello World!"
val ciphertext = tinkManager.encrypt(text)
println("Encrypted data: ${ciphertext}") // Encrypted data: [B@5cd1921
// データを復号化
val decryptedText = tinkManager.decrypt(ciphertext)
println("Decrypted data: $decryptedText") // Decrypted data: Hello World!
}
認証情報を渡してみる
AEADの特徴である認証情報を渡して暗号化・複合化を行なってみます。認証情報文字列として渡すのはユーザーIDなどになるかと思います。
val plaintext = "Hello World!"
// 認証情報を渡す
val userId = UUID.randomUUID().toString()
val ciphertext = tinkManager.encrypt(plaintext, userId)
println("Encrypted data: ${ciphertext}")
val decryptedText = tinkManager.decrypt(ciphertext, userId)
println("Decrypted data: ${decryptedText.isSuccess}")
同じ認証情報を使用すれば問題なく暗号化・複合化を行うことができますが、暗号化・複合化に異なる認証情報を渡した場合は複合化の際にjava.security.GeneralSecurityException: decryption failed
という例外を吐きます。
FATAL EXCEPTION: main
Process: com.XXXXXX.プロジェクト名, PID: 19291
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:562)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: java.security.GeneralSecurityException: decryption failed
at com.google.crypto.tink.aead.AeadWrapper$WrappedAead.decrypt(AeadWrapper.java:112)
at com.XXXXXX.プロジェクト名.Utility.TinkManager.decrypt(TinkManager.kt:62)
そのため複合化するdecrypt
メソッドは例外をキャッチできるようにしておいた方が安全です。
public fun decrypt(ciphertext: ByteArray, associatedData: String? = null): Result<String> {
val associatedDataBytes = associatedData?.toByteArray(StandardCharsets.UTF_8)
return kotlin.runCatching {
val decryptedBytes = aead.decrypt(ciphertext, associatedDataBytes)
String(decryptedBytes, StandardCharsets.UTF_8)
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。