[Android]DataStore + tinkで文字列を暗号化して保存する

9319 ワード

はじめに

何らかの文字列を暗号化して保存したい事ありますよね?
上記の用途で使えるものとしてAndroidXではEncryptedSharedPreferencesが提供されていますが、

という事で、DataStoreを使いつつ文字列を暗号化/復号化する方針で検討をおこないました。

どうやって暗号化・復号化をおこなうか?

結論から言うと、Google Tinkを使いました。

  • Googleヘルプの「安全では無い暗号化の修正」でおすすめの方法の一つとして紹介されている
  • EncryptedSharedPreferencesも内部的にはTinkを使って暗号化・復号化をおこなっている
  • (ここら辺、ちょっと理解が合っているか不安なところなのですが)暗号化・復号化のマスターキーはKeystoreで生成・保持するのが安全だが、Android6.0未満では使用できる暗号化アルゴリズムに制限がある
    • しかし、Tinkはこれを良い感じに扱ってくれる
    • 少なくともEncryptedSharedPreferencesの1.1.0-alpha3と同じTinkの使い方をすれば、大きな問題は無いはず

一方、DataStoreはProtoと組み合わせて使用する例がGoogle のドキュメントに記載されていますが、DataStoreそのものはProtoに直接依存していません。
なので、自前でSerializerを実装すれば、Proto無しでも型安全なDataStoreをつくる事が出来ます。

コードの例

こんな感じのSerializerを作れば、あとはGoogleのドキュメントに記載されているとおり読み書きするだけです。

@JvmInline
value class Token(val value: String)

class TokenSerializer @Inject constructor(
    @ApplicationContext appContext: Context,
) : Serializer<Token> {
    private val aead: Aead by lazy {
        AeadConfig.register()
        AndroidKeysetManager.Builder()
            .withSharedPref(
                appContext,
                "master_keyset", // keysetName
                "master_key_preference" // prefFileName
            )
            .withKeyTemplate(KeyTemplates.get("AES256_GCM"))
            .withMasterKeyUri("android-keystore://master_key")
            .build()
            .keysetHandle
            .getPrimitive(Aead::class.java)
    }

    override val defaultValue = Token("")

    override suspend fun readFrom(input: InputStream): Token {
        val encrypted = input.readBytes()

        try {
            val decrypted = aead.decrypt(encrypted, null)

            return Token(String(decrypted))
        } catch (exception: GeneralSecurityException) {
            throw CorruptionException("Cannot decrypt.", exception)
        }
    }

    override suspend fun writeTo(t: Token, output: OutputStream) {
        val encrypted = aead.encrypt(t.value.toByteArray(), null)
        output.write(encrypted)
    }
}