Unable to retrieve or delete item from keychain, but can save the item

I am working on an iOS/iPadOS application, written in Swift and Obj-C, using both UIKit and SwiftUI.

I am attempting to save an item to the keychain using the following code:


@objc class UserPin : NSObject {
    @objc var uuid: UUID = UUID()
    @objc var PIN: String = ""

    @objc convenience init(uuid: UUID, PIN: String){
        self.init()
        self.uuid = uuid
        self.PIN = PIN
    }
}

@objc func saveUserPin(userPin: UserPin){
        self.deleteUserPinForUser(uuid: userPin.uuid)
        do {
            try KeychainAccessorService.storeItem(keyName: kUserPin, username: userPin.uuid.uuidString.data(using: .utf8)!, password: EncryptionHelper.encryptString(stringToEncrypt: userPin.PIN)!)
        } catch {
            print(error)
        }

    }

    static func storeItem(keyName: String, username: Data, password: Data, otherData: Data? = nil) throws {
        var query = [kSecClass: kSecClassGenericPassword,
                     kSecAttrLabel: keyName,
                     kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
                     kSecAttrIsInvisible: true,
                     kSecAttrSynchronizable: false,
                     kSecUseDataProtectionKeychain: true,
                     kSecValueData: password,
                     kSecAttrAccount: username] as [String: Any]

        if otherData != nil {
            query[kSecAttrGeneric as String] = otherData
        }

        // Add the key data.
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            throw TransnetError("Unable to store item: \(status.message)")
        }
    }

Upon executing this code, I get the error: Unable to store item: The specified item already exists in the keychain.

This code returns that an item is not present with these criteria (I tried with both the KSecAttrAccount commented out and in, it does not appear to matter either way. It returns status code -25300.

    @objc func deleteUserPinForUser(uuid: UUID){
        var query = getDeletionDictionaryForTag(tag: kUserPin)
//        query[kSecAttrAccount as String] = uuid.uuidString.data(using: .utf8)
        let _ = SecItemDelete((query as CFDictionary))
    }

    private func getDeletionDictionaryForTag(tag: String) -> [String: Any]{
        let dict = [kSecClass as String: kSecClassGenericPassword,
                    kSecAttrLabel as String: tag,
                    kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked,
                    kSecAttrIsInvisible as String: true,
                    kSecAttrSynchronizable as String: false,
                    kSecUseDataProtectionKeychain as String: true] as [String : Any]
        return dict;
    }

I am also not able to retrieve this item using the following code:

    @objc func fetchUserPin(uuid: UUID) -> UserPin{
        let userPin = UserPin()
        do {
            let itemDictionary = try KeychainAccessorService.fetchItemForTag(name: kUserPin, username: uuid.uuidString.data(using: .utf8))
            let passwordData = EncryptionHelper.decryptString(dataToDecrypt: itemDictionary?[kSecValueData as String] ?? Data()) ?? ""
            userPin.uuid = uuid
            userPin.PIN = passwordData
        } catch {

        }
        return userPin
    }

    private static func fetchItemForTag(name: String, username: Data? = nil)  throws -> Dictionary<String, Data>? {
        // Seek a generic password with the given account.
        var query = getFetchDictionary(name: name, matchLimit: kSecMatchLimitOne as String)
        if let username = username {
            query[kSecAttrAccount as String] = username
        }

        var returnDict = Dictionary<String, Data>()
        // Find and cast the result as data.
        var item: CFTypeRef?
        switch SecItemCopyMatching(query as CFDictionary, &item) {
        case errSecSuccess:
            guard let dataDict = item as? Dictionary<String, Any> else { return nil }
            if let data = dataDict[kSecAttrAccount as String] as? Data{
                returnDict.updateValue(data, forKey: kSecAttrAccount as String)
            }
            if let data = dataDict[kSecAttrGeneric as String] as? Data{
                returnDict.updateValue(data, forKey: kSecAttrGeneric as String)
            }
            returnDict.updateValue(dataDict[kSecValueData as String] as! Data, forKey: kSecValueData as String)
            return returnDict
        case errSecItemNotFound: return nil
        case let status: throw TransnetError("Keychain read failed: \(status.message)")
        }
    }

This code drops into the errSecItemNotFound case.

Does anyone have any ideas what I'm doing wrong here?

Accepted Reply

Problems like this are almost always caused by a misunderstanding about how the keychain manages uniqueness. This varies by keychain item class. For the details, see the errSecDuplicateItem documentation.

For a generic password the key attributes are kSecAttrService and kSecAttrAccount. I generally recommend that you set both of these when you save an item to the keychain and only include these (oh, and kSecClass) when you query for that item.

The errSecDuplicateItem you’re geting from your storeItem(…) method is because an item exists with kSecAttrAccount for that user name and kSecAttrService empty. You then try to delete the item and that fails with errSecItemNotFound because one of the other attributes in the query dictionary returned by getDeletionDictionaryForTag(…) doesn’t match the item that’s currently in the keychain.

Does your app support multiple simultaneous accounts? If not, I typically hardcode kSecAttrService and kSecAttrAccount and store the user name elsewhere (in some other attribute). That means that the query dictionary is just:

let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: … fixed value …,
    kSecAttrAccount: … fixed value …,
    kSecReturnData: true,
]

and it’s hard to go wrong.

If your app supports multiple accounts then it makes sense to use kSecAttrAccount to store the account name. In that case your query dictionary would look like this:

let accountName: String = …
let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: … fixed value …,
    kSecAttrAccount: accountName,
    kSecReturnData: true,
]

if you know the account name in advance, or:

let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: … fixed value …,
    kSecReturnAttributes: true,
]

when querying for all known accounts.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Eskimo, you rock. I can't tell you how long I spent trying to figure this out and you just instilled some knowledge into me this morning. Thanks so much for saving my day again. :)

    This forum is an awesome part of being a developer for Apple platforms and why I prefer working with Apple products over the competition, and why we're moving away from supporting Android as an organization.

    Thanks again! :)

Add a Comment

Replies

Certain files have a property incorrectly set that will prevent them from being changed or deleted while the system is running. That property can only be removed in Recovery mode. If you don't feel that you can carry out this procedure yourself, please get someone more experienced to help you.

  1. Back up all data. There are ways to back up a computer that isn't fully functional. Ask if you need guidance. Don't skip this step.

  2. Disconnect all external storage devices.

3.Start up in Recovery mode. Select a language, if prompted. The OS X Utilities screen will appear.

  1. This step is only necessary if you use FileVault 2. If you don't know what FileVault is, you're not using it. Go to the next step. Otherwise, launch Disk Utility, then select the icon of the FileVault volume ("Macintosh HD," unless you gave it a different name.) It will be nested below another drive icon. Click the Unlock button in the toolbar and enter your login password when prompted. Then quit Disk Utility to be returned to the main screen.

  2. Select Get Help Online. Safari will launch. While in Recovery, you'll have no access to your bookmarks, but you won't need them. Load this web page.

  3. Triple-click anywhere in the line below to select it:

chflags norestricted /V*//L/Keyc*/*

Copy the selected text to the Clipboard by pressing the key combination command-C.

  1. Quit Safari. From the menu bar, select

Utilities ▹ Terminal

The Terminal application will launch. Paste into the Terminal window by pressing the key combination command-V.

Wait for a new line ending in a dollar sign ($) to appear. Quit Terminal to be returned to the main screen.

  1. Select

 ▹ Restart

from the menu bar.

You should now be able to change or delete the file(s) in question.

  • Hi there, Sorry, I added some more information to my post, but I am working with iOS/iPadOS in a custom developed application using Swift and Objective-C, and UIKit and SwiftUI. I am attempting to remove and/or retrieve an item from the Keychain using the Security framework SecItem class. Thanks!

Add a Comment

Problems like this are almost always caused by a misunderstanding about how the keychain manages uniqueness. This varies by keychain item class. For the details, see the errSecDuplicateItem documentation.

For a generic password the key attributes are kSecAttrService and kSecAttrAccount. I generally recommend that you set both of these when you save an item to the keychain and only include these (oh, and kSecClass) when you query for that item.

The errSecDuplicateItem you’re geting from your storeItem(…) method is because an item exists with kSecAttrAccount for that user name and kSecAttrService empty. You then try to delete the item and that fails with errSecItemNotFound because one of the other attributes in the query dictionary returned by getDeletionDictionaryForTag(…) doesn’t match the item that’s currently in the keychain.

Does your app support multiple simultaneous accounts? If not, I typically hardcode kSecAttrService and kSecAttrAccount and store the user name elsewhere (in some other attribute). That means that the query dictionary is just:

let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: … fixed value …,
    kSecAttrAccount: … fixed value …,
    kSecReturnData: true,
]

and it’s hard to go wrong.

If your app supports multiple accounts then it makes sense to use kSecAttrAccount to store the account name. In that case your query dictionary would look like this:

let accountName: String = …
let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: … fixed value …,
    kSecAttrAccount: accountName,
    kSecReturnData: true,
]

if you know the account name in advance, or:

let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: … fixed value …,
    kSecReturnAttributes: true,
]

when querying for all known accounts.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Eskimo, you rock. I can't tell you how long I spent trying to figure this out and you just instilled some knowledge into me this morning. Thanks so much for saving my day again. :)

    This forum is an awesome part of being a developer for Apple platforms and why I prefer working with Apple products over the competition, and why we're moving away from supporting Android as an organization.

    Thanks again! :)

Add a Comment