You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

whenPasscodeSetThisDeviceOnly模式下Keychain值二次被擦除求助

Keychain值在重设密码后锁屏10秒被擦除的问题分析与解决

我来帮你拆解这个Keychain的异常问题——这个现象其实和iOS底层的加密机制初始化流程有关,先从背景和问题细节说起:

背景代码

我们使用KeychainAccess库实现Keychain存储,代码如下:

private let keychain = Keychain(service: "som.service.name")
func getProperty(_ prop: String) -> String? {
    return try? keychain.get(prop)
}
func setProperty(_ prop: String, value: String) {
    try? keychain.accessibility(.whenPasscodeSetThisDeviceOnly).set(value, key: prop)
}

问题现象

使用.whenPasscodeSetThisDeviceOnly属性时,预期逻辑是:

  • 移除设备密码后,Keychain值自动擦除(这部分符合预期)
  • 重设新密码后,能正常存储、检索新值

但实际出现了奇怪的异常:重设密码并存储新值后,首次锁屏停留至少10秒再解锁,刚存的Keychain值会被再次擦除;而之后的锁屏操作就不会再影响Keychain的持久性,一切恢复正常。

复现步骤

  • 设置设备密码,正常存储/检索Keychain值(无问题)
  • 完全移除设备密码(注意:修改密码不会触发这个问题,必须是彻底删除密码)
  • 设置新设备密码,不锁屏直接打开应用
  • 尝试检索旧值→值已被擦除(符合预期)
  • 存储新值并即时检索(正常工作)
  • 锁定设备并等待至少10秒
  • 解锁后检索新值→值被擦除(异常点)
  • 首次10秒锁屏后,后续锁屏操作不再影响Keychain持久性

原因拆解

这个问题本质是iOS Keychain的加密上下文初始化延迟导致的:

  1. 移除密码时,iOS会自动清理所有标记为.whenPasscodeSetThisDeviceOnly的Keychain项——这是Apple设计的安全机制,符合预期。
  2. 重设密码后,iOS会在后台启动设备加密环境的重新初始化流程,这个过程不是瞬间完成的,通常需要几秒到十几秒的时间。
  3. 在初始化完成前,你存储的新Keychain项虽然能被即时访问,但并没有真正绑定到新的密码加密上下文。当设备锁屏并停留足够时间后,iOS会触发Keychain的一致性校验,发现这些“未正确绑定”的项,就会将其删除。
  4. 一旦加密环境初始化完成,后续存储的Keychain项就会正确关联到新密码,锁屏操作就不会再触发删除逻辑。

解决思路与可行方案

方案1:等待加密环境就绪后再存储

利用iOS的系统通知,等设备的加密环境完全初始化完成后再执行存储操作:

import Security
import UIKit

// 监听加密环境就绪通知
NotificationCenter.default.addObserver(forName: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil, queue: .main) { [weak self] _ in
    // 当protectedData可用时,说明加密上下文已就绪
    self?.safeStoreProperty("yourKey", value: "yourValue")
}

func safeStoreProperty(_ prop: String, value: String) {
    // 先确认设备已设置密码
    var isPasscodeSet = false
    let status = SecItemCopyMatching([
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: "som.service.name",
        kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
        kSecReturnAttributes: kCFBooleanTrue!
    ] as CFDictionary, nil)
    if status != errSecItemNotFound && status != errSecAuthFailed {
        isPasscodeSet = true
    }
    
    if isPasscodeSet {
        try? keychain.accessibility(.whenPasscodeSetThisDeviceOnly).set(value, key: prop)
    }
}

UIApplication.protectedDataDidBecomeAvailableNotification会在设备加密环境完全就绪、受保护数据可访问时触发,此时存储的Keychain项会正确绑定到新密码,避免后续被删除。

方案2:调整Keychain访问属性(业务允许的话)

如果你的业务场景对安全性要求可以稍微放宽,可以考虑用.afterFirstUnlockThisDeviceOnly替代.whenPasscodeSetThisDeviceOnly。这个属性的Keychain项不会在移除密码时被删除,同时也能避开加密初始化延迟的问题,但要注意:没有密码时,设备被物理获取的话,这些项存在被访问的风险。

方案3:添加检索重试机制

结合业务逻辑,在检索Keychain值失败时自动重新存储(需要注意避免无限重试):

func getPropertyWithRetry(_ prop: String, fallbackValue: String? = nil) -> String? {
    // 先尝试正常检索
    if let value = try? keychain.get(prop) {
        return value
    }
    // 检索失败时,尝试存储 fallback 值(根据你的业务需求调整)
    guard let fallback = fallbackValue else { return nil }
    try? keychain.accessibility(.whenPasscodeSetThisDeviceOnly).set(fallback, key: prop)
    return fallback
}

内容的提问来源于stack exchange,提问作者Przemysław Jaskot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 20:22:45