升级到 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
...即使我之前已经存储了要查询的数据。
我的包装器是一个具有静态属性的类,以便在组装查询字典时方便地提供kSecAttrService
和kSecAttrAccount
的值:
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
帮助选择kSecClassGenericPassword
为kSecClass
并提供生成主键的“必需”值:
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
除了kSecAttrAccount
和kSecAttrService
,我的代码只使用后两者。
包装器将kSecAttrGeneric
和kSecAttrAccount
的值编码为Data
,我的代码将这些值直接存储为String
。
我的插入字典使用 kSecAttrAccessControl
和 kSecUseAuthenticationUI
,包装器没有(它使用 kSecAttrAccessible
和可配置的值。就我而言,我相信 kSecAttrAccessibleWhenUnlocked
适用)。
我的检索字典使用kSecUseOperationPrompt
,包装器没有
包装器将 kSecMatchLimit
指定为值 kSecMatchLimitOne
,我的代码没有。
(第 6 点和第 7 点并不是真正必要的,因为虽然我最初设计课程时考虑到了生物特征认证,但我目前没有使用它。)
...等等。
我将我的字典与包装器的字典相匹配,最终使复制查询成功。然后,我删除了不同的项目,直到我可以查明原因。事实证明:
-
我不需要
kSecAttrGeneric
(只需kSecAttrService
和kSecAttrAccount
,正如@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)