Roberto Leinardi
Roberto Leinardi

Reputation: 14444

What is in 2021 the best and safest way to encrypt Strings in Android?

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 Strings 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: https://developer.android.com/training/id-auth/custom_auth#Security

Searching online there are plenty of tutorial on how to encrypt Strings 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: enter image description here

So, what would be the right and safe way to encrypt Strings 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 Files and SharedPreferences?

Upvotes: 3

Views: 936

Answers (1)

Roberto Leinardi
Roberto Leinardi

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 ByteArrays:

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

Related Questions