我将密码存储到 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)
}
}
}
}
(我不认为我用于任何一次调用的任何字典条目都有不适当的值......但也许我错过了一些刚刚发生的“通过”直到现在)
我已经建立了一个存储库,其中包含一个演示问题的工作项目(Xcode 11 beta)。
密码存储始终成功;密码加载:
- 在 Xcode 10 - iOS 12(及更早版本)上成功,但
.errSecItemNotFound
在 Xcode 11 - iOS 13 上失败。
更新:我无法在设备上重现该问题,只能在模拟器上重现。在设备上,已成功检索存储的密码。这可能是适用于 x86 平台的 iOS 13 模拟器和/或 iOS 13 SDK 的错误或限制。
更新 2:如果有人想出一种替代方法以某种方式解决该问题(无论是通过设计还是利用 Apple 的一些疏忽),我会接受它作为答案。