Android加密学API之 Cipher

社区Android
揭开 Android 加密的神秘面纱. Android加密学API - Cipher

picture.image Android加密学

欢迎来到Android加密学API的第二部分. 请务必参阅第一部分以加深理解. 看过的人, 真的恭喜你, 干得真漂亮! 现在, 请与我一起探索今天的主题, 我们将在这里解开以下谜题:

揭密 Cipher

首先, 让我们以添加一些常量开始.

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 (算法/块模式/填充)

现在的问题是, TRANSFORMATION是什么? 让我们一探究竟. 正如你所看到的, 变换是AES_ALGORITHM, BLOCK_MODEPADDING的组合.

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.

picture.image 使用 AES CBC 模式加密数据.

picture.image 使用带Padding的 AES CBC 模式加密数据..

picture.image 使用 AES CBC 模式解密数据.

# 使用Cipher加密和解密数据

回到我们的代码. 要开始使用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")
}

现在, 让我们测试一下我们的代码.

picture.image 演示程序

运行正常. 你可以在本文末尾找到完整的代码.

key材料和 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, 192256比特. 你可以在某些地方看到类似AES-192AES-256的内容. 该值指定了密钥长度. 更长的密钥意味着更高的安全性. 如果你愿意, 可以深入研究. AES-128足以确保我们所需的安全性. 因此, 我们输入的是一个 ByteArray, 即23字节184比特. SecretKeySpec 无法找到具有该长度密钥的 AES 算法, 这就是它崩溃的原因. 对于 AES-128, 我们的密钥大小应该是 128 位或 16 字节. 让我们修复它.

private val keyValue = "keep_this_key_sc".toByteArray(Charsets.UTF_8)

正如我们所说, 尽可能保证安全是至关重要的. 在实际应用中, 至少应该考虑将其保存在本地属性中, 然后从本地属性中读取, 而不是硬编码. 为了便于演示, 我采用了硬编码方式.

其他信息

有时, 你可能需要在某个存储空间中持久保存密钥. 为了确保这一操作的安全性, 你应该执行密钥封装. 简单地说, 就是创建一个新密钥, 并对要保存的密钥进行加密. 之后, 你应将该Base64字符串存储到数据库或其他地方. 这也会给你带来额外的安全保障. 然后, 你就可以用你封装的密钥对其进行解封, 并执行加密操作.

picture.image 密钥封装

重要: 密钥封装时应使用 Cipher.WRAP_MODE.

如果要根据用户输入生成密钥怎么办? 例如, 一个受密码保护的应用程序. 为此, 你应该看看 PBEKeySpec.

关于Cipher的内容今天就到这里啦!

一家之言, 欢迎斧正!

Happy Coding! Stay GOLDEN!

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
字节跳动客户端性能优化最佳实践
在用户日益增长、需求不断迭代的背景下,如何保证 APP 发布的稳定性和用户良好的使用体验?本次分享将结合字节跳动内部应用的实践案例,介绍应用性能优化的更多方向,以及 APM 团队对应用性能监控建设的探索和思考。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论