移动端开发中,一旦涉及用户密码、访问令牌、客户端证书或本地加密密钥,就不能再把这些数据随意写进 UserDefaults、SharedPreferences 或普通文件。iOS 和 Android 分别提供了系统级安全容器:Keychain 和 Keystore。它们虽然名字相似,但设计目标、工作方式和适用场景并不相同。把两者混为一谈,容易在跨平台方案中留下密钥管理漏洞。

一、Keychain 与 Keystore 的核心定位不同
Keychain 是 iOS、macOS 等苹果系统内置的加密数据库,设计目标是安全保存小体量的敏感数据,例如用户密码、访问令牌、Wi-Fi 密码、证书私钥以及对称加密密钥。它本质上更像一个受系统保护的键值存储,应用可以把任意 Data 写入指定条目,并通过服务名、账号名等属性进行查询。系统在写入和读取时自动完成加密和解密,不需要调用方关心加密算法细节。
Android Keystore 则不是用来存业务数据的。它是一套密钥管理系统,专注于生成密钥、保护密钥材料以及使用密钥执行签名、加密、解密等密码学运算。私钥或对称密钥一旦进入 Keystore,通常无法导出为明文,调用方拿到的只是一个引用或句柄。你的应用不能把用户的登录令牌直接塞进 Keystore,但可以让 Keystore 生成一把密钥,再用这把密钥去加密令牌后存储到私有目录或数据库。
两者最关键的区别可以这样理解:Keychain 负责保存敏感数据本身;Keystore 负责保存和保护用于加密敏感数据的密钥。前者解决的是数据静态存储问题,后者解决的是密钥生命周期与密码学运算隔离问题。如果你的跨平台方案只是要把 OAuth 刷新令牌持久化,iOS 端可以直接写 Keychain,而 Android 端更合理的做法是用 Keystore 保护一把主密钥,再用主密钥加密 SharedPreferences 中的令牌。
二、iOS Keychain 的存储边界与访问控制
在 iOS 上使用 Keychain 时,最常用的 API 是 SecItemAdd、SecItemCopyMatching、SecItemUpdate 和 SecItemDelete。与 UserDefaults 相比,Keychain 数据在设备锁屏后仍可保持加密,应用卸载后也可以按照系统策略保留,适合需要跨启动、跨重装恢复的令牌。不过 Keychain 并不是无限容量,单个条目通常建议控制在几 KB 以内,不适合存储图片、数据库备份等大对象。
下面是一个写入通用密码的 Swift 示例:
import Security
let account = "refresh_token"
let tokenData = Data("eyJhbGciOiJIUzI1NiJ9".utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: tokenData,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecSuccess {
print("令牌已写入 Keychain")
}
上面的 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 表示设备首次解锁后才可以访问,且数据不会随备份迁移到其他设备。这个后缀 ThisDeviceOnly 很关键:如果希望 Keychain 条目只留在本机,避免从旧手机恢复到新手机时泄露残留令牌,就应选择带有该后缀的访问级别。相反,如果希望用户换机后仍能恢复密钥,可以使用不带 ThisDeviceOnly 的选项,但需要评估同步备份路径的风险。
需要绑定生物识别或设备密码时,Keychain 还支持基于访问控制列表的约束。例如通过 SecAccessControlCreateWithFlags 设置 kSecAccessControlBiometryCurrentSet,可以要求每次读取时重新完成 Face ID 或 Touch ID 验证。这个能力与本地加密结合后,能防止攻击者在手机已解锁状态下直接通过备份或调试工具读取敏感条目。不过要注意,生物识别状态变化会导致之前写入的条目无法读取,因此需要设计合理的降级和恢复流程。
三、Android Keystore 的密钥保护与密码学运算
Android Keystore 的核心价值是让密钥材料尽量不离开安全环境。应用通过 KeyStore 类访问 AndroidKeyStore 这个系统级 Provider,可以生成 AES 对称密钥、RSA 或 EC 非对称密钥对。生成的私钥无法通过标准 Java 接口导出明文,即便应用进程被攻破,攻击者也只能调用受限的签名或解密接口,而拿不到原始密钥字节。部分设备还提供 StrongBox 硬件安全模块,将密钥直接放在独立芯片中,进一步降低内核被攻破后的风险。
生成一把受用户认证保护的 AES 密钥,可以使用如下 Kotlin 代码:
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
if (!keyStore.containsAlias("session_key")) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
"session_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(60)
.build()
keyGenerator.init(spec)
val secretKey: SecretKey = keyGenerator.generateKey()
}
其中 setUserAuthenticationRequired(true) 表示每次使用密钥前都必须验证用户身份,setUserAuthenticationValidityDurationSeconds 则可以设置一个短时窗口,避免高频操作反复弹窗。对于需要无界面后台加密的场景,可以关闭用户认证要求,但应当提高应用自身的运行时防护等级。要注意,Android Keystore 的认证通常依赖锁屏密码、PIN 或生物识别,如果用户清除了锁屏凭据,受用户认证约束的密钥会被系统永久删除,这是平台为防密钥泄露采取的主动失效机制。
实际加密业务数据时,应用从 Keystore 取出密钥引用,创建 Cipher 对象执行加密,再把密文写入私有文件或 EncryptedSharedPreferences。整个过程中密钥不会以明文形式出现在内存变量中。Keystore 也支持非对称签名和密钥协商,常用于客户端证书私钥保护、服务器挑战签名以及端到端加密中的身份验证。相比直接使用硬编码密钥或软件随机数,Keystore 把算法实现和密钥存储下沉到系统层,能显著减少敏感材料暴露面。
四、跨平台场景下的选型与避坑
做跨平台安全设计时,最容易犯的错误是把 Keychain 和 Keystore 当成同一层的替代品。比如 iOS 端把访问令牌直接写进 Keychain 没问题,但 Android 端如果坚持找一个同样能直接存任意字节的系统容器,往往会误用 Keystore API,甚至把令牌当作密钥别名保存,这种做法既不符合接口语义,也享受不到加密保护。正确思路是明确数据分类:需要持久化的秘密数据用加密存储,需要参与密码学运算的密钥用 Keystore。
对于刷新令牌、客户端证书这类秘密数据,Android 推荐的方案是使用 Keystore 保护主密钥,再用主密钥加密业务数据后写入私有存储。你可以使用 Jetpack Security 提供的 EncryptedSharedPreferences 或 EncryptedFile,它们内部已经完成了 Keystore 主密钥管理和数据加解密。iOS 端则可以继续使用 Keychain,但要注意设置合适的 kSecAttrAccessible 等级,并在备份策略、设备迁移和生物识别变化之间做权衡。两端都应当避免把密钥硬编码在 APK 或 IPA 中,因为通过逆向工程可以轻易恢复。
另一个容易忽略的点是系统版本和厂商实现差异。Android 的 StrongBox 支持因设备而异,部分低端设备仍然只有软件实现或 TEE 支持不完整;iOS 的 Keychain 虽然接口统一,但不同芯片和系统版本在生物识别 ACL 失效表现上也有细微差别。因此关键功能上线前,需要在目标设备上进行异常注入测试,比如清除锁屏密码、恢复备份、升级系统、强制重启后检查凭据是否还能按预期读取。只有在这些边界条件下验证通过,才能说你的密钥存储方案是可靠的。
最后,Keychain 和 Keystore 都不是万能的防泄漏方案。它们解决的是静态存储与密钥保护问题,但如果应用本身存在越权读取漏洞、调试日志泄露或 WebView 明文缓存,攻击者仍然可以在授权进程内拿到解密后的数据。安全设计需要把系统能力、最小权限、运行时检测和接口鉴权组合起来,才能形成完整闭环。