If I do not set kSecAttrAccount right now, is it possible that I cannot read the keychain items correctly?
It’s hard to answer this because you’ve phrased it as a negative question. So, lemme rephrase it:
- If I’m using the data protection keychain (the default on iOS, something you have to opt in to on macOS),
- And I create a generic password keychain item without specifying
kSecAttrAccount, - Will I be able to access that item’s contents using
SecItemCopyMatching?
The answer to that is “Yes.”
At the end of this post I’ve included some code that I used to test this. (Sorry it’s in Swift. I built this by cobbling together various existing bits of code that were all written in Swift, and I don’t have time today to convert them to Objective-C.)
Having said that, my advice is that, when you creating a generic password item you always populate both kSecAttrService and kSecAttrAccount. If you don’t have a sensible value to use for one of these, hard code some fixed string.
One of the reasons for that recommendation is that there’s a fundamental ambiguity between:
- Not supplying an attribute
- Supplying an empty attribute
This breaks down as follows:
- In an add dictionary, not supplying an attribute is equivalent to supplying the empty attribute.
- In a pure query dictionary and a query and return dictionary, not supplying an attribute acts as a wildcard [1]. In contrast, if you supply the empty attribute you’ll only match items where that attribute is empty.
- In an update dictionary, not supplying an attribute leaves the attribute unchanged.
Also, if you call SecItemCopyMatching and pass in kSecReturnAttributes, the system might omit an attribute from the returned dictionary if its value is empty.
Oh, and this is all assuming that you’re using the data protection keychain. My experience is that, on macOS, the file-based keychain shim tends to run into problems when you explore subtle edge cases like this.
This is a huge field of potential pitfall, and hence my advice.
if Keychain Sharing is enabled for the app, but I do not specify kSecAttrAccessGroup when saving keychain data, will the storage location be the same as when Keychain Sharing is disabled?
That depends on how you set up keychain sharing. Sharing access to keychain items among a collection of apps explains:
- How the system uses entitlements to determine your keychain access group list
- And that the first item in that list becomes your default keychain access group
When you enable the Keychain Sharing capability, that changes the keychain access group list. If you want the default keychain access group to stay the same, it’s important to check the new list against the old list and take action if the first item changes. The Lost Keychain Items section of SecItem: Pitfalls and Best Practices walks you through a concrete example of that [2].
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] There are exceptions here. For example, on macOS you don’t get wildcard behaviour if you omit the kSecAttrSynchronizable attribute. Rather, you must explicitly pass in the kSecAttrSynchronizableAny value.
[2] I’ve just updated that example to cover the most common case, that is, when folks add the Keychain Sharing capability to an app that previously didn’t use it. So thanks for asking questions like this! It helps me reassess whether my advice actually addresses the problems that folks see in practice.
enum KeychainBasics {
static func addPassword(_ password: String) throws {
try secCall { SecItemAdd([
kSecClass: kSecClassGenericPassword,
kSecAttrService: "keychain-basics-service",
// kSecAttrAccount: "keychain-basics-account",
kSecValueData: Data(password.utf8),
] as NSDictionary, nil) }
}
static func copyPassword() throws -> String {
let copyResult = try secCall { SecItemCopyMatching([
kSecClass: kSecClassGenericPassword,
kSecAttrService: "keychain-basics-service",
// kSecAttrAccount: "keychain-basics-account",
kSecAttrAccount: "",
kSecReturnData: true,
] as NSDictionary, $0) } as! Data
// The following will ‘repair’ malformed UTF-8 by replacing it with the
// U+FFFD REPLACEMENT CHARACTER.
return String(decoding: copyResult, as: UTF8.self)
}
static func reset() throws {
try secCall { SecItemDelete([
kSecClass: kSecClassGenericPassword,
] as NSDictionary) }
}
}