升级到 iOS 13 后,钥匙串查询总是返回 errSecItemNotFound

Posted

技术标签:

【中文标题】升级到 iOS 13 后,钥匙串查询总是返回 errSecItemNotFound【英文标题】:Keychain Query Always Returns errSecItemNotFound After Upgrading to iOS 13 【发布时间】:2019-11-04 03:24:36 【问题描述】:

我将密码存储到 ios 钥匙串中,然后检索它们以在我的应用程序上实现“记住我”(自动登录)功能。

我围绕 Security.framework 函数(SecItemCopyMatching() 等)实现了我自己的包装器,并且在 iOS 12 之前它的工作原理就像一个魅力。

现在我正在测试我的应用不会与即将推出的 iOS 13 中断,你瞧:

SecItemCopyMatching() 总是返回 .errSecItemNotFound

...即使我之前已经存储了要查询的数据。

我的包装器是一个具有静态属性的类,以便在组装查询字典时方便地提供kSecAttrServicekSecAttrAccount 的值:

class LocalCredentialStore 

    private static let serviceName: String = 
        guard let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String else 
            return "Unknown App"
        
        return name
    ()
    private static let accountName = "Login Password" 

// ...

我正在将密码插入到钥匙串中,代码如下:

/* 
  - NOTE: protectWithPasscode is currently always FALSE, so the password
  can later be retrieved programmatically, i.e. without user interaction. 
 */
static func storePassword(_ password: String, protectWithPasscode: Bool, completion: (() -> Void)? = nil, failure: ((Error) -> Void)? = nil) 
    // Encode payload:
    guard let dataToStore = password.data(using: .utf8) else 
        failure?(NSError(localizedDescription: ""))
        return
    

    // DELETE any previous entry:
    self.deleteStoredPassword()

    // INSERT new value: 
    let protection: CFTypeRef = protectWithPasscode ? kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly : kSecAttrAccessibleWhenUnlocked
    let flags: SecAccessControlCreateFlags = protectWithPasscode ? .userPresence : []

    guard let accessControl = SecAccessControlCreateWithFlags(
        kCFAllocatorDefault,
        protection,
        flags,
        nil) else 
            failure?(NSError(localizedDescription: ""))
            return
    

    let insertQuery: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccessControl: accessControl,
        kSecValueData: dataToStore,
        kSecUseAuthenticationUI: kSecUseAuthenticationUIAllow,
        kSecAttrService: serviceName, // These two values identify the entry;
        kSecAttrAccount: accountName  // together they become the primary key in the Database.
    ]
    let resultCode = SecItemAdd(insertQuery as CFDictionary, nil)

    guard resultCode == errSecSuccess else 
        failure?(NSError(localizedDescription: ""))
        return
    
    completion?()

...后来,我正在检索密码:

static func loadPassword(completion: @escaping ((String?) -> Void)) 

    // [1] Perform search on background thread:
    DispatchQueue.global().async 
        let selectQuery: NSDictionary = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: serviceName,
            kSecAttrAccount: accountName,
            kSecReturnData: true,
            kSecUseOperationPrompt: "Please authenticate"
        ]
        var extractedData: CFTypeRef?
        let result = SecItemCopyMatching(selectQuery, &extractedData)

        // [2] Rendez-vous with the caller on the main thread:
        DispatchQueue.main.async 
            switch result 
            case errSecSuccess:
                guard let data = extractedData as? Data, let password = String(data: data, encoding: .utf8) else 
                    return completion(nil)
                
                completion(password) // < SUCCESS

            case errSecUserCanceled:
                completion(nil)

            case errSecAuthFailed:
                completion(nil)

            case errSecItemNotFound:
                completion(nil)

            default:
                completion(nil)
            
        
    

(我认为我用于任何一次调用的字典中的任何条目都没有不合适的值......但也许我错过了直到现在才“通过”的东西)

我已经设置了a repository 一个工作项目(Xcode 11 beta)来演示这个问题。

密码存储总是成功;密码加载:

在 Xcode 10 - iOS 12(及更早版本)上成功,但 失败,在 Xcode 11 - iOS 13 上使用 .errSecItemNotFound

更新:我无法在设备上重现该问题,只能在模拟器上重现。在设备上,已成功检索存储的密码。 这可能是适用于 x86 平台的 iOS 13 模拟器和/或 iOS 13 SDK 的错误或限制。

更新 2:如果有人想出一种替代方法以某种方式解决该问题(无论是通过设计还是利用 Apple 的某些疏忽),我将接受它作为答案。

【问题讨论】:

似乎在 Beta 5 中已修复 我有 Beta 7,我在模拟器上遇到了同样的问题。也许这是一种回归?一旦我使用访问标志,就找不到该项目。 @AKM 我从来没有机会玩 Beta 5,但我刚刚检查了 GM 并且它仍在发生(仅限模拟器,在它工作正常的设备)。 更新:一些模拟器。例如,iPhone XS 和 iPhone 8 失败,但 iPad Pro (12.9) 成功...... 使用 Xcode 11 GM 和 iPhone XR Simulator 对我来说失败了 :-( 【参考方案1】:

我遇到过类似的问题,我在任何与钥匙串相关的操作中收到errSecItemNotFound,但在模拟器上。在真实设备上它是完美的,我已经在不同的模拟器上使用最新的 Xcode(beta、GM、stable)进行了测试,而让我感到困难的是 iOS 13。

问题是我在查询属性kSecClass 中使用kSecClassKey,但没有“必需”值(查看哪些类与哪些值here)来生成主键:

kSecAttrApplicationLabel kSecAttrApplicationTag kSecAttrKeyType kSecAttrKeySizeInBits kSecAttrEffectiveKeySize

帮助选择kSecClassGenericPasswordkSecClass 提供生成主键的“必需”值:

kSecAttrAccount kSecAttrService

请参阅here,了解更多关于 kSecClass 类型以及它们应具备的其他属性的信息。

我通过启动一个新的 iOS 13 项目并复制我们的应用程序中使用的 Keychain 包装器得出了这个结论,正如预期的那样,它不起作用,所以我找到了这个可爱的使用 keychain 的指南here 并尝试了取出他们毫不奇怪工作的包装器,然后逐行比较我的实现与他们的实现。

此问题已在雷达中报告:http://openradar.appspot.com/7251207

希望这会有所帮助。

【讨论】:

谢谢,我会在周一上班后尽快调查。我必须想出一个聪明的方法来迁移到你建议的这个正确的实现,而我的用户在更新我的应用程序后不会丢失他们存储的密码。或许可以连续尝试这两种方法,直到成功找回密码... 是的,之后需要进行非常彻底的测试,尤其是从一个版本更新到另一个版本。我在传递给SecItemAdd 等的查询属性字典中更改的另一件事是kSecAccessControl 属性,在它获得SecAccessControlCreateWithFlags 的对象之前,但现在我只使用kSecAttrAccessible,因为前面提到的对象也是如此。所以这也可能有所帮助。祝你好运! 好吧,我会被诅咒的......包装器确实有效。仍然没有弄清楚与我的原始代码到底有什么不同;将继续调查。不确定您的答案是否算作解决方案,但您肯定为我指明了正确的方向。谢谢。 我认为这里真正的问题是在使用 SecAccessControlCreateFlags 进行身份验证策略(例如 userPresence)时 iOS 13 模拟器中的某种错误。我还没有看到任何解决方法,我认为不可能修复它。恕我直言,Apple 需要修复它。 @emirc 但是这些属性并不意味着同样的事情,因此使用一个而不是另一个会导致不同的行为(这可能是不希望的,具体取决于用例)。例如,如果想通过应用程序密码保护存储的密钥,kSecAttrAccessible 将无济于事。【参考方案2】:

经过半天的实验,我发现使用一个非常基本的 kSecClassGenericPassword 实例,我在模拟器和真实硬件上都遇到了问题。在阅读完文档后,我注意到 kSecAttrSynchronizable 有一个 kSecAttrSynchronizableAny。要接受任何其他属性的任何值,您只需不将其包含在查询中。这是一个线索。

我发现当我将 kSecAttrSynchronizable 设置为 kSecAttrSynchronizableAny 时,所有查询都有效。当然,如果我确实想要过滤该值,我也可以将其设置为 kCFBooleanTrue(或 *False)。

鉴于该属性,一切似乎对我来说都按预期工作。希望这可以为其他一些人节省半天的时间来处理测试代码。

【讨论】:

【参考方案3】:

更新

由于上述增强的安全要求,我将访问属性从kSecAttrAccessibleWhenUnlocked 更改为kSecAttrAccessibleWhenUnlockedThisDeviceOnly(即防止在设备备份期间复制密码)。

...现在我的代码又被破坏了! 这不是尝试使用包含kSecAttrAccessibleWhenUnlockedThisDeviceOnly 的字典读取属性设置为kSecAttrAccessibleWhenUnlocked 的密码的问题相反,不;我删除了该应用程序并从头开始,它仍然失败。

我已经发布了a new question(带有返回此邮件的链接)。


原答案:

感谢@Edvinas 在his answer above 中的建议,我能够找出问题所在。

按照他的建议,我下载了 this Github repository(项目 28)中使用的 Keychain 包装类,并将我的代码替换为对主类的调用,你瞧 - 它确实有效

接下来,我添加了控制台日志来比较 Keychain 包装器中用于存储/检索密码的 查询字典(即 SecItemAdd() 和 @ 的参数) 987654330@) 反对我正在使用的那些。有几个不同之处:

    包装器使用 Swift Dictionary ([String, Any]),而我的代码使用 NSDictionary(我必须更新它。已经是 2019 年了!)。 包装器使用 bundle identifier 作为kSecAttrService 的值,我使用的是CFBundleName。这应该不是问题,但我的捆绑包名称包含日文字符... 包装器使用 CFBoolean 值作为 kSecReturnData,我使用的是 Swift 布尔值。 包装器使用kSecAttrGeneric除了kSecAttrAccountkSecAttrService,我的代码只使用后两者。 包装器将kSecAttrGenerickSecAttrAccount 的值编码为Data,我的代码将这些值直接存储为String。 我的插入字典使用 kSecAttrAccessControlkSecUseAuthenticationUI,包装器没有(它使用 kSecAttrAccessible 和可配置的值。就我而言,我相信 kSecAttrAccessibleWhenUnlocked 适用)。 我的检索字典使用kSecUseOperationPrompt,包装器没有 包装器将 kSecMatchLimit 指定为值 kSecMatchLimitOne,我的代码没有。

(第 6 点和第 7 点并不是真正必要的,因为虽然我最初设计课程时考虑到了生物特征认证,但我目前没有使用它。

...等等。

我将我的字典与包装器的字典相匹配,最终使复制查询成功。然后,我删除了不同的项目,直到我可以查明原因。事实证明:

    我不需要kSecAttrGeneric(只需kSecAttrServicekSecAttrAccount,正如@Edvinas 的回答中所述)。 我不需要对 kSecAttrAccount 的值进行数据编码(这可能是个好主意,但在我的情况下,它会破坏以前存储的数据并使迁移复杂化)。 原来kSecMatchLimit 也不需要(可能是因为我的代码导致存储/匹配的唯一值?),但我想我会添加它只是为了安全(不觉得它会破坏向后兼容)。 Swift 布尔值,例如kSecReturnData 工作正常。分配 integer 1 会破坏它(尽管这是在控制台上记录值的方式)。 (日语)包名称作为kSecService 的值也可以。

...等等。

所以最后,我:

    从插入字典中删除了kSecUseAuthenticationUI,并将其替换为kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked。 从插入字典中删除了kSecUseAuthenticationUI。 从复制字典中删除了kSecUseOperationPrompt

...现在我的代码可以工作了。我将不得不测试这是否会在实际设备上加载使用旧代码存储的密码(否则,我的用户将在下次更新时丢失他们保存的密码)。

这是我最后的工作代码:

import Foundation
import Security

/**
 Provides keychain-based support for secure, local storage and retrieval of the
 user's password.
 */
class LocalCredentialStore 

    private static let serviceName: String = 
        guard let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String else 
            return "Unknown App"
        
        return name
    ()

    private static let accountName = "Login Password"

    /**
     Returns `true` if successfully deleted, or no password was stored to begin
     with; In case of anomalous result `false` is returned.
     */
    @discardableResult  static func deleteStoredPassword() -> Bool 
        let deleteQuery: NSDictionary = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,

            kSecAttrService: serviceName,
            kSecAttrAccount: accountName,

            kSecReturnData: false
        ]
        let result = SecItemDelete(deleteQuery as CFDictionary)
        switch result 
        case errSecSuccess, errSecItemNotFound:
            return true

        default:
            return false
        
    

    /**
     If a password is already stored, it is silently overwritten.
     */
    static func storePassword(_ password: String, protectWithPasscode: Bool, completion: (() -> Void)? = nil, failure: ((Error) -> Void)? = nil) 
        // Encode payload:
        guard let dataToStore = password.data(using: .utf8) else 
            failure?(NSError(localizedDescription: ""))
            return
        

        // DELETE any previous entry:
        self.deleteStoredPassword()

        // INSERT new value:
        let insertQuery: NSDictionary = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,

            kSecValueData: dataToStore,

            kSecAttrService: serviceName, // These two values identify the entry;
            kSecAttrAccount: accountName  // together they become the primary key in the Database.
        ]
        let resultCode = SecItemAdd(insertQuery as CFDictionary, nil)

        guard resultCode == errSecSuccess else 
            failure?(NSError(localizedDescription: ""))
            return
        
        completion?()
    

    /**
     If a password is stored and can be retrieved successfully, it is passed back as the argument of
     `completion`; otherwise, `nil` is passed.

     Completion handler is always executed on themain thread.
     */
    static func loadPassword(completion: @escaping ((String?) -> Void)) 

        // [1] Perform search on background thread:
        DispatchQueue.global().async 
            let selectQuery: NSDictionary = [

                kSecClass: kSecClassGenericPassword,
                kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,

                kSecAttrService: serviceName,
                kSecAttrAccount: accountName,

                kSecMatchLimit: kSecMatchLimitOne,

                kSecReturnData: true
            ]
            var extractedData: CFTypeRef?
            let result = SecItemCopyMatching(selectQuery, &extractedData)

            // [2] Rendez-vous with the caller on the main thread:
            DispatchQueue.main.async 
                switch result 
                case errSecSuccess:
                    guard let data = extractedData as? Data, let password = String(data: data, encoding: .utf8) else 
                        return completion(nil)
                    
                    completion(password)

                case errSecUserCanceled:
                    completion(nil)

                case errSecAuthFailed:
                    completion(nil)

                case errSecItemNotFound:
                    completion(nil)

                default:
                    completion(nil)
                
            
        
    


Final Words Of Wisdom:除非你有充分的理由这样做,否则只需抓住@Edvinas 在他的回答中提到的钥匙串包装器(this repository,项目 28 )) 继续前进!

【讨论】:

我认为你的代码主要是因为你不再使用SecAccessControlCreateFlags,即SecAccessControlCreateFlags.userPresence。如果您要再次使用该标志,那么您发布的这个新版本的代码将不起作用。 iOS 13 模拟器中存在某种错误或其他问题。【参考方案4】:

关于kSecClassGenericPassword 中的问题,我试图了解问题所在,并找到了解决方案。

基本上,Apple 似乎正在解决 kSecAttrAccessControl 的问题,因此在 iOS 版本 13 以下,您添加带有 kSecAttrAccessControl 的 keyChain 对象,没有生物识别身份,而在 iOS 13 以上,则在模拟器中不再工作。

因此,解决方案是,当您想使用生物特征加密 keyChain 对象时,您需要将 kSecAttrAccessControl 添加到您的查询中,但如果您不需要通过生物特征加密,则只需添加 kSecAttrAccessible 这就是做这些的正确方法。

示例

生物特征加密查询:

guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                          kSecAttrAccessibleWhenUnlocked,
                                                          userPresence,
                                                          nil) else 
                                                              // failed to create accessControl
                                                              return 
                                                          


var attributes: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                           kSecAttrService: "Your service",
                                           kSecAttrAccount: "Your account",
                                           kSecValueData: "data",
                                           kSecAttrAccessControl: accessControl]

查询常规 KeyChain(无生物特征):

var attributes: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                               kSecAttrService: "Your service",
                                               kSecAttrAccount: "Your account",
                                               kSecValueData: "data",
                                               kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]

【讨论】:

感谢您的回答。我一到办公桌就去看看。【参考方案5】:

我们在生成密钥对时遇到了同样的问题 - 在设备上运行良好,但在 iOS 13 及更高版本的模拟器上,当我们稍后尝试检索它时它无法找到密钥。

解决方案在 Apple 文档中:https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain

当您自己生成密钥时,如生成新密钥中所述 加密密钥,您可以将它们作为隐式存储在钥匙串中 该过程的一部分。如果您通过其他方式获得密钥,您可以 仍然将其存储在钥匙串中。

简而言之,使用SecKeyCreateRandomKey创建密钥后,需要使用SecItemAdd将此密钥保存在Keychain中:

var error: Unmanaged<CFError>?
guard let key = SecKeyCreateRandomKey(createKeyQuery as CFDictionary, &error) else 
    // An error occured.
    return


let saveKeyQuery: [String: Any] = [
    kSecClass as String: kSecClassKey,
    kSecAttrApplicationTag as String: tag,
    kSecValueRef as String: key
]

let status = SecItemAdd(saveKeyQuery as CFDictionary, nil)
guard status == errSecSuccess else 
    // An error occured.
    return


// Success!

【讨论】:

如果您使用 SecKeyCreateRandomKey 您确实必须手动保存密钥。但是,例如使用 SecKeyGeneratePair 和 kSecAttrIsPermanent 的 Query 参数会自动将密钥保存在钥匙串中。不幸的是,在 iOS 13 Simulator 之后访问密钥现在被破坏了。所以绝对是个bug。您是否 100% 测试过您的上述代码确实有效? 所以我尝试手动添加密钥,虽然它有效,但仍然无法在 iOS 13 模拟器上检索密钥(至少使用我之前工作的查询代码)。似乎是 100% 与 SecItemCopyMatching 相关的错误 我们正在使用kSecAttrIsPermanent 以及手动保存密钥的代码。这似乎在我们的 iOS 13 模拟器上运行良好。

以上是关于升级到 iOS 13 后,钥匙串查询总是返回 errSecItemNotFound的主要内容,如果未能解决你的问题,请参考以下文章

OSX 10.9 Mavericks 钥匙串 API 坏了?

访问钥匙串中的安全项目有时会在 iOS 中返回错误 -25308 (errSecInteractionNotAllowed)

使用 Swift 查询 iOS 钥匙串

即使在钥匙串中也找不到 iOS 应用商店分发证书

使用 Swift 向 iOS 钥匙串添加项目和查询

iOS 钥匙串安全性