DataStore + Tink で保存したシークレット情報を Flutter へ引き継いでみた

DataStore + Tink で保存したシークレット情報を Flutter へ引き継いでみた

2026.04.16

こんにちは。
リテールアプリ共創部マッハチーム所属のたじまです!

この記事でわかること

  • DataStore + Tink で保存されたデータを Flutter から直接読めない理由
  • Tink で直接暗号化されたデータを Flutter へ引き継ぐ方法

はじめに

EncryptedSharedPreferences は Jetpack Security 1.1.0-alpha07(2025年4月)で deprecated になりました。

本記事では、代替として DataStore(永続化)+ Tink(暗号化)+ Android Keystore(鍵管理)の組み合わせで保存されたシークレット情報を、Flutter アプリへ引き継ぐ方法を解説します。


1. なぜ直接引き継げないのか

Kotlin 側で DataStore + Tink を使って保存したデータは、Tink の暗号鍵が Android Keystore(ハードウェア)で保護されています。

Flutter(Dart)から Android Keystore に直接アクセスする手段がないため、MethodChannel 経由でネイティブコードに復号してもらう必要があります。


2. Flutter 側の実装

依存関係の追加

pubspec.yaml:

dependencies:
  flutter_secure_storage: ^9.0.0

flutter_secure_storage は Android 上では内部で Android Keystore を使用するため、移行後もセキュリティレベルはネイティブと同等です。

移行ロジック

final channel = MethodChannel('migration_channel');
final storage = FlutterSecureStorage();

// flutter_secure_storage にデータがなければ、ネイティブから移行
final existing = await storage.read(key: 'secret');
if (existing == null) {
  // Kotlin ネイティブ側で暗号化された値を復号し、平文として取得
  final secret = await channel.invokeMethod<String>('getMigratedSecret');
  // 値が存在すれば flutter_secure_storage に保存
  if (secret != null) {
    await storage.write(key: 'secret', value: secret);
  }
}

移行フロー


3. Android ネイティブ側の実装

Flutter プロジェクトの android/app/ に以下の Kotlin コードを配置します。

依存関係の追加(android/app/build.gradle.kts)

dependencies {
    implementation("androidx.datastore:datastore-preferences:1.1.1")
    implementation("com.google.crypto.tink:tink-android:1.13.0")
}

TinkCryptoManager

Kotlin アプリ側で暗号化に使われた同じ鍵セットを使って復号するヘルパーです。

import android.content.Context
import android.util.Base64
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.aead.AeadConfig
import com.google.crypto.tink.integration.android.AndroidKeysetManager

class TinkCryptoManager(context: Context) {

    private val aead: Aead

    init {
        AeadConfig.register()
        aead = AndroidKeysetManager.Builder()
            .withSharedPref(context, "master_keyset", "master_key_pref") // Kotlin側と同じデータ暗号鍵の保存先を指定
            .withKeyTemplate(KeyTemplates.get("AES256_GCM"))             // Kotlin側と同じ暗号化アルゴリズムを指定
            .withMasterKeyUri("android-keystore://tink_master_key")      // Kotlin側と同じマスターキーの保存先を指定
            .build()
            .keysetHandle
            .getPrimitive(Aead::class.java)
    }

    // Base64 エンコードされた暗号文を平文に復号
    fun decrypt(ciphertext: String): String {
        val encrypted = Base64.decode(ciphertext, Base64.NO_WRAP)
        return String(aead.decrypt(encrypted, null), Charsets.UTF_8)
    }
}

SecureStorage

DataStore から暗号化された値を読み出して復号するクラスです。

import android.content.Context
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

// Kotlin側と同じ DataStore ファイル名を指定
private val Context.dataStore by preferencesDataStore(name = "secure_prefs")

class SecureStorage(
    private val context: Context,
    private val crypto: TinkCryptoManager
) {
    companion object {
        // Kotlin側で保存時に使ったキー名と一致させる
        private val KEY_SECRET = stringPreferencesKey("secret")
    }

    // DataStore から暗号化された値を取得し、TinkCryptoManager で復号して返す
    fun get(): Flow<String?> = context.dataStore.data.map { prefs ->
        prefs[KEY_SECRET]?.let { crypto.decrypt(it) }
    }
}

MainActivity.kt(MethodChannel の登録)

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        val crypto = TinkCryptoManager(applicationContext)
        val storage = SecureStorage(applicationContext, crypto)

        // Flutter から Kotlin ネイティブの復号処理を呼び出せるよう MethodChannel を設定
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "migration_channel")
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getMigratedSecret" -> {
                        kotlinx.coroutines.MainScope().launch {
                            val secret = storage.get().first()
                            // 復号済みの平文を Flutter 側に返却
                            result.success(secret)
                        }
                    }
                    else -> result.notImplemented()
                }
            }
    }
}

まとめ

項目 内容
Flutter 側の保存先 flutter_secure_storage
移行方法 MethodChannel 経由でネイティブから復号済みの値を受け取る
直接移行の可否 不可(Android Keystore に Dart からアクセスできないため)

本記事では MethodChannel + flutter_secure_storage を使った移行方法を紹介しました。

ただし、flutter_secure_storage は Android 上では内部的に EncryptedSharedPreferences(SharedPreferences)を使用しており、Kotlin 側で DataStore に移行しても Flutter 側では SharedPreferences に保存し直すことになります。

SharedPreferencesAsync と暗号化パッケージの組み合わせなど、他の選択肢も含めて検討の余地があります。

その他のマッハチームの記事

https://dev.classmethod.jp/articles/git-worktree-useful-for-flutter-development/
https://dev.classmethod.jp/articles/dart-dot-shorthands/

この記事をシェアする

関連記事