As @mike-c stated in his answer, it seems that the solution cannot be achieved with newer Keychain APIs that were introduced for supporting iOS and iCloud in the first place. Especially the access control API is no longer compatible with the original way macOS used to treat it. And there were a ton of different APIs introduced on top of that, as shown in session 711 from WWDC 2014: Keychain and Authentication with Touch ID (from 09:40), which makes the understanding even harder.
@mike-c’s answer contains many hints but doesn’t show the full picture. Similarly to this question on Apple Developer Forums, it tells that the appropriate access control for the Keychain item has to be set at the point of creation. It’s a bit unfortunate that the old Keychain APIs are no longer mentioned in the new documentation a so I had to dig into an archived version of the Keychain Services Programming Guide.
Creating Access Object
To describe the access configuration, we have to work with so-called access objects (SecAccess
), which have associated access control lists (SecACL
). To learn more about ACLs, see the explanation in the old or new (and less detailed) version of the documentation. For the purposes of this answer, ACLs define which application can access the Keychain item for a given operation.
Using SecAccessCreate(…)
you can create a new access object with a predefined system default configuration, which is based on the provided parameters. By default, it holds three ACLs. I tried to add a new ACL as suggested in @mike-c’s answer but run into a strange behavior where the access to the item was sometimes granted and sometimes not.
The approach which did the trick is described in the „Advanced Topics“ section of this documentation page. Basically, instead of adding a new ACL, we modify one of the existing ones.
This function creates an access object configured with unrestricted access for all applications. It’s written in Swift by keeping the style of the Keychain C API. It’s quite awkward, but consistency won this time.
/// Creates an access object with the system default configuration which has an altered ACL
/// to allow access for all applications.
///
/// - Parameter descriptor: The name of the item as it should appear in security dialogs.
/// - Parameter accessRef: The pointer to the new access object.
/// - Returns: A result code.
func SecAccessCreateForAllApplications(
descriptor: CFString,
accessRef outerAccessRef: UnsafeMutablePointer<SecAccess?>
) -> OSStatus {
var accessRef: SecAccess?
// Create an access object with access granted to no application (2nd parameter).
// It comes configured with 3 default ACLs.
let accessCreateStatus = SecAccessCreate(
descriptor,
[] as CFArray, // No application has access
&accessRef
)
guard accessCreateStatus == errSecSuccess else { return accessCreateStatus }
guard let access = accessRef else { return accessCreateStatus }
// Extract the default ACLs from the created access object for the *decrypt* authorization tag.
guard let aclList = SecAccessCopyMatchingACLList(
access,
kSecACLAuthorizationDecrypt
) as? [SecACL] else { return errSecInvalidACL }
// There should be exactly one ACL for the *decrypt* authorization tag.
guard aclList.count == 1 else { return errSecInvalidACL }
guard let decryptACL = aclList.first else { return errSecInvalidACL }
// Extract all authorizations from the default ACL for the *decrypt* authorization tag.
let allAuthorizations = SecACLCopyAuthorizations(decryptACL)
// Remove the default ACL for the *decrypt* authorization tag from the access object.
let aclRemoveStatus = SecACLRemove(decryptACL)
guard aclRemoveStatus == errSecSuccess else { return aclRemoveStatus }
// Create a new ACL with access for all applications and add it to the access object.
var newDecryptACLRef: SecACL?
let aclCreateStatus = SecACLCreateWithSimpleContents(
access,
nil, // All applications have access
descriptor,
[], // Empty prompt selector
&newDecryptACLRef
)
guard aclCreateStatus == errSecSuccess else { return aclCreateStatus }
guard let newDecryptACL = newDecryptACLRef else { return aclCreateStatus }
// Set the authorizations extracted from the default ACL to the newly created ACL.
let aclUpdateAuthorizationStatus = SecACLUpdateAuthorizations(newDecryptACL, allAuthorizations)
guard aclUpdateAuthorizationStatus == errSecSuccess else { return aclUpdateAuthorizationStatus }
// Finally, write the access to the outer pointer.
outerAccessRef.initialize(to: access)
return errSecSuccess
}
Writing Keychain Item
The second part is fairly simple. It turns out you can specify an access object when creating a Keychain item using SecItemAdd(…)
. Put the custom access object as a value for the key kSecAttrAccess
into the attributes dictionary, and it will be applied.
This code writes the Keychain item with the custom access object configured:
var accessRef: SecAccess?
let accessCreateStatus = SecAccessCreateForAllApplications(
descriptor: "" as CFString, // Actually not necessary
accessRef: &accessRef
)
guard accessCreateStatus == errSecSuccess else { exit(EXIT_FAILURE) }
guard let access = accessRef else { exit(EXIT_FAILURE) }
let attributes: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: "Foo",
kSecValueData: "bar".data(using: .utf8)!,
kSecAttrAccess: access
]
let itemAddStatus = SecItemAdd(attributes as CFDictionary, nil)
guard itemAddStatus == errSecSuccess else { exit(EXIT_FAILURE) }
Reading Keychain Item
When revealing the Keychain item in the Keychain Access app, it is correctly marked as accessible to all applications.
You can read the item from another app without any dialog being presented. Here is the code to read and decrypt the item’s payload:
let attributes: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: "Foo",
kSecReturnData: kCFBooleanTrue as Any,
kSecMatchLimit: kSecMatchLimitOne
]
var dataRef: AnyObject?
let status = SecItemCopyMatching(attributes as CFDictionary, &dataRef)
guard status == errSecSuccess else { exit(EXIT_FAILURE) }
guard let data = dataRef as? Data else { exit(EXIT_FAILURE) }
guard let string = String(data: data, encoding: .utf8) else { exit(EXIT_FAILURE) }
print(string) // => bar
I still haven’t mastered the legacy Keychain API, so any feedback on the presented approach is welcome.