👏
[Android]DataStore + tinkで文字列を暗号化して保存する
はじめに
何らかの文字列を暗号化して保存したい事ありますよね?
上記の用途で使えるものとしてAndroidXではEncryptedSharedPreferencesが提供されていますが、
- 現在、SharedPreferencesよりもDataStoreが推奨されている
- 1年近くEncryptedSharedPreferencesは新バージョンがリリースされておらず、今後のサポートが心配
- stableな1.0.0のEncryptedSharedPreferencesはAndroid6.0以上でしか使えない
- alphaな1.1.0であればAndroid5.0でも使えるが、alpha版も1年近く更新が無いので今から採用するのは不安がある
という事で、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)
}
}
Discussion