I am storing passwords into the iOS keychain and later retrieving them to implement a "remember me" (auto-login) feature on my app.
I implemented my own wrapper around the Security.framework
functions (SecItemCopyMatching()
, etc.), and it was working like a charm up until iOS 12.
Now I am testing that my app doesn't break with the upcoming iOS 13, and lo and behold:
SecItemCopyMatching()
always returns .errSecItemNotFound
...even though I have previously stored the data I am querying.
My wrapper is a class with static properties to conveniently provide the values of the kSecAttrService
and kSecAttrAccount
when assembling the query dictionaries:
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" // ...
I am inserting the password into the keychain with code like the following:
/* - 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?() }
...and later, I am retrieving the password with:
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) } } } }
(I don't think any of the entries of the dictionaries I use for either call has an inappropriate value... but perhaps I am missing something that just happened to "get a pass" until now)
I have set up a repository with a working project (Xcode 11 beta) that demonstrates the problem.
The password storing always succeeds; The password loading:
.errSecItemNotFound
on Xcode 11 - iOS 13.UPDATE: I can not reproduce the issue on the device, only Simulator. On the device, the stored password is retrieved successfully. Perhaps this is a bug or limitation on the iOS 13 Simulator and/or iOS 13 SDK for the x86 platform.
UPDATE 2: If someone comes up with an alternative approach that somehow works around the issue (whether by design or by taking advantage of some oversight by Apple), I will accept it as an answer.
I've had a similar issue where I was getting errSecItemNotFound
with any Keychain-related action but only on a simulator. On real device it was perfect, I've tested with latest Xcodes (beta, GM, stable) on different simulators and the ones that were giving me a hard time were iOS 13 ones.
The problem was that I was using kSecClassKey
in query attribute kSecClass
, but without the 'required' values (see what classes go with which values here) for generating a primary key:
kSecAttrApplicationLabel
kSecAttrApplicationTag
kSecAttrKeyType
kSecAttrKeySizeInBits
kSecAttrEffectiveKeySize
And what helped was to pick kSecClassGenericPassword
for kSecClass
and provide the 'required' values for generating a primary key:
kSecAttrAccount
kSecAttrService
See here on more about kSecClass types and what other attributes should go with them.
I came to this conclusion by starting a new iOS 13 project and copying over the Keychain wrapper that was used in our app, as expected that did not work so I've found this lovely guide on using keychain here and tried out their wrapper which no surprise worked, and then went line by line comparing my implementation with theirs.
This issue already reported in radar: http://openradar.appspot.com/7251207
Hope this helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With