弃用 SharedPreferences:DataStore + Android Keystore 打造硬件级安全存储全攻略

核心趋势

  • 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.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生产,确保钥匙直接生成在安全硬件内
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)
}
相关推荐
hai_android5 小时前
LiveEventBusCore 跨 Activity 通讯源码解析
android·kotlin·android jetpack
我命由我123453 天前
Android 广播 - 组件通信
android·java·开发语言·android studio·android jetpack·android-studio·android runtime
alexhilton4 天前
为什么你的 LazyColumn 仍然卡顿
android·kotlin·android jetpack
hai_android4 天前
Android 嵌套滑动冲突:内部拦截 vs 外部拦截实战
android·android studio·android jetpack
alexhilton8 天前
模块化Jetpack Compose架构
android·kotlin·android jetpack
hai_android8 天前
InputStage 责任链核心执行流程
android·android studio·android jetpack
传奇开心果编程9 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程10 天前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程10 天前
【现代声明式UI学与练】第9课 性能优化——渲染优化、列表优化、内存优化、启动优化
学习·flutter·react native·ui·性能优化·swiftui·android jetpack
天空之城--12 天前
Android一周动态:Android 18首次官宣、Compose Material3 1.4转正(5趋势+5资讯)
android·人工智能·flutter·架构·android jetpack