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:
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
}
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
andSecItemAdd
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 witherrSecItemNotFound
while the subsequent add fails witherrSecDuplicateItem
).
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"