When is kSecAttrService actually required when saving an item to the Keychain?

I was basically saving items into the Keychain with the following query dictionary:

    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: value,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
    ]

Where key is a String value and value is a Data that used to be a String.

I was getting the following error:

  • code: -25299
  • description: The specified item already exists in the keychain

After a lot of digging in I saw that I needed to add kSecAttrService to the dictionary and after that it all started working. The service value is a String value.

   let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: key,
        kSecValueData as String: value,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
    ]

These were the articles that suggested adding the kSecAttrService parameter:

  1. https://stackoverflow.com/a/11672200
  2. https://stackoverflow.com/a/58233542

But in the same code base I found that other developers were saving using a dictionary similar to the one I first provided and it works:

    var query: [String : Any] = [
        kSecClass as String       : kSecClassGenericPassword as String,
        kSecAttrAccount as String : key,
        kSecValueData as String   : data
    ]

I don't know how to explain why my first implementation didn't work even though it was similar to what was already in the code base but the second approach worked well.

Regardless of the query dictionary, this is how I'm saving things:

static func save(value: Data, key: String, service: String) -> KeyChainOperationStatus {
    logInfo("Save Value - started, key: \(key), service: \(service)")
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: key,
        kSecValueData as String: value,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
    ]
    
    // Remove any existing key
    let cleanUpStatus = SecItemDelete(query as CFDictionary)
    let cleanUpStatusDescription = SecCopyErrorMessageString(cleanUpStatus, nil)?.asString ?? "__cleanup_status_unavailable"
    logInfo("Save Value - cleanup status: \(cleanUpStatus), description: \(cleanUpStatusDescription)")
    guard cleanUpStatus == errSecSuccess || cleanUpStatus == errSecItemNotFound else {
        logError("Save Value - Failed cleaning up KeyChain")
        return .cleanupFailed(code: cleanUpStatus)
    }
    
    // Add the new key
    let saveStatus = SecItemAdd(query as CFDictionary, nil)
    let saveStatusDescription = SecCopyErrorMessageString(saveStatus, nil)?.asString ?? "__save_status_unavailable"
    logInfo("Save Value - save status [\(saveStatus)] : \(saveStatusDescription)")
    guard saveStatus == errSecSuccess else {
        logError("Save Value - Failed saving new value into KeyChain")
        return .savingFailed(code: saveStatus)
    }
    
    return .successs
}
Answered by DTS Engineer in 824189022

I recommend that you have a read of:

It goes into a lot of detail about some the core concept of uniqueness and the different types of dictionaries. Specifically:

  • In an add dictionary, if you don’t supply a value for an item attribute property, the keychain will synthesise a default. Usually this is the empty string.

  • In a query dictionary, if you don’t supply a value for an item attribute property, it acts like a wildcard.

With reference to your last code snippet, there are a number of issues:

  • You are deleting and then re-adding, which is something I recommend against. Instead, update your existing item. See Prefer to Update SecItem: Pitfalls and Best Practices.

  • If you continue with your current approach, you need a separate dictionary for your SecItemDelete and SecItemAdd calls. The delete wants a pure query dictionary and you’re giving it an add dictionary. This ends up over-specifying the query, exposing you to nonsensical results (like the delete failing with errSecItemNotFound while the subsequent add fails with errSecDuplicateItem).

Share and Enjoy

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

I recommend that you have a read of:

It goes into a lot of detail about some the core concept of uniqueness and the different types of dictionaries. Specifically:

  • In an add dictionary, if you don’t supply a value for an item attribute property, the keychain will synthesise a default. Usually this is the empty string.

  • In a query dictionary, if you don’t supply a value for an item attribute property, it acts like a wildcard.

With reference to your last code snippet, there are a number of issues:

  • You are deleting and then re-adding, which is something I recommend against. Instead, update your existing item. See Prefer to Update SecItem: Pitfalls and Best Practices.

  • If you continue with your current approach, you need a separate dictionary for your SecItemDelete and SecItemAdd calls. The delete wants a pure query dictionary and you’re giving it an add dictionary. This ends up over-specifying the query, exposing you to nonsensical results (like the delete failing with errSecItemNotFound while the subsequent add fails with errSecDuplicateItem).

Share and Enjoy

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

When is kSecAttrService actually required when saving an item to the Keychain?
 
 
Q