移动端安全存储:Keychain 与 EncryptedSharedPreferences
为什么移动应用需要安全存储
移动端应用常常需要保存用户的敏感信息,例如登录令牌(Token)、密码、API 密钥或支付信息。如果直接以明文方式存入 SharedPreferences(Android)或 UserDefaults(iOS),这些数据极易被逆向工程或设备数据提取工具窃取。安全存储方案正是为了在系统层面提供加密保护,使敏感数据难以被未授权访问。
本教程将带你入门 iOS 平台的 Keychain 和 Android 平台的 EncryptedSharedPreferences,帮助你用平台推荐的方式保护用户数据。
iOS 安全存储:Keychain
Keychain 是苹果提供的硬件级安全存储服务,数据以加密形式存储在设备的专用安全隔区中。相比 UserDefaults,Keychain 更适合保存密码、密钥等少量敏感数据,并且支持在应用卸载后仍可选择保留数据(取决于访问控制)。
Keychain 的基本概念
- Keychain Services API – C 语言接口,直接与钥匙串交互。
- Keychain Item – 每条记录称为一个项,通过一个唯一的标识符(如账号名和服务名)进行区分。
- kSecClass – 定义存储的数据类型,常用
kSecClassGenericPassword(通用密码)或kSecClassInternetPassword。 - 可访问性(Accessibility) – 通过
kSecAttrAccessible控制数据在设备锁定、解锁状态下的可访问级别,例如仅解锁时可访问(kSecAttrAccessibleWhenUnlockedThisDeviceOnly)。
使用 Keychain 存储与读取数据
直接使用原生 Keychain Services API 比较繁琐,建议采用轻量封装。以下示例展示一个简单的 Keychain 管理器类。
import Foundation
import Security
class KeychainManager {
static func save(key: String, value: String) -> Bool {
guard let valueData = value.data(using: .utf8) else { return false }
// 构造查询字典,指定服务名与账号
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.yourapp.service",
kSecAttrAccount as String: key,
kSecValueData as String: valueData,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
// 先删除已存在的同名项
SecItemDelete(query as CFDictionary)
// 添加新项
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
static func load(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.yourapp.service",
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
static func delete(key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.yourapp.service",
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess
}
}
// 使用示例
let saved = KeychainManager.save(key: "authToken", value: "eyJhbGciOi...")
if let token = KeychainManager.load(key: "authToken") {
print("读取到令牌: \(token)")
}
Keychain 访问控制与安全建议
- 使用最高限制级别的可访问性:若数据仅在本设备使用且应用卸载后无需保留,可选用
kSecAttrAccessibleWhenUnlockedThisDeviceOnly。如需后台传输或设备锁定后访问,请谨慎开放权限。 - 避免存储大量数据:Keychain 适合小型密钥或字符串,大文件应使用文件加密(Data Protection)。
- 添加应用层加密:对于特别敏感的数据,可以在存入 Keychain 前再进行一次 AES 加密,双重保护。
- 同步与备份:Keychain 条目默认参与 iCloud 密钥串同步(可通过
kSecAttrSynchronizable控制)。如果数据是设备特异的,务必关闭同步。
Android 安全存储:EncryptedSharedPreferences
Android 平台在 Jetpack Security 库中提供了 EncryptedSharedPreferences,它基于 SharedPreferences 的接口,但自动对键值对进行加密。内部使用 Android Keystore 系统生成和保存 AES 密钥,达到安全存储的目的。
添加依赖
在模块级 build.gradle.kts 或 build.gradle 中加入:
dependencies {
implementation "androidx.security:security-crypto:1.1.0-alpha06"
}
创建 EncryptedSharedPreferences 实例
使用 EncryptedSharedPreferences.create() 方法即可得到一个加密的 SharedPreferences 对象。需要提供 MasterKeys 或直接使用默认的密钥生成方案。
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class SecurePrefs(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val sharedPrefs = EncryptedSharedPreferences.create(
context,
"my_secure_prefs", // 文件名
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
读写操作
与普通 SharedPreferences 完全一致,只需通过 edit() 提交数据。
// 写入加密数据
sharedPrefs.edit()
.putString("authToken", "eyJhbGciOi...")
.apply()
// 读取数据
val token = sharedPrefs.getString("authToken", null)
对于更复杂的对象,可以结合 Gson 或 Kotlin Serialization 序列化后存入。
密钥管理与版本考量
- MasterKey 默认使用 Android Keystore 生成的密钥,攻击者无法在没有 root 权限的情况下导出密钥材料。
EncryptedSharedPreferences会为每个键、值分别加密(可通过加密方案控制),因此具有一定的防篡改能力。- 注意:如果使用
setKeyScheme指定自定义方案,需确保与 Android 版本兼容。AES256_GCM要求 API 23+。对于更低版本应用,考虑使用第三方安全存储库。
常见问题处理
- KeyStore 错误:部分设备制造商实现存在缺陷,可能导致
KeyStoreException。可以通过捕获异常并提示用户,或在create()方法中传入预定义的SharedPreferences作为降级方案(但会失去加密保护)。 - 数据迁移:如果从普通 SharedPreferences 迁移到
EncryptedSharedPreferences,需在升级逻辑中一次性读取明文数据并重新存入加密实例,然后删除原文件。 - 备份与还原:加密数据依赖于设备 Keystore,因此在设备间直接复制备份文件通常无效,应使用应用自身的账户系统或安全远程存储密钥。
Keychain 与 EncryptedSharedPreferences 对比
| 特性 | iOS Keychain | Android EncryptedSharedPreferences |
|---|---|---|
| 加密硬件支持 | 安全隔区,硬件根密钥 | 基于 Android Keystore(硬件支持因设备而异) |
| 存储类型 | 数据库(按项查询) | 键值对文件(基于 SharedPreferences) |
| 数据保留策略 | 可控制卸载后保留 | 随应用删除而被清除 |
| 跨应用共享 | 支持同开发者团队的应用组 | 不支持直接共享(需 ContentProvider 等) |
| API 易用性 | 原生 C API,第三方封装丰富 | 几乎与 SharedPreferences 用法一致,学习成本低 |
| 最低系统版本 | iOS 2.0+ | Android 6.0+(部分库要求 API 23+) |
两者均能安全保护用户凭据,选择时只需遵循各自平台的最佳实践即可。
实践建议与总结
- 不要尝试自己实现加密存储:平台提供的方案经历过安全社区的审查和大量测试,自己实现很容易留下漏洞。
- 最小化敏感数据的存储时间:令牌和密码在使用后尽快丢弃,仅存储刷新令牌等必要信息。
- 结合生物识别增加一层保护:iOS 可设置 Keychain 的可访问属性需要生物认证;Android 可配合 BiometricPrompt 加密/解密数据后再写入 EncryptedSharedPreferences。
- 定期轮换密钥:对于长期有效的 API Key,在服务端定期更换,客户端同步更新存储。
- 代码混淆与完整性检查:即使数据已加密,也应启用 ProGuard/R8 混淆和 Android 的 SafetyNet/App Check,降低逆向风险。
通过掌握 Keychain 和 EncryptedSharedPreferences,你就能在 iOS 与 Android 应用中以系统级安全强度存储用户的敏感信息,极大提升应用整体的安全性和用户信任度。请立即在你的下一个项目中将明文存储升级为安全存储方案吧!