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 Answer

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"

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