Android加密学
欢迎来到Android加密学API的第二部分. 请务必参阅第一部分以加深理解. 看过的人, 真的恭喜你, 干得真漂亮! 现在, 请与我一起探索今天的主题, 我们将在这里解开以下谜题:
首先, 让我们以添加一些常量开始.
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"
我们稍后将逐行分析. 现在, 我们先获取我们的 Cipher
实例:
// If we have a look at the docs it says:
// Returns a Cipher object that implements the specified transformation.
val cipher = Cipher.getInstance(TRANSFORMATION)
现在的问题是, TRANSFORMATION是什么? 让我们一探究竟. 正如你所看到的, 变换是AES_ALGORITHM
, BLOCK_MODE
和PADDING
的组合.
AES_ALGORITHM: 我们将使用 AES进行加密. 这是一种对称密钥加密算法, 意味着加密和解密都使用相同的密钥. 让我们仔细看看 AES 是如何进行加密的. 假设我们有初始数据(纯文本). AES 将其分成较小的部分, 我们称之为块. 每个块都有一个固定的大小, 对于 AES 来说是128比特(16 字节). 它对每个区块分别加密, 然后进行多轮加密. 轮数取决于密钥长度. 它可以是128/192/256比特. 这就是我们需要知道的全部内容, 但你也可以深入研究.
在 API 23 级(Android 6)之前, 我们没有对称密钥. 从 API 23 开始就有了.
块模式: 块模式定义了 AES 等块密码如何以块为单位处理和加密数据. 它们决定了加密过程中这些数据块之间的交互. 有 ECB, CBC, CFB 等不同的块模式. 在本教程中, 我们将使用CBC(密码块链). 在 CBC 模式下, 每个区块都与前一个ciphertext区块混合, 形成一个相互依存的加密区块链. 这可以防止相同的明文产生相同的密文, 并增加了一层额外的安全性.
嗯, 不, 实际上不会:)) 不过, 相同的明文也会得到相同的密文.
IV: 那么, 为什么相同的明文会得到相同的密文呢? CBC 中的连锁效应是基于将每个明文块与前一个块的密文进行XOR. 这就在区块之间产生了依赖关系. 但第一个区块是什么情况呢? 没有前一个区块. 这就是初始化向量(IV) -- 一个随机生成的值, 在加密前与第一个明文块相结合. 它的大小与区块相同, 即16 字节. 由于它是随机的, 每次都会为每个区块生成不同的混合值, 因此相同的明文总能得到不同的密文.
PADDING: 如前所述, CBC 以固定大小的区块处理数据. 然而, 并不是所有信息都能完全放入这些数据块中. 这就是填充的作用. 它增加了额外的字节, 以确保数据与数据块大小一致. 填充有 PKCS5, Zero Padding, ISO Padding等. 我们将使用 PKCS7.
因此, 以下是所有相关步骤: 首先, 我们将数据分割成块(AES). 如果数据不能完全放入数据块中, 我们就在其中添加额外的字节(Padding). 然后, 我们生成一个随机 IV, 并在加密第一个数据块时将其混合(CBC). 最后, 我们得到的密文即使是相同的明文也总是不同的. 这就是我们传递给 Cipher 的TRANSFORMATION.
使用 AES CBC 模式加密数据.
使用带Padding的 AES CBC 模式加密数据..
使用 AES CBC 模式解密数据.
回到我们的代码. 要开始使用Cipher
实例, 我们需要为特定的操作用密钥初始化它.
// Operation Mode - Cipher.ENCRYPT_MODE
// Key - SecretKeySpec(keyValue, AES_ALGORITHM)
// Algorithm params - IvParameterSpec(iv)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyValue, AES_ALGORITHM), IvParameterSpec(iv))
对于操作模式, 我们通过 ENCRYPT_MODE 来进行加密. 同样, 如果要解密, 我们也要传递 DECRYPT_MODE. 第二个参数是我们的密钥. 我们稍后再讨论. 第三个参数是初始化向量 (IV). 让我们看看如何生成它.
private fun generateRandomIV(size: Int): ByteArray {
val random = SecureRandom()
val iv = ByteArray(size)
random.nextBytes(iv)
return iv
}
在这里, 我们生成一个给定大小的随机 IV. 初始化密码并生成 IV 后, 我们就可以开始加密了. 首先, 我将展示完整的代码, 然后我们将逐行查看:
@Throws(Exception::class)
fun encrypt(inputText: String): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
val iv = generateRandomIV(cipher.blockSize)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyValue, AES_ALGORITHM), IvParameterSpec(iv))
val encryptedBytes = cipher.doFinal(inputText.toByteArray())
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)
}
值得一提的是, 我们有密码的块大小. 此外, 该函数可能会抛出多个异常, 因此我们只需抛出一个普通异常即可.
在处理加密/解密时, 错误处理是必不可少的.
val encryptedBytes = cipher.doFinal(inputText.toByteArray())
doFinal()
函数将ByteArray
作为输入并进行加密. 因此, 我们在输出中得到了加密字节. 但如何将它与我们的 IV 连接起来呢? 稍后, 我们应该在解密时使用相同的生成 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)
我们创建一个新的 ByteArray
, 其中包含生成的 IV 和加密字节的总长度. 首先, 我们使用 System.arrayCopy()
将我们的IV
复制到ByteArray
中. 我们的 IV 占用了最初的 16 个字节. 然后, 我们将加密字节追加到 IV 中. 最后, 我们使用 Base64
将其编码为字符串.
可以考虑直接处理
ByteArray
, 并在此函数之外将其编码为String
或其他类型. 为了简单起见, 我在同一个函数中进行了编码.
让我们看看我们的解密函数, 然后测试我们的代码.
@Throws(Exception::class)
fun decrypt(data: String): String {
val encryptedDataWithIV = Base64.decode(data, Base64.DEFAULT)
val cipher = Cipher.getInstance(TRANSFORMATION)
val iv = encryptedDataWithIV.copyOfRange(0, cipher.blockSize)
val encryptedData = encryptedDataWithIV.copyOfRange(cipher.blockSize, encryptedDataWithIV.size)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(keyValue, AES_ALGORITHM), IvParameterSpec(iv))
val decryptedBytes = cipher.doFinal(encryptedData)
return String(decryptedBytes, Charsets.UTF_8)
}
首先, 我们将 String
解码为 ByteArray
. 然后, 我们使用 copyOfRange()
函数分割 ByteArray
以从中提取 IV. 分离出 IV 和加密字节后, 我们就可以初始化 Cipher
并将 IV 作为参数传递. 正如你所看到的, 这次我们传递的是DECRYPT_MODE操作模式. 然后使用 doFinal()
函数解密数据. 最后, 我们将解密后的 ByteArray
转换为字符串.
如果仔细观察, 你会发现我们在加密和解密时都使用了相同的
doFinal()
. 那么, 该函数是如何知道应该加密还是解密我们的信息的呢? 它会根据我们的Cipher
初始化的操作模式来决定.
点击按钮时, 你可以通过传递输入文本来调用encrypt()
, 也可以通过传递加密数据来调用decrypt()
.
Button(onClick = { AESUtils.encrypt(input) }) {
Text(text = "Encrypt")
}
Button(onClick = { AESUtils.decrypt(encryptedText) }) {
Text(text = "Decrypt")
}
现在, 让我们测试一下我们的代码.
演示程序
运行正常. 你可以在本文末尾找到完整的代码.
SecretKeySpec
要对数据进行加密和解密, 我们需要一个密钥. 在上述示例中, 我们使用了
SecretKeySpec
类, 并将其作为密钥的参数传递. 该类用于从给定的字节数组中构造密钥.
但我们所说的字节数组是什么呢? 为了更好地理解, 让我们仔细看看它的构造函数.
/**
@param key - the key material of the secret key.
@param algorithm - the name of the secret-key algorithm to be associate with the given key material.
*/
public SecretKeySpec(byte[] key, String algorithm) { ... }
因此, 要构造一个键, 它需要一个key材料. 在这里, 它是一个byte[]
.
为了更好地理解, 可以把它看作是 key 的某种签名. 如果有人拥有该key材料(签名), 就可以构造出相同的密钥. 因此, 尽可能保证它的安全至关重要.
让我们看看如何创建ByteArray
(key材料).
private val keyValue = "keep_this_key_in_secret".toByteArray(Charsets.UTF_8)
如果我们运行代码并点击加密, 我们的应用程序就会崩溃, 并显示如下信息: java.security.InvalidKeyException: Unsupported key size: 23 bytes
.
为什么会这样呢? 上面我们提到了密钥长度的重要性. 对于 AES 来说, 密钥长度可以是 128, 192 或 256比特. 你可以在某些地方看到类似AES-192或AES-256的内容. 该值指定了密钥长度. 更长的密钥意味着更高的安全性. 如果你愿意, 可以深入研究. AES-128足以确保我们所需的安全性. 因此, 我们输入的是一个 ByteArray
, 即23字节或184比特. SecretKeySpec
无法找到具有该长度密钥的 AES 算法, 这就是它崩溃的原因. 对于 AES-128, 我们的密钥大小应该是 128 位或 16 字节. 让我们修复它.
private val keyValue = "keep_this_key_sc".toByteArray(Charsets.UTF_8)
正如我们所说, 尽可能保证安全是至关重要的. 在实际应用中, 至少应该考虑将其保存在本地属性中, 然后从本地属性中读取, 而不是硬编码. 为了便于演示, 我采用了硬编码方式.
有时, 你可能需要在某个存储空间中持久保存密钥. 为了确保这一操作的安全性, 你应该执行密钥封装. 简单地说, 就是创建一个新密钥, 并对要保存的密钥进行加密. 之后, 你应将该Base64
字符串存储到数据库或其他地方. 这也会给你带来额外的安全保障. 然后, 你就可以用你封装的密钥对其进行解封, 并执行加密操作.
密钥封装
重要: 密钥封装时应使用
Cipher.WRAP_MODE
.
如果要根据用户输入生成密钥怎么办? 例如, 一个受密码保护的应用程序. 为此, 你应该看看 PBEKeySpec.
关于Cipher
的内容今天就到这里啦!
一家之言, 欢迎斧正!
Happy Coding! Stay GOLDEN!