3

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?

Roberto Leinardi
  • 10,641
  • 6
  • 65
  • 69
  • Store your strings in a file. Encrypt the file. – rossum Nov 13 '21 at 12:05
  • Is that really the best practice to encrypt Strings? – Roberto Leinardi Nov 13 '21 at 12:30
  • How are you storing your strings if not in a file? – rossum Nov 13 '21 at 12:36
  • "I need to use the AccountManager and to store refresh and access tokens" -- `AccountManager` seems like it is not particularly popular in Android app development circles. Why do you feel that you need to use it? "why it does not support String encryption out of the box" -- questions of the form "why did Developer X make Decision Y?" are not great for Stack Overflow. Usually, only Developer X can answer the question, and it is unlikely that Developer X will even see the question. – CommonsWare Nov 13 '21 at 13:20
  • @rossum like I said in the question, with the `AccountManager`: https://developer.android.com/reference/android/accounts/AccountManager#setPassword(android.accounts.Account,%20java.lang.String) – Roberto Leinardi Nov 13 '21 at 13:54
  • @CommonsWare not being popular isn't per se a valid argument for not using it. Telegram and TripAdvisor use it. And, after digging into it, I like how it works and I'd like to try to use it. Frankly I don't care too much about "why did Developer X make Decision Y". What I care is "how can I achieve X" but, instead of the answer, I'm getting told "why don't you instead try Y". https://i.redd.it/per2eihv0jn31.png – Roberto Leinardi Nov 13 '21 at 13:58
  • "I like how it works and I'd like to try to use it" -- that is fine, though that is not how you phrased your question. Note that the `AccountManager` docs do not suggest storing an encrypted string per se, but rather a "cryptographically secure token". In many cases, that is something like a server-supplied API key. Still, if you really want to store an encrypted string there, using the `KeyStore` is a fairly common practice and is what Jetpack Security uses. – CommonsWare Nov 13 '21 at 14:11
  • @CommonsWare sorry, English is not my first language. I tried to rephrase it to make clear that it's a wish of my to use the `AccountManager` and not a real need. I'll try to find some comprehensive documentation about the `KeyStore`, I'm just afraid to do something wrong since I'm not really experienced in the encryption field. – Roberto Leinardi Nov 13 '21 at 14:26

1 Answers1

0

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.

Roberto Leinardi
  • 10,641
  • 6
  • 65
  • 69