Keychain Query Always Returns errSecItemNotFound After Upgrading to iOS 13
Asked Answered
Y

5

35

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.

Yolande answered 21/6, 2019 at 9:33 Comment(14)
Seems to be fixed in Beta 5Galbreath
@Galbreath Thanks for the comment... Haven't had time to play with the latest betasYolande
I have Beta 7 and I have the same issue on simulator. Maybe it's a regression? Once I use access flags, the item is not found.Compliance
@Galbreath I never had a chance to play with Beta 5, but I just checked the GM and it's still happening (Simulator only, on the device it works fine).Yolande
Update: Some simulators. For example, iPhone XS and iPhone 8 fail, but iPad Pro (12.9) succeeds...Yolande
Fails for me with Xcode 11 GM and iPhone XR Simulator :-(Boart
@NicolasMiari Did you find solution?Hosier
@Hosier No, I don’t think there’s anything I can do on my side. I’m just glad it doesn’t happen on the device so I guess my users will be fine...Yolande
We're extensively using keychain in our app and experienced the same issue. Xcode 11 (released version), all iOS 13.0 simulators fail, real device works ok.Hourglass
I am seeing this issue on real device with iOS 13.1 in it. But, it does not occur always. Is someone else facing the same issue and is there any update on this from Apple side?Upstart
@Upstart Does neither of the two answers help you fix it?Yolande
@NicolasMiari- It is randomly occurring issue in the device. I will let you once i get that. iOS 13.1.3 got released and is there any luck with this issue?Upstart
@NicolasMiari- This solution does not work out. I am doing the same as the keychain wrapper you have mentioned for both saving and retrieving dataUpstart
Some time ago I created a bug report (radar) about this problem: openradar.appspot.com/7251207 Please, dupe it, so that Apple fixes it. (FB7422066 and FB7251207)Boart
C
6

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.

Contentment answered 4/10, 2019 at 9:25 Comment(15)
Thank you, I’ll look into this as soon as I get to work on Monday. I’ll have to come up with a clever way to migrate to this correct implementation you suggest without my users losing their stored passwords once they update my app. Perhaps try both approaches in succession until the password is successfully retrieved...Yolande
(I don’t want to accept the answer until I’ve confirmed in actual code that it solves the issue, but I’m very confident it will)Yolande
Yes, very thorough testing will be needed afterwards, especially update from one version to another. Another thing that I've changed in the query attribute dictionary that you pass to SecItemAdd and etc. was the kSecAccessControl attribute, before it would get an object of SecAccessControlCreateWithFlags but now I'm only using kSecAttrAccessible since the beforehand mentioned object did the same. So that might help as well. Good luck!Contentment
I've looked into it, but I don't understand: I'm already using kSecClassGenericPassword and specifying both kSecAttrService and kSecAttrAccount; according to your answer, my code should have been working all along...?Yolande
Well that's unfortunate, another thing that you could is to see if you're using anything from the comment I've made above regarding kSecAccessControl attribute. Or you could go the boring route and find a working implementation of what you need online (keychain wrapper) and try comparing it to your implemetation.Contentment
I'm going through the source code for the Keychain Wrapper in your link. I'll report any progress on ym question...Yolande
Well I'll be damned... the wrapper does work. Still haven't figured out what exactly is different from my original code; will keep investigating. Not sure your answer counts as a solution, but you definitely pointed me in the right direction. Thanks.Yolande
I think the real issue here is some kind of bug in iOS 13 simulator when using SecAccessControlCreateFlags for authentication policy (such as userPresence). I have not seen any work around for it yet and I don't think it's possible to fix it. Apple needs to fix it on their side IMHO.Matronize
@Matronize is correct. It's an issue with using SecAccessControlCreateFlags in iOS 13 simulators. The fix that worked for me is what Edvinas said, to use kSecAttrAccessible instead of kSecAttrAccessControl and access control object produced by the erroneous SecAccessControlCreateFlags function.Bricebriceno
@Bricebriceno these attributes however don't mean the same thing, so using one over the other leads to a different behaviour (which may not be desired, depending on the usecase). If for example one wants to protect the stored key by application password, kSecAttrAccessible will not help with that.Matronize
Thank you. In my case I use “...WhenUnlocked” option and Security enclave. Hope in this case it’s OK.Bricebriceno
Confirmed. As @Matronize says, this is an issue with using SecAccessControlCreateFlags in iOS 13 simulators. If the key is created without the attribute kSecAttrAccessControl, then all works as previously.Rippy
kSecAttrAccessControl changes absolutely nothing in my case, what I've found out is that if you run / build the app on one of the first released versions of Xcode 11 ( 11.0 (11A420a) AND you run the app on iOS 10.3.4 or iOS 10.3.4. Any later version of Xcode seems to have fixed this issue.Contentment
Direct link to the keychainWrapper code.Tijerina
I am having the same issue, but I still could not make it work. I dont know what account and service parameter should be when using kSecClassGenericPassword so I used kSecClassKey but does not it work on iphone 13? Do I have to use kSecClassGenericPassword?Exogenous
A
5

After half a day of experimentation I discovered that using a pretty basic instance of kSecClassGenericPassword I had the problem on both the simulator and real hardware. After having a read over of the docs I noticed that kSecAttrSynchronizable has a kSecAttrSynchronizableAny. To accept any value for any other attribute, you simply don't include it in the query. That's a clue.

I found that when I included kSecAttrSynchronizable set to kSecAttrSynchronizableAny the queries all worked. Of course I could also set it to either kCFBooleanTrue (or *False) if I actually do want to filter on that value.

Given that attribute everything seems to work as expected for me. Hopefully this will save some other people a half day of mucking around with test code.

Adrienadriena answered 4/9, 2020 at 21:38 Comment(0)
B
3

Regarding the issue in kSecClassGenericPassword, I was trying to understand what is the problem and I found a solution for that.

Basically it seems like Apple was fixing an issue with kSecAttrAccessControl, so below iOS version 13 you add keyChain object with kSecAttrAccessControl without biometric identity and above iOS 13 that does not work anymore in a simulator.

So the solution for that is when you want to encrypt the keyChain object with biometric you need to add kSecAttrAccessControl to your query but if you don't need to encrypted by biometric you need to add only kSecAttrAccessible that's the right way to do these.

Examples

Query for biometric encrypt:

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]

Query for regular KeyChain (without biometric):

var attributes: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                               kSecAttrService: "Your service",
                                               kSecAttrAccount: "Your account",
                                               kSecValueData: "data",
                                               kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]
Butacaine answered 17/2, 2020 at 22:38 Comment(1)
Thanks for the answer. I’ll check it out once I get to my desk.Yolande
Y
2

Update

Due to enhanced security requirements from above, I changed the access attribute from kSecAttrAccessibleWhenUnlocked to kSecAttrAccessibleWhenUnlockedThisDeviceOnly (i.e., prevent the password from being copied during device backups).

...And now my code is broken again! This isn't an issue of trying to read the password stored with the attribute set to kSecAttrAccessibleWhenUnlocked using a dictionary that contains kSecAttrAccessibleWhenUnlockedThisDeviceOnly instead, no; I deleted the app and started from scratch, and it still fails.

I have posted a new question (with a link back to this one).


Original Answer:

Thanks to the suggestion by @Edvinas in his answer above, I was able to figure out what was wrong.

As he suggests, I downloaded the Keychain wrapper class used in this Github repository (Project 28), and replaced my code with calls to the main class, and lo and behold - it did work.

Next, I added console logs to compare the query dictionaries used in the Keychain wrapper for storing/retrieving the password (i.e., the arguments to SecItemAdd() and SecItemCopyMatching) against the ones I was using. There were several differences:

  1. The wrapper uses Swift Dictionary ([String, Any]), and my code uses NSDictionary (I must update this. It's 2019 already!).
  2. The wrapper uses the bundle identifier for the value of kSecAttrService, I was using CFBundleName. This shouldn't be an issue, but my bundle name contains Japanese characters...
  3. The wrapper uses CFBoolean values for kSecReturnData, I was using Swift booleans.
  4. The wrapper uses kSecAttrGeneric in addition to kSecAttrAccount and kSecAttrService, my code only uses the latter two.
  5. The wrapper encodes the values of kSecAttrGeneric and kSecAttrAccount as Data, my code was storing the values directly as String.
  6. My insert dictionary uses kSecAttrAccessControl and kSecUseAuthenticationUI, the wrapper doesn't (it uses kSecAttrAccessible with configurable values. In my case, I believe kSecAttrAccessibleWhenUnlocked applies).
  7. My retrieve dictionary uses kSecUseOperationPrompt, the wrapper doesn't
  8. The wrapper specifies kSecMatchLimit to the value kSecMatchLimitOne, my code doesn't.

(Points 6 and 7 are not really necessary, because although I first designed my class with biometric authentication in mind, I am not using it currently.)

...etc.

I matched my dictionaries to those of the wrapper and finally got the copy query to succeed. Then, I removed the differing items until I could pinpoint the cause. It turns out that:

  1. I don't need kSecAttrGeneric (just kSecAttrService and kSecAttrAccount, as mentioned in @Edvinas's answer).
  2. I don't need to data-encode the value of kSecAttrAccount (it may be a good idea, but in my case, it would break previously stored data and complicate migration).
  3. It turns out kSecMatchLimit isn't needed either (perhaps because my code results in a unique value stored/matched?), but I guess I will add it just to be safe (doesn't feel like it would break backward compatibility).
  4. Swift booleans for e.g. kSecReturnData work fine. Assigning the integer 1 breaks it though (although that's how the value is logged on the console).
  5. The (Japanese) bundle name as a value for kSecService is ok too.

...etc.

So in the end, I:

  1. Removed kSecUseAuthenticationUI from the insert dictionary and replaced it with kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked.
  2. Removed kSecUseAuthenticationUI from the insert dictionary.
  3. Removed kSecUseOperationPrompt from the copy dictionary.

...and now my code works. I will have to test whether this load passwords stored using the old code on actual devices (otherwise, my users will lose their saved passwords on the next update).

So this is my final, working code:

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: Unless you have a strong reason not to, just grab the Keychain Wrapper that @Edvinas mentioned in his answer (this repository, project 28)) and move on!

Yolande answered 8/10, 2019 at 8:40 Comment(1)
I think your code mostly works because you're no longer using SecAccessControlCreateFlags, namely SecAccessControlCreateFlags.userPresence. If you were to use that flag again, event this new version of the code you posted would not work. There's some kind of bug or something in the iOS 13 simulator.Matronize
L
0

We had the same issue when generating a key pair - works just fine on devices, but on simulator iOS 13 and above it cannot find the key when we try to retreive it later on.

The solution is in Apple documentation: https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain

When you generate keys yourself, as described in Generating New Cryptographic Keys, you can store them in the keychain as an implicit part of that process. If you obtain a key by some other means, you can still store it in the keychain.

In short, after you create a key with SecKeyCreateRandomKey, you need to save this key in the Keychain using 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!
Lenin answered 1/11, 2019 at 12:2 Comment(3)
If you use SecKeyCreateRandomKey you do indeed have to save the key manually. However using SecKeyGeneratePair for example with an Query parameter of kSecAttrIsPermanent saves the key in the keychain automatically. Unfortunately accessing the key after in iOS 13 Simulator is now broken using this. So definitely a bug. Have you 100% tested that your code above does indeed work?Rippy
So I experimented with adding the key manually and although it works, the key can still not be retrieved on an iOS 13 simulator (at least with my previously working query code). Seems to be the bug is 100% to do with SecItemCopyMatchingRippy
We are using kSecAttrIsPermanent as well as having code for manually persisting the key. This seems to work fine on our iOS 13 simulators.Lenin

© 2022 - 2024 — McMap. All rights reserved.