在2021年,Android中加密字符串的最佳且最安全的方法是什么?

3
我很惊讶地发现,Jatpack安全性仅提供对FileSharedPreferences加密的支持(链接1)。但是我需要能够加密和解密String,因为我想使用AccountManager并存储刷新和访问令牌,并且根据官方文档的建议,这种数据应该加密发送到AccountManagerhttps://developer.android.com/training/id-auth/custom_auth#Security
在网上搜索,有许多关于如何在Android上加密String的教程,但其中大部分似乎都比较古老,我担心选择错误的方法会导致在Play Store控制台上出现这种警告: enter image description here 那么,在2021年的Android应用程序中,什么是正确和安全的加密String的方式?Jetpack Security仍然可以在某种程度上使用(也许用于生成密钥?),为什么它不支持String加密,而只支持File和SharedPreferences?

如果不是在文件中存储字符串,你是如何存储它们的? - rossum
我需要使用AccountManager来存储刷新和访问令牌。在Android应用程序开发圈中,AccountManager似乎并不特别流行。你为什么觉得需要使用它呢?“为什么它不支持字符串加密”这样的问题不适合在Stack Overflow上提问。“为什么开发者X做出了决策Y?”这种问题通常只有开发者X才能回答,并且开发者X甚至可能不会看到这个问题。 - CommonsWare
@rossum 就像我在问题中所说的那样,使用 AccountManager:https://developer.android.com/reference/android/accounts/AccountManager#setPassword(android.accounts.Account,%20java.lang.String)。 - Roberto Leinardi
@CommonsWare 不太受欢迎并不是不使用它的有效论据。Telegram和TripAdvisor都在使用它。而且,在深入研究后,我喜欢它的工作方式,并想尝试使用它。坦白地说,我不太关心“为什么开发者X做出决策Y”。我关心的是“如何实现X”,但是,我得到的回答却是“为什么不尝试Y”。https://i.redd.it/per2eihv0jn31.png - Roberto Leinardi
我喜欢它的工作方式,想尝试使用它。请注意,“AccountManager”文档并不建议存储加密字符串,而是“具有密码学安全性的令牌”。在许多情况下,这类似于由服务器提供的API密钥。但是,如果您真的想在那里存储加密字符串,使用“KeyStore”是一种相当常见的做法,这也是Jetpack Security使用的方法。 - CommonsWare
显示剩余3条评论
1个回答

0
经过深入研究EncryptedSharedPreferencesEncryptedFile的实现,我成功创建了一个CryptoHelper类,它使用Jetpack Security中2个类相同的方法,提供了一种加密、解密、签名和验证ByteArray的方式。
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"
    }
}

别忘了将 com.google.crypto.tink:tink-android 添加到实现依赖项中,因为Jetpack安全性不会将其公开作为api。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接