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

核心趋势

  • Google已明建议弃用EncryptedSharedPreferences
  • 要实现加密的现代做法是使用DataStoreAndroid 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)
}
相关推荐
撩得Android一次心动2 天前
Android Jetpack 概述
android·android jetpack
我命由我123452 天前
Android Jetpack Compose - Compose 重组、AlertDialog、LazyColumn、Column 与 Row
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
EnzoRay2 天前
DataBinding的使用
android jetpack
QING6183 天前
简单说下Kotlin 作用域函数中 apply 和 also 为什么不能空安全调用?
android·kotlin·android jetpack
神话20093 天前
使用 Jetpack Compose 和 ML Kit 打造现代化二维码扫描应用
android jetpack·composer
我命由我123453 天前
Android 控件 - 悬浮常驻文本交互(IBinder 实现、BroadcastReceiver 实现)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
我命由我123454 天前
Android Jetpack Compose - enableEdgeToEdge 函数、MaterialTheme 函数、remember 函数
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
林栩link4 天前
【车载Android】多媒体开发入门(上) - MediaSession
android·android jetpack