我将密码存储到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)
}
}
}
}
(我不认为我用来打任何电话的词典中的任何条目都具有不适当的价值……但也许直到现在我仍遗漏了一些只是为了“获得通过”的东西)< / em>
我已经建立了一个a repository并正在运行的项目(Xcode 11 beta)证明了这个问题。
密码存储总是成功;密码加载:
.errSecItemNotFound
失败。更新:我无法在设备上重现该问题,只能在Simulator上重现。在设备上,成功检索到存储的密码。 也许这是针对x86平台的iOS 13 Simulator和/或iOS 13 SDK上的错误或限制。
答案 0 :(得分:4)
我遇到了类似的问题,我在仿真器上的任何与钥匙串相关的操作都得到了errSecItemNotFound
,但仅。在真实的设备上,它是完美的,我已经在不同的模拟器上测试了最新的Xcode(beta,GM,稳定版),而给我带来麻烦的是iOS 13。
问题是我在查询属性kSecClassKey
中使用kSecClass
,但是没有生成主键的'required'值(请参阅哪些类与哪个值here一起使用) :
kSecAttrApplicationLabel
kSecAttrApplicationTag
kSecAttrKeyType
kSecAttrKeySizeInBits
kSecAttrEffectiveKeySize
有用的是为kSecClassGenericPassword
选择了kSecClass
,并且提供了用于生成主键的“必需”值:
kSecAttrAccount
kSecAttrService
通过得出一个结论,我开始了一个新的iOS 13项目,并复制了应用程序中使用的钥匙串包装程序,但效果并不理想,所以我找到了这个关于使用钥匙串http://openradar.appspot.com/7251207的可爱指南并尝试了毫不奇怪的包装器,然后逐行比较了我的实现和他们的实现。
此问题已在雷达中报告:{{3}}
希望这会有所帮助。
答案 1 :(得分:2)
由于上面提出的更高的安全性要求,我将访问属性从kSecAttrAccessibleWhenUnlocked
更改为kSecAttrAccessibleWhenUnlockedThisDeviceOnly
(即,防止在设备备份期间复制密码)。
...现在我的代码又被破坏了!。这不是尝试使用包含{的字典来读取存储在属性设置为kSecAttrAccessibleWhenUnlocked
的密码的问题。 {1}},否;我删除了该应用程序并从头开始,但仍然失败。
我已经发布了a new question(带有指向该链接的链接)。
由于@Edvinas在his answer above中的建议,我得以找出问题所在。
按照他的建议,我下载了this Github repository(项目28)中使用的Keychain包装器类,并用对主类的调用替换了我的代码,瞧瞧-它确实起作用了。
下一步,我添加了控制台日志,以比较在钥匙串包装器中用于存储/检索密码(即kSecAttrAccessibleWhenUnlockedThisDeviceOnly
的参数)的查询词典和SecItemAdd()
)反对我正在使用的内容。有几个区别:
SecItemCopyMatching
),而我的代码使用[String, Any]
(我必须对此进行更新。已经是2019年了!)。NSDictionary
的值,而我使用的是kSecAttrService
。这应该不成问题,但是我的捆绑包名称包含日语字符... CFBundleName
的值CFBoolean
用于kSecReturnData
,而我使用的是Swift布尔值。kSecAttrGeneric
和kSecAttrAccount
之外,包装器还使用kSecAttrService
,我的代码仅使用后两个。kSecAttrGeneric
和kSecAttrAccount
的值编码为Data
,我的代码将这些值直接存储为String
。kSecAttrAccessControl
和kSecUseAuthenticationUI
,而包装器则不使用(它使用具有可配置值的kSecAttrAccessible
。对于我来说,我相信kSecAttrAccessibleWhenUnlocked
适用)。 kSecUseOperationPrompt
,但包装器未使用kSecMatchLimit
指定为值kSecMatchLimitOne
,但我的代码未指定。(第6点和第7点并不是真正必要的,因为尽管我最初是在设计课程时考虑了生物特征认证的,但我目前并未在使用它。)
...等等。
我将字典与包装器的字典相匹配,最终使复制查询成功。然后,我删除了不同的项目,直到找到原因为止。事实证明:
kSecAttrGeneric
(只需@ {Edvinas的答案中提到的kSecAttrService
和kSecAttrAccount
)。kSecAttrAccount
的值进行数据编码(这可能是个好主意,但就我而言,这会破坏以前存储的数据并使迁移复杂化)。kSecMatchLimit
(也许因为我的代码导致存储/匹配的唯一值?),但是我想我会添加它只是为了安全(不是这样)会破坏向后兼容性。kSecReturnData
工作正常。分配整数 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)
}
}
}
}
}
最终的智慧话语::除非您有很强的理由不这么做,否则请抓住@Edvinas在他的回答中提到的钥匙扣包装纸(this repository,项目28)),继续前进!
答案 2 :(得分:2)
经过半天的实验,我发现使用kSecClassGenericPassword的一个非常基本的实例,我在模拟器和实际硬件上都遇到了问题。阅读完文档后,我注意到kSecAttrSynchronizable具有kSecAttrSynchronizableAny。要接受任何其他属性的任何值,只需将其不包括在查询中即可。这是一个线索。
我发现,当我将kSecAttrSynchronizable设置为kSecAttrSynchronizableAny时,所有查询均有效。当然,如果我确实想过滤该值,也可以将其设置为kCFBooleanTrue(或* False)。
鉴于此属性,一切似乎都按我的预期工作。希望这将使其他人节省半天的测试代码。
答案 3 :(得分:1)
关于kSecClassGenericPassword
中的问题,我试图了解问题所在,并且找到了解决方案。
基本上看来,苹果公司正在解决kSecAttrAccessControl
的问题,因此,在iOS版本13以下,您添加了带有kSecAttrAccessControl
且没有生物识别信息的keyChain对象,而在iOS 13以上则在模拟器中不再起作用。
因此,解决方案是当您要使用生物特征加密keyChain对象时,需要向查询中添加kSecAttrAccessControl
,但是如果您不需要通过生物特征加密,则只需添加{{1 }}是执行这些操作的正确方法。
查询生物特征加密:
kSecAttrAccessible
查询常规KeyChain(不使用生物特征识别):
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]
答案 4 :(得分:0)
生成密钥对时,我们也遇到了同样的问题-在设备上工作正常,但是在iOS 13及更高版本的模拟器上,当我们稍后尝试取回密钥时,找不到密钥。
该解决方案在Apple文档中:https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain
如生成新内容中所述,当自己生成密钥时 加密密钥,您可以将它们作为隐式密钥存储在密钥链中 该过程的一部分。如果您通过其他方式获得密钥,则可以 仍将其存储在钥匙串中。
简而言之,使用SecKeyCreateRandomKey
创建密钥后,需要使用SecItemAdd
将密钥保存在钥匙串中:
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!