Android 加密学
之前, 我们使用SecretKeySpec
来生成密钥. 我们创建了一个key材料, 它是一个字节数组, 然后把它传给SecretKeySpec
, SecretKeySpec
从中构造出一个密钥.
但它真的那么安全吗?
让我们回忆一下上一部分中遇到的问题.
首先--key材料长度. 我们应该总是给出一个精确的密钥长度(16/24/32). 否则, 我们的应用就会出现InvalidKeyException: Unsupported key size.
第二, 我们是生成key材料的人. 老实说, 这并不那么安全, 如果有人获取了key材料, 就能创建相同的密钥.
即使我们进行了密钥封装并为第二把密钥生成了随机key材料, 我们仍然需要处理我们的第一把密钥. 此外, 密钥一般存在于不安全的环境中, 例如Android OS. Android OS或我们的应用都有可能被入侵. 如你所见, 这种方法并不那么安全.
这就是Android Keystore 系统, 它能为我们的密钥提供安全存储. 它在 Trusty OS 提供的 可信执行环境 (TEE) 中运行. 它处于设备处理器或芯片组(SoC)内的隔离环境中, 即与设备的 OS脱钩.
可信 TEE 概览图
因此, 它可以保护我们的key和key材料, 防止被提取和未经授权的访问. key材料永远不会进入应用流程. 即使攻击者成功访问了我们的key, 他们也无法提取key材料.
通过Keymaster HAL 访问 Keymaster
我们不会深入研究上图, 但你要知道 -- 这个keymaster模块是用来保护我们的加密密钥安全的. 它在 TEE 中运行, 与Android OS完全分离. Keymaster TA(可信应用)也称为硬件支持的Keystore, 它提供所有安全的Keystore操作, 并拥有我们的key材料. 为保护key材料, 密钥在 TEE 内部使用硬件派生key加密. 它不会将key材料暴露在可信环境之外. 相反, 它只公开所谓的密钥“blobs”, 即封装(加密)密钥. 此外, 封装, 解封等所有操作都在 Keymaster TA 内部完成.
加密和解密部分与我们之前的代码大致相同. 不同之处在于我们如何生成密钥. 这一次, 让我们做得更简洁一些, 并为加密/解密创建一个接口.
interface CipherManager {
@Throws(Exception::class)
fun encrypt(inputText: String): String
@Throws(Exception::class)
fun decrypt(data: String): String
}
再次考虑直接使用
ByteArray
.
让我们创建实现类并定义一些常量.
class CipherManagerImpl : CipherManager {
@Throws(Exception::class)
override fun encrypt(inputText: String): String {
TODO("Not yet implemented")
}
@Throws(Exception::class)
override fun decrypt(data: String): String {
TODO("Not yet implemented")
}
companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val AES_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val TRANSFORMATION = "$AES_ALGORITHM/$BLOCK_MODE/$PADDING"
}
}
使用这种方法, 你还可以创建其他实现类. 在常量部分, 除了 ANDROID_KEY_STORE 是我们用来获取 Keystore 实例的提供者名称外, 一切都和之前一样.
private val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply {
load(null) // With load function we initialize our keystore
}
KeyGenerator 是一个类, 很明显, 它是一个用于生成加密密钥的类. 不过, 它是用来生成对称密钥的. 要生成非对称密钥, 你应该看看KeyPairGenerator, 它可以生成一对公钥和一对私钥.
private val keyAlias = "aes_key_alias" // TODO - Should be secure
@Throws(Exception::class)
private fun createKey(): SecretKey {
@Throws(Exception::class)
private fun createKey(): SecretKey {
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(BLOCK_MODE)
.setEncryptionPaddings(PADDING)
.setUserAuthenticationRequired(false)
.setRandomizedEncryptionRequired(true)
.build()
return KeyGenerator.getInstance(AES_ALGORITHM).apply {
init(keyGenParameterSpec)
}.generateKey()
}
}
在创建 KeyGenerator
之前, 我们先创建一个 KeyGenParameterSpec
. 简单地说, 这是一个类, 我们通过向其传递不同的参数来定制密钥生成. 让我们逐行分析它.
你可以在
KeyGenerator
和KeyPairGenerator
中使用它, 但仅限于 API 23 以上版本. 在此之前, 对于KeyPairGenerator
你应该使用KeyPairGenParameterSpec
. 在 API 23 之前, 我们没有对称密钥.
我们使用 KeyGenParameterSpec.Builder
来构建我们的 KeyGenParameterSpec
. Builder
需要两个参数: alias 和 purpose. 将别名视为分配给 Android Keystore 中生成的密钥的唯一标识符. 稍后我们将使用它从 Keystore 获取密钥.
请注意, 我们无需手动生成任何key材料. 这不是很完美吗? 在
SecretKeySpec
中, 如果有人获得了key材料, 他们就可以在自己的设备上生成相同的密钥并使用它. 但在 Android Keystore 中, key材料永远不会进入应用流程. 即使有人获得了我们的别名, 他们也无法在自己的设备上创建相同的密钥, 因为key材料是不同的. 不过, 还是要考虑将别名保存在安全的地方:)
让我们继续. KeyGenParameterSpec.Builder
的第二个参数表示我们使用密钥的目的. 只需传递 PURPOSE_ENCRYPT
或 PURPOSE_DECRYPT
.
接下来, 我们调用多个生成器方法. 我们指定块模式和填充. 下面我们来谈谈另外两个函数:
setUserAuthenticationRequired(false) - 该方法用于指定使用密钥是否需要用户验证. 当设置为“true”时, 意味着密钥只能在用户通过 PIN, 密码或生物识别(如指纹或人脸识别)等方法进行身份验证后才能使用. 默认情况下, 无论用户是否通过身份验证, 密钥都被授权使用. 因此, 在本文的讨论范围内, 我们实际上并不需要这个功能. 我写这个函数只是想让你知道还有这样一个东西.
setRandomizedEncryptionRequired(true) - 根据 文档: 设置使用此密钥进行加密时是否必须充分随机化, 以便每次都能为相同的明文生成不同的密文. *但是, 使用随机 IV 不是每次都能得到不同的密文吗? 如果有人使用某个固定值而不是随机生成 IV 呢? 为了防止这种情况, Android Keystore 提供商不允许使用自定义 IV 值. 该函数指示你是否可以使用自己的 IV.
然而, 即使向该函数传递 false, 我们也会收到 java.security.InvalidAlgorithmParameterException: Caller-provided IV not permitted异常. Cipher 每次都会自动生成一个随机 IV.
KeyGenParameterSpec.Builder
的所有函数都可以在这里 找到.
最后, 我们获取 KeyGenerator
实例, 将 KeyGenParameterSpec
作为参数传递给它进行初始化, 然后调用 generateKey()
. 密钥将安全地存储在Android Keystore 系统中.
之后, 在执行加密/解密时, 我们应检查该密钥是否已经存在.
@Throws(Exception::class)
private fun getOrCreateKey(): SecretKey {
val existingKey = keyStore.getEntry(keyAlias, null) as? KeyStore.SecretKeyEntry
return existingKey?.secretKey ?: createKey()
}
如果密钥存在, 我们通过其别名检索并使用; 否则, 我们创建一个新密钥.
是时候进行加密了.
@Throws(Exception::class)
override fun encrypt(inputText: String): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
val encryptedBytes = cipher.doFinal(inputText.toByteArray())
val iv = cipher.iv
val encryptedDataWithIV = ByteArray(iv.size + encryptedBytes.size)
System.arraycopy(iv, 0, encryptedDataWithIV, 0, iv.size)
System.arraycopy(encryptedBytes, 0, encryptedDataWithIV, iv.size, encryptedBytes.size)
return Base64.encodeToString(encryptedDataWithIV, Base64.DEFAULT)
}
这与我们在 第二部分 中看到的非常相似. 当我们初始化Cipher
时, 在第二个参数中, 我们传递了生成的密钥. 但我们并没有像上一部分那样传递我们的IV. 如前所述, Cipher
会在内部处理随机 IV 生成(考虑到我们的块模式). 稍后, 我们可以从中获取 IV. 这里的函数与我们之前使用的函数类似. 如果还不清楚, 请参考上一部分的逐步解释.
同样, 解密函数如下所示:
@Throws(Exception::class)
override fun decrypt(data: String): String {
val encryptedDataWithIV = Base64.decode(data, Base64.DEFAULT)
val cipher = Cipher.getInstance(TRANSFORMATION)
val iv = encryptedDataWithIV.copyOfRange(0, cipher.blockSize)
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), IvParameterSpec(iv))
val encryptedData = encryptedDataWithIV.copyOfRange(cipher.blockSize, encryptedDataWithIV.size)
val decryptedBytes = cipher.doFinal(encryptedData)
return String(decryptedBytes, Charsets.UTF_8)
}
对于解密, 我们应该自己传递IV. 首先, 我们将其从加密数据中提取出来, 然后在 init
函数中传递一个 IvParameterSpec
.
让我们测试一下:)
应用演示
- 你可以使用
deleteEntry()
函数, 通过别名删除密钥条目. 如果无法删除条目, 将抛出 KeyStoreException.
keyStore.deleteEntry(keyAlias)
// Under the hood it is implemented this way
public final void deleteEntry(String alias) throws KeyStoreException
- 如果要加密SharedPreferences, 请使用 Android Security 库(Android Jetpack 的一部分)中的 EncryptedSharedPreferences. 你可以对密钥和值进行加密.
- 如果要加密 Room DB, 一种方法是通过上述步骤手动执行. 在将它们持久化到数据库之前, 可以先加密, 然后再持久化. 对于对象, 可以使用 JsonSerializer 或 Gson 将其序列化, 然后加密并持久化到数据库中. 另一种方法是使用 SQLCipher. 这是一个开源库, 使用它可以通过 AES-256 加密算法加密 SQLite 数据库文件.
- 在撰写本文时, AndroidX 未提供用于加密或解密 Datastore 的库. 因此, 如果要使用它, 可以在 Github 上找到第三方库, 但大多数情况下还是要手动操作. 你可以编写 Datastore 的
edit
和map
函数的扩展, 对对象进行序列化, 加密和持久化. 确保实现适当的异常处理.
今天的内容就分享到这里啦!
一家之言, 欢迎拍砖!
Happy Coding! Stay GOLDEN!