Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keychain Query Always Returns errSecItemNotFound After Upgrading to iOS 13

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:

  • Succeeds on Xcode 10 - iOS 12 (and earlier), but
  • Fails with .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.

like image 300
Nicolas Miari Avatar asked Jun 21 '19 09:06

Nicolas Miari


1 Answers

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.

like image 98
Edvinas Avatar answered Oct 19 '22 05:10

Edvinas