核心趋势
- Google已明建议弃用
EncryptedSharedPreferences - 要实现加密的现代做法是使用
DataStore和Android Keystore - 下面是一些定义和操作流程
Android Keystore 常量定义
kotlin
// 1. 密钥库提供者名称(固定值)
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
-
这是指定"仓库"的类型
-
在Android 中,你可以把密钥存在内存里面,也可以存在文件里面,但安全系数肯定都不如存放在
AndroidKeyStore中- 所以这句话作用就是让系统知道我们要把密钥存在Android专门的硬件安全模块
kotlin
// 2. 密钥别名
private const val KEYSTORE_ALIAS = "keystore_alias"
-
这标识密钥,密钥别名,相当于这个密钥的身份证
-
在
AndroidKeyStore中,可以存放很多个密钥- 用这个在这个仓库中唯一标识你的密钥
- 下次使用的时候,通过这个Alias取仓库找到对应的密钥,无需记住复杂的二进制数据
kotlin
// 3. 定义了加密和解密操作中使用的算法、模式和填充方式
private const val TRANSFORMATION = "AES/GCM/NoPadding"
-
这指定了加密方法
-
遵循算法/模式/填充的格式
-
算法: AES
-
决定锁的类型
-
对称加密算法(加密和解密用同一把钥匙)
-
速度快,适合加密大量数据
-
其他可选参数
- RSA:非对称加密 安全性极高但速度慢
-
-
模式:GCM
-
决定锁的操作逻辑
-
当你的数据很长的时候,算法需要分块处理,模式决定了这些块怎么连接
-
GCM 不仅加密(把加密内容变成乱码)还会有一个身份标识,只要其中任何一个字节被修改了,解密的时候会直接抛出异常
-
其他可选参数
- ECB 最简单的模式 不安全
- CBC 比ECB安全,但是无法防止篡改
-
-
填充:NoPadding
-
决定如何塞满
-
大部分加密算法要求的数据长度必须是固定数的倍数
-
NoPadding 是不填充
-
看到这你可能有点疑惑,为什么要求必须是固定数的倍数,但是却选择不填充呢
- 这是因为GCM 这个模式它可以处理任意长度的数据不需要对齐
-
其他可选参数
-
PKCS7Padding / PKCS5Padding:如果数据不够长,在末尾自动填充特定格式的字节(常用于 AES/CBC 模式)。
PKCS1Padding:专门用于 RSA 算法的填充方式。
-
-
-
获取或生成密钥
kotlin
private fun getOrCreateKey(): SecretKey {
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
keyStore.getKey(KEYSTORE_ALIAS, null)?.let { return it as SecretKey }
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
val spec = KeyGenParameterSpec.Builder(
KEYSTORE_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256) // 使用 256 位加密
.build()
keyGenerator.init(spec)
return keyGenerator.generateKey()
}
-
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }-
KeyStore.getInstance(KEYSTORE_PROVIDER):- 这个
KEYSTORE_PROVIDER是前面定义的"AndroidKeyStore",作用是告诉系统,我要找密钥库
- 这个
-
.apply { load(null) }:- KeyStore 对象在刚创建的时候需要初始化,这个
load(null)就是初始化/加载仓库 - 如果没有这一步,后面的任何读取或者写入数据的操作都会报错
- KeyStore 对象在刚创建的时候需要初始化,这个
-
-
keyStore.getKey(KEYSTORE_ALIAS, null)?.let { return it as SecretKey }-
getKey(KEYSTORE_ALIAS, null):- 这个是在库里面搜索别名是KEYSTORE_ALIAS的钥匙
- 第二个参数是用于保护钥匙的密码,但由于
AndroidKeyStore是由系统硬件保护的,它不需要应用层额外提供密码,所以传null
-
?.let { return it as SecretKey }:-
这是一个复用逻辑。如果找到了(不为null),说明之前已经生成过密钥。
-
这个操作的重要性:
- 对于
DataStore,必须复用同一把要是。如果每次都是重新生成新的钥匙,之前加密的数据就永远无法解密
- 对于
-
-
-
如果没有找到钥匙,程序会继续往下走,开始生产新的钥匙
-
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)-
KeyGenerator- 这是专门生产对称加密 (AES)钥匙的工具。同样指定由
AndroidKeySyore生产,确保钥匙直接生成在安全硬件内
- 这是专门生产对称加密 (AES)钥匙的工具。同样指定由
-
scss
val spec = KeyGenParameterSpec.Builder(
KEYSTORE_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256) // 使用 256 位加密
.build()
-
定义钥匙的规格说明书
-
PURPOSE_ENCRYPT or PURPOSE_DECRYPT:- 限制这把钥匙只能用于加密和解密,不能用于签名等其他用途。
-
BLOCK_MODE_GCM:- 指定必须配合GCM模式使用,这是目前最安全的模式。
-
ENCRYPTION_PADDING_NONE:- 因为GCM模式不需要填充,所以设置为NoPadding
-
setKeySize(256):- 设置密钥长度。256 位是 AES 的最高安全等级。
-
kotlin
keyGenerator.init(spec)
return keyGenerator.generateKey()
-
init(spec):- 把刚才定义的"图纸"交给造锁匠
-
generateKey():-
正式在硬件中生成钥匙。
-
神奇之处:
- 生成的密钥数据
直接存储在硬件安全模块里,方法返回的SecretKey 对象其实只是一个门牌号,你的App代码永远无法通过key.getEncoded() 拿到真正的二进制原始密钥。
- 生成的密钥数据
-
不需要要亲自动手操作钥匙,至少需要把锁和信给硬件安全模块就可以了
-
解密
kotlin
fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
val iv = cipher.iv // 系统自动生成的 12 字节随机 IV
val encrypted = cipher.doFinal(bytes)
// 关键步骤:先写 IV 长度(虽然通常固定12),再写 IV,最后写密文
outputStream.use {
it.write(iv)
it.write(encrypted)
}
return encrypted
}
-
val cipher = Cipher.getInstance(TRANSFORMATION):- 获取Cipher的实例
- Cipher接受明文(或密文)、钥匙(Key)、算法参数,然后输出处理后的结果。
- 它不存储任何数据,只负责计算。
- 所以你需要告诉他你的加密模式
-
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey()):- 初始化Cipher
- 告知Cipher是进行加密还是解密,所需要的钥匙是什么。
从输入流中读取数据并解密
kotlin
fun decrypt(inputStream: InputStream): ByteArray {
return inputStream.use {
val iv = ByteArray(12) // GCM 默认 IV 长度
it.read(iv) // 先读取前 12 字节得到 IV
val encryptedBytes = it.readBytes() // 剩下的是密文
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(128, iv) // 128 位是认证标签长度
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
cipher.doFinal(encryptedBytes)
}
}
-
return inputStream.use {}:- 这个是开启流式处理并自动管理关闭。
- use 是Kotlin的一个扩展函数,保证了无论是否成功,在操作结束后文件流都可以被自动关闭。
- 防止内存泄露
-
val iv = ByteArray(12):- 准备IV的容器
- 先划出一个12字节的空间,放置获取出来的IV
-
it.read(iv):- 获取IV
-
val encryptedBytes = it.readBytes():- 去除前12字节后,后面的是加密的数据。
- 这是获取加密之后的数据
-
val cipher = Cipher.getInstance(TRANSFORMATION):- 获取处理器
- 告诉系统我要处理的算法是TRANSFORMATION
-
val spec = GCMParameterSpec(128, iv):- 前面128是认证标签,在加密的时候会自动生成一个128位标签,放在最后。指定这个长度才会去验证数据是否完整。
-
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec):- 现在我们有了目的,钥匙,密文,这一步就是明确这次的任务就是解密
-
cipher.doFinal(encryptedBytes):- 明确任务后,就该执行任务了
- 返回的值就是解密之后的数据
- 并且如果数据被篡改了,这一步会抛出异常,让我们发现。
定义序列化
kotlin
@Serializable
data class UserSettings(
val token: String = "",
val userId: String = ""
)
-
@Serializable:- 这是一个注解,告诉编译器,或者可以序列化
kotlin
object UserSettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings = UserSettings()
// DataStore 从磁盘读数据时,会自动调用这个方法
override suspend fun readFrom(input: InputStream): UserSettings {
return try {
val decryptedBytes = CryptoManager.decrypt(input)
Json.decodeFromString(decryptedBytes.decodeToString())
} catch (e: Exception) {
defaultValue
}
}
// 你调用 dataStore.updateData 时,会自动调用这个方法
override suspend fun writeTo(t: UserSettings, output: OutputStream) {
val bytes = Json.encodeToString(t).encodeToByteArray()
CryptoManager.encrypt(bytes, output)
}
}
-
object UserSettingsSerializer : Serializer<UserSettings> { override val defaultValue: UserSettings = UserSettings() }:-
Serializer<UserSettings>:- 告诉
DataStore。我是专门负责UserSetting defaultValue这是一个兜底方案,如果发生错误会返回这个默认值,防止App崩溃
- 告诉
-
kotlin
override suspend fun readFrom(input: InputStream): UserSettings {
return try {
// 第一步:【解密】
// 这里的 input 是磁盘上的原始加密字节。
// 我们调用 CryptoManager.decrypt,它会自动读取 IV 并用硬件钥匙解密。
val decryptedBytes = CryptoManager.decrypt(input)
// 第二步:【反序列化】
// 将解密后的"明文字节"转回"JSON字符串",
// 再通过 Json.decodeFromString 把它还原成 UserSettings 对象。
Json.decodeFromString(
deserializer = UserSettings.serializer(),
string = decryptedBytes.decodeToString()
)
} catch (e: Exception) {
defaultValue // 出错就给默认值,稳健性极高
}
}
kotlin
override suspend fun writeTo(t: UserSettings, output: OutputStream) {
// 第一步:【序列化】
// 将 UserSettings 对象转换成 JSON 格式的字符串,再变成明文字节数组。
val bytes = Json.encodeToString(
serializer = UserSettings.serializer(),
value = t
).encodeToByteArray()
// 第二步:【加密】
// 调用 CryptoManager.encrypt,
// 它会自动生成一个随机 IV,并将加密后的数据顺着 output 写入磁盘。
CryptoManager.encrypt(bytes, output)
}