Reputation: 14444
I was surprised to find that Jatpack Security provides only support for File
and SharedPreferences
encryption. But I need to be able to encrypt and decrypt String
s because I want to use the AccountManager
and to store refresh and access tokens and, as suggested in the official documentation, this kind of data should be send encrypted to the AccountManager
:
Searching online there are plenty of tutorial on how to encrypt String
s on Android but most of them seems to be pretty old and I'm afraid to pick the wrong one that could lead to this kind of warnings on the Play Store Console:
So, what would be the right and safe way to encrypt String
s in an Android application in 2021? Can Jetpack Security still be used to some extend (perhaps to generate the keys?) and why it does not support String encryption out of the box, but only File
s and SharedPreferences
?
Upvotes: 3
Views: 936
Reputation: 14444
After a deep look at the implementation of EncryptedSharedPreferences
and EncryptedFile
, I manage to create a CryptoHelper
class that, using the same approach of the 2 classes from Jetpack Security, provides a way to encrypt, decrypt, sign and verify ByteArray
s:
import android.content.Context
import androidx.security.crypto.MasterKeys
import com.google.crypto.tink.Aead
import com.google.crypto.tink.DeterministicAead
import com.google.crypto.tink.KeyTemplate
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.PublicKeySign
import com.google.crypto.tink.PublicKeyVerify
import com.google.crypto.tink.aead.AeadConfig
import com.google.crypto.tink.daead.DeterministicAeadConfig
import com.google.crypto.tink.integration.android.AndroidKeysetManager
import com.google.crypto.tink.signature.SignatureConfig
import java.io.IOException
import java.security.GeneralSecurityException
/**
* Class used to encrypt, decrypt, sign ad verify data.
*
* <pre>
* // Encrypt
* val cypherText = cryptoHelper.encrypt(text.toByteArray())
* // Decrypt
* val plainText = cryptoHelper.decrypt(cypherText)
* // Sign
* val signature = cryptoHelper.sign(text.toByteArray())
* // Verify
* val verified = cryptoHelper.verify(signature, text.toByteArray())
* </pre>
*/
@Suppress("unused")
class CryptoHelper(
private val aead: Aead,
private val deterministicAead: DeterministicAead,
private val signer: PublicKeySign,
private val verifier: PublicKeyVerify,
) {
/**
* Builder class to configure CryptoHelper
*/
class Builder(
// Required parameters
private val context: Context,
) {
// Optional parameters
private var masterKeyAlias: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
private var keysetPrefName = KEYSET_PREF_NAME
private var keysetAlias = KEYSET_ALIAS
private var aeadKeyTemplate: KeyTemplate
private var deterministicAeadKeyTemplate: KeyTemplate
private var signKeyTemplate: KeyTemplate
init {
AeadConfig.register()
DeterministicAeadConfig.register()
SignatureConfig.register()
aeadKeyTemplate = KeyTemplates.get("AES256_GCM")
deterministicAeadKeyTemplate = KeyTemplates.get("AES256_SIV")
signKeyTemplate = KeyTemplates.get("ECDSA_P256")
}
/**
* @param masterKey The SharedPreferences file to store the keyset.
* @return This Builder
*/
fun setMasterKey(masterKey: String): Builder {
this.masterKeyAlias = masterKey
return this
}
/**
* @param keysetPrefName The SharedPreferences file to store the keyset.
* @return This Builder
*/
fun setKeysetPrefName(keysetPrefName: String): Builder {
this.keysetPrefName = keysetPrefName
return this
}
/**
* @param keysetAlias The alias in the SharedPreferences file to store the keyset.
* @return This Builder
*/
fun setKeysetAlias(keysetAlias: String): Builder {
this.keysetAlias = keysetAlias
return this
}
/**
* @param keyTemplate If the keyset for Aead encryption is not found or valid, generates a new one using keyTemplate.
* @return This Builder
*/
fun setAeadKeyTemplate(keyTemplate: KeyTemplate): Builder {
this.aeadKeyTemplate = keyTemplate
return this
}
/**
* @param keyTemplate If the keyset for deterministic Aead encryption is not found or valid, generates a new one using keyTemplate.
* @return This Builder
*/
fun setDeterministicAeadKeyTemplate(keyTemplate: KeyTemplate): Builder {
this.deterministicAeadKeyTemplate = keyTemplate
return this
}
/**
* @param keyTemplate If the keyset for signing/verifying is not found or valid, generates a new one using keyTemplate.
* @return This Builder
*/
fun setSignKeyTemplate(keyTemplate: KeyTemplate): Builder {
this.signKeyTemplate = keyTemplate
return this
}
/**
* @return An CryptoHelper with the specified parameters.
*/
@Throws(GeneralSecurityException::class, IOException::class)
fun build(): CryptoHelper {
val aeadKeysetHandle = AndroidKeysetManager.Builder()
.withKeyTemplate(aeadKeyTemplate)
.withSharedPref(context, keysetAlias + "_aead__", keysetPrefName)
.withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
.build().keysetHandle
val deterministicAeadKeysetHandle = AndroidKeysetManager.Builder()
.withKeyTemplate(deterministicAeadKeyTemplate)
.withSharedPref(context, keysetAlias + "_daead__", keysetPrefName)
.withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
.build().keysetHandle
val signKeysetHandle = AndroidKeysetManager.Builder()
.withKeyTemplate(signKeyTemplate)
.withSharedPref(context, keysetAlias + "_sign__", keysetPrefName)
.withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
.build().keysetHandle
val aead = aeadKeysetHandle.getPrimitive(Aead::class.java)
val deterministicAead = deterministicAeadKeysetHandle.getPrimitive(DeterministicAead::class.java)
val signer = signKeysetHandle.getPrimitive(PublicKeySign::class.java)
val verifier = signKeysetHandle.publicKeysetHandle.getPrimitive(PublicKeyVerify::class.java)
return CryptoHelper(aead, deterministicAead, signer, verifier)
}
}
fun encrypt(plainText: ByteArray, associatedData: ByteArray = ByteArray(0)): ByteArray =
aead.encrypt(plainText, associatedData)
fun decrypt(ciphertext: ByteArray, associatedData: ByteArray = ByteArray(0)): ByteArray =
aead.decrypt(ciphertext, associatedData)
fun encryptDeterministically(plainText: ByteArray, associatedData: ByteArray = ByteArray(0)): ByteArray =
deterministicAead.encryptDeterministically(plainText, associatedData)
fun decryptDeterministically(ciphertext: ByteArray, associatedData: ByteArray = ByteArray(0)): ByteArray =
deterministicAead.decryptDeterministically(ciphertext, associatedData)
fun sign(data: ByteArray): ByteArray =
signer.sign(data)
fun verify(signature: ByteArray, data: ByteArray): Boolean =
try {
verifier.verify(signature, data)
true
} catch (e: GeneralSecurityException) {
false
}
companion object {
private const val KEYSTORE_PATH_URI = "android-keystore://"
private const val KEYSET_PREF_NAME = "__crypto_helper_pref__"
private const val KEYSET_ALIAS = "__crypto_helper_keyset"
}
}
Don't forget to add com.google.crypto.tink:tink-android
as implementation dependency, since Jetpack Security does not exposes it as api.
Upvotes: 0