SecItemCopyMatching can't read iCloud keychain - macos

I grabbed code off Stack Overflow to access some web browser passwords. It works great as long as the passwords are in the login keychain. At some point, the particular account I am interested in got moved to the iCloud keychain and no longer exists in the login keychain. SecItemCopyMatching can't find it. It returns OSStatus -23500 which is "item not found". How can I access those entries?
CFArrayRef result = NULL;
NSDictionary *params = #{ (__bridge id)kSecClass : (__bridge id)kSecClassInternetPassword,
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitAll,
(__bridge id)kSecReturnAttributes : (__bridge id)kCFBooleanTrue,
(__bridge id)kSecAttrProtocol : (__bridge id)kSecAttrProtocolHTTPS,
(__bridge id)kSecAttrServer : #"accounts.mydomain.com"
};
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(params), (CFTypeRef *) &result);
if (status == noErr) {
// item found!
}

Though on macOS you could see both login and iCloud keychains, former is macOS-style keychain while latter is iOS-style keychain. macOS-style keychain items can be freely accessed by apps as long as the user consents. iOS-style items follow access group policies strictly.
Every key in the iCloud keychain is under an access group. An app. calling SecItemCopyMatching should've said access group as an entitlement for a key to turn up in the query results.
Once you're sure the searched key's access group is among one of the access groups listed under your app's entitlements, there're a couple of attributes you've to pass to SecItemCopyMatching query dictionary (in addition to the mandatory ones like kSecClass):
kSecAttrAccessGroup
kSecAttrSynchronizable
If your app. is entitled to more than one access group, set the right group through kSecAttrAccessGroup. Set kSecAttrSynchronizable to kCFBooleanTrue to search only iOS-style items (which are synchronized); setting it to kSecAttrSynchronizableAny searches both synced and not-synced keys (like the ones in login keychain). Default is to search only local keys (kCFBooleanFalse).
Example Query
This returns an array of dictionaries; one dictionary per key; all iCloud keys your app. has access to turn up. The key-value pairs in each dictionary are corresponding key's properties; this doesn't return the key's value 1
NSString* account = #"my_account";
NSString* service = #"some_service";
NSDictionary* query = #{
(id)kSecClass: (id) kSecClassGenericPassword,
// skip as this example app has only one access group
// (id)kSecAttrAccessGroup: (id) keychainAccessGroup,
(id)kSecAttrSynchronizable: (id)kCFBooleanTrue,
(id)kSecAttrAccount: (id) account,
(id)kSecAttrService: (id) service,
(id)kSecReturnAttributes: (id)kCFBooleanTrue,
(id)kSecMatchLimit: (id)kSecMatchLimitAll,
};
CFTypeRef result;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, &result);
if (status != errSecSuccess) {
CFStringRef err = SecCopyErrorMessageString(status, nil);
NSLog(#"Error: %#", err);
CFRelease(err);
} else {
NSArray* attributes_of_keys = (__bridge NSArray*)result;
}
Inspecting attributes_of_keys under the debugger should tell you plenty.
Source: Apple Developer forum thread, SecItemCopyMatching and beyond.
Apple has done a shoddy job of documenting the SecItem* API family. Information is there but is strewn across different (irrelevant) places (some even outside the official documentation).
1: pass (id)kSecReturnData: (id)kCFBooleanTrue to get key's data; however you can't use kSecReturnData and kSecMatchLimitAll together

Related

What API is there for selecting iTunes library location?

How does one programatically set the iTunes library location on macOS to custom locations using e.g. C / Obj-C or Swift API?
Alternatively, environmental settings, such as modifying plists, using the defaults CLI tool, or similar approaches, are also OK for me.
Ordinarily, selecting a custom iTunes library location is done by launching iTunes while holding down the option key. I need to be able to do this in e.g. a unit testing environment / programatically.
You may be able to set it via the prefs.
This is how I access it.
-(void)loadITunesPrefLibraryPath {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSDictionary *userPref = [userDefaults persistentDomainForName:#"com.apple.iTunes"];
id dataBaseLoc = [userPref objectForKey:#"Database Location"];
NSLog(#"%s dataBaseLoc is:%#", __PRETTY_FUNCTION__, dataBaseLoc);
NSLog(#"%s dataBaseLoc class is:%#", __PRETTY_FUNCTION__, [dataBaseLoc class]);
NSData* dataBaseData = (NSData*)dataBaseLoc;
BOOL staleBook = NO;
NSError* bookError = nil;
NSURL* dataBaseURL = [NSURL URLByResolvingBookmarkData:dataBaseData options:NSURLBookmarkResolutionWithoutMounting relativeToURL:nil bookmarkDataIsStale:&staleBook error:&bookError];
self.libExtDBfile = dataBaseURL;
}
Once you get the userPrefs for iTunes.
And create a BookMarkData from URL.
You might be able to set it via
[userPref setObject:newDataBaseLoc forKey:#"Database Location"];
also see next answer for possible ITLibrary framework private API access

Cloudkit: " error saving record WRITE operation not permitted"

I'm trying to save a record CloudKit but I'm getting the following error from cloudkit:
error saving record este es error: Error saving record <CKRecordID: 0x7fef15b5d2a0; 2:(_defaultZone:__defaultOwner__)> to server: WRITE operation not permitted
Here is how I'm trying to save the record:
[publicDatabase saveRecord:recordContent completionHandler:^(CKRecord *record, NSError *error){
if (!error)
{
NSLog(#"saved!!!");
}
else
{
if ([[error.userInfo valueForKey:#"ErrorDescription"] isEqualToString:#"record to insert already exists"])
{
NSLog(#"record already exist %#",[error.userInfo valueForKey:#"ErrorDescription"]);
}
NSLog(#"error saving record : %#",error.localizedDescription);
}
}];
But before had I check if cloudkit is available:
[myContainer accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError *error)
{
NSLog(#" no error but status %ld",accountStatus);
if (((accountStatus == 3) || (accountStatus == 2)) && (!error))
{
NSLog(#" no error but status %ld",accountStatus);
// typedef NS_ENUM(NSInteger, CKAccountStatus) {
// /* An error occurred when getting the account status, consult the corresponding NSError */
// CKAccountStatusCouldNotDetermine = 0,
// /* The iCloud account credentials are available for this application */
// CKAccountStatusAvailable = 1,
// /* Parental Controls / Device Management has denied access to iCloud account credentials */
// CKAccountStatusRestricted = 2,
// /* No iCloud account is logged in on this device */
// CKAccountStatusNoAccount = 3,
//
// }
}
if (error)
{
NSLog(#" accountStatus error %#",error);
}
} ];
Where I'm getting status 1, meaning CKAccountStatusAvailable.
Any of you knows why this is happening it has been working fine until the last record or any of you knows a work around this?
I'll really appreciate your help.
You need to set permission to allow a user to write (or delete) a record created by someone else. You do that in the Development Environment, under Schema, Record Types, select the specific record, then over on the right there is a drop down menu labelled Security. Grant to the Role ' Authenticated' the right to Read and Write. Then deploy to Production.
This was moved in the latest CloudKit, took me a while to track it down.
Beware, this isn't instant, it takes a while for these changes to propagate after saving them. Come back later and refresh the page to see if they've been applied.
If you are still getting this error after setting these permissions and letting them propagate then it's likely that your iCloud Login in Simulator is messed up. Logging out of iCloud and logging in again fixed this for me.
I spent 2 days for "Permission failure (10/2007)". All permissions and security roles was set as in William.T picture. Relogin into iCloud doesn't got any results.
The snag was in one small setting (not documented by Apple) - it was switcher "iCloud Drive" which located in
Settings/[your account]/iCloud/iCloud Drive
on my device. Apple's documentation is terrible! no one will return me 2 days spent.
Hope this helps someone with the same problem.
In my case i forgot to do a basic check: make sure you're logged in with your Apple ID and make sure the iCloud checkmark for your application is turned on.
This can happen especially using Simulators.

Keychain Access as root user from MAC Application

I am working on MAC Application to create VPN Connection from MY Application.
After a lot of Research i found that i need to run application as ROOT to store User password and sharedSecretKey in SYSTEM keychain.
Application user will not gonna open application as ROOT so that i need to add user password and sharedsecretkey in SYSTEM KEYCHAIN without ROOT Access.
I search on web on this and found that Apple Provide this code : https://developer.apple.com/library/mac/samplecode/SMJobBless/Introduction/Intro.html
https://developer.apple.com/library/mac/samplecode/EvenBetterAuthorizationSample/Introduction/Intro.html
but didn't understand how can i use this 2 code in my application to store user's password and SharedSecretKey in SYSTEM KEYCHAIN WITH OUT ROOT ACCESS.
Any help will be appreciated.
Thanks in advance.
Here is my code to add Password in SYSTEM KEYCHAIN Which is work great if i run my code as ROOT.
// Vendor dependencies
#import <Security/SecKeychain.h>
// Local dependencies
#import "VPNKeychain.h"
// These are the applications which are going to get access to new Keychain items.
// How do we know them? Just create a VPN service manualy and run the following command:
// security dump-keychain -a /Library/Keychains/System.keychain
// Among the results, you will find your VPN service and you can see the paths that have access to it
static const char * trustedAppPaths[] = {
"/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/Helpers/SCHelper", "/System/Library/PreferencePanes/Network.prefPane/Contents/XPCServices/com.apple.preference.network.remoteservice.xpc",
"/System/Library/CoreServices/SystemUIServer.app",
"/usr/sbin/pppd",
"/usr/sbin/racoon",
"/usr/libexec/configd",
};
// This class contains all code we need to handle System Keychain Items
// Exit status codes: 60-79
#implementation VPNKeychain
// This will create a PPP Password Keychain Item
+ (int) createPasswordKeyChainItem:(NSString*)label forService:(NSString*)service withAccount:(NSString*)account andPassword:(NSString*)password {
return [self createItem:label withService:service account:account description:#"PPP Password" andPassword:password];
}
// This will create an IPSec Shared Secret Keychain Item
+ (int) createSharedSecretKeyChainItem:(NSString*)label forService:(NSString*)service withPassword:(NSString*)password {
service = [NSString stringWithFormat:#"%#.SS", service];
return [self createItem:label withService:service account:#"" description:#"IPSec Shared Secret" andPassword:password];
}
// A generic method to create Keychain Items holding Network service passwords
+ (int) createItem:(NSString*)label withService:(NSString*)service account:(NSString*)account description:(NSString*)description andPassword:(NSString*)password {
// This variable will hold all sorts of operation status responses
OSStatus status;
// Converting the NSStrings to char* variables which we will need later
const char *labelUTF8 = [label UTF8String];
const char *serviceUTF8 = [service UTF8String];
const char *accountUTF8 = [account UTF8String];
const char *descriptionUTF8 = [description UTF8String];
const char *passwordUTF8 = [password UTF8String];
// This variable is soon to hold the System Keychain
SecKeychainRef keychain = NULL;
status = SecKeychainCopyDomainDefault(kSecPreferencesDomainSystem, &keychain);
if (status == errSecSuccess) {
NSLog(#"Succeeded opening System Keychain");
} else {
NSLog(#"Could not obtain System Keychain: %#", SecCopyErrorMessageString(status, NULL));
return 60;
}
NSLog(#"Unlocking System Keychain");
status = SecKeychainUnlock(keychain, 0, NULL, FALSE);
if (status == errSecSuccess) {
NSLog(#"Succeeded unlocking System Keychain");
} else {
NSLog(#"Could not unlock System Keychain: %#", SecCopyErrorMessageString(status, NULL));
return 61;
}
// This variable is going to hold our new Keychain Item
SecKeychainItemRef item = nil;
SecAccessRef access = nil;
status = SecAccessCreate(CFSTR("Some VPN Test"), (__bridge CFArrayRef)(self.trustedApps), &access);
if(status == noErr) {
NSLog(#"Created empty Keychain access object");
} else {
NSLog(#"Could not unlock System Keychain: %#", SecCopyErrorMessageString(status, NULL));
return 62;
}
// Putting together the configuration options
SecKeychainAttribute attrs[] = {
{kSecLabelItemAttr, (int)strlen(labelUTF8), (char *)labelUTF8},
{kSecAccountItemAttr, (int)strlen(accountUTF8), (char *)accountUTF8},
{kSecServiceItemAttr, (int)strlen(serviceUTF8), (char *)serviceUTF8},
{kSecDescriptionItemAttr, (int)strlen(descriptionUTF8), (char *)descriptionUTF8},
};
SecKeychainAttributeList attributes = {sizeof(attrs) / sizeof(attrs[0]), attrs};
status = SecKeychainItemCreateFromContent(kSecGenericPasswordItemClass, &attributes, (int)strlen(passwordUTF8), passwordUTF8, keychain, access, &item);
if(status == noErr) {
NSLog(#"Successfully created Keychain Item");
} else {
NSLog(#"Creating Keychain item failed: %#", SecCopyErrorMessageString(status, NULL));
return 63;
}
return 0;
}
+(NSArray*) trustedApps {
NSMutableArray *apps = [NSMutableArray array];
SecTrustedApplicationRef app;
OSStatus err;
for (int i = 0; i < (sizeof(trustedAppPaths) / sizeof(*trustedAppPaths)); i++) {
err = SecTrustedApplicationCreateFromPath(trustedAppPaths[i], &app);
if (err == errSecSuccess) {
//NSLog(#"SecTrustedApplicationCreateFromPath succeeded: %#", SecCopyErrorMessageString(err, NULL));
} else {
NSLog(#"SecTrustedApplicationCreateFromPath failed: %#", SecCopyErrorMessageString(err, NULL));
}
[apps addObject:(__bridge id)app];
}
return apps;
}
In OS X, applications do not handle users' credentials directly, but instead request the system to do so, via the function call AuthorizationCopyRights, which is documented in Authorization Services.
A Gui application cannot directly perform administrative (root) actions and since Yosemite (10.10), Gui applications cannot run as root. Instead, your application must use a 'helper' application via XPC services, which is what the SMJobBless and BetterAuthorization samples demonstrate. You can read more about XPC here.
In your case, you would need to create such a helper application, which will have the necessary rights to access the system keychain.
Note that if you plan to distribute your application via the Apple Store, the application must be sandboxed and cannot use any security services, such as calling the function AuthorizationCopyRights.
It is explained in the sample code that you link to, see ReadMe.txt:
Once you run the sample you'll be prompted for an admin user name and
password. Enter your admin user name and password and, if all goes
well, the sample's window will show "The Helper Tool is available!"
indicating that everything is OK. If not, you can look in the console
log for information about the failure.
So generally, your application will have to ask for admin credentials at some point.
Update:
This should be done through a privileged helper tool, as demonstrated in cited SMJobBless example. Your helper tool should perform keychain access for your app. Here are main steps to install such helper tool:
Create authorisation object with AuthorizationCreate function.
Perform preauthorisation on the object with given set of rights using
AuthorizationCopyRights function. This will in fact result in asking your user for admin credentials.
Verify, install and register helper tool with launchd using
SMJobBless function.
Once the helper tool is installed and registered you should use NSXPCConnection to talk to your helper tool. See Sandboxing with NSXPCConnection sample code for details on how to achieve it.

Accessing 'Internet Accounts' programmatically

I'm trying to access the 'accounts' that are stored in the Internet Accounts area on the system settings on OSX. I'm aware of the 'ACAccount' library, but this only really seems to be of any use for Social integration, for example Facebook/Twitter, what i would like is to be able to detect that you have an Exchange account there and open up certain features within an app. But i'm guessing i'll need to get my users to re-enter their details in my app?
I did try and use it, however i get an empty array.
ACAccountStore *store = [[ACAccountStore alloc] init];
NSArray *accounts = [store accounts]; //This is empty
Does anyone know if i'm missing something, or if it's not possible? Thanks!
Edit
I have gained access to the Twitter account for example, but it's not returned in the Accounts list, i had to request permission first. Which makes sense. However, i still see no way of getting access to the Exchange account.
ACAccountStore *account = [[ACAccountStore alloc] init];
ACAccountType *accountType = [account accountTypeWithAccountTypeIdentifier:
ACAccountTypeIdentifierTwitter];
[account requestAccessToAccountsWithType:accountType options:nil
completion:^(BOOL granted, NSError *error)
{
if (granted == YES)
{
NSArray *arrayOfAccounts = [account
accountsWithAccountType:accountType];
}
}];

What is the correct way to handle stale NSURL bookmarks?

When resolving an NSURL from a security scoped bookmark, if the user has renamed or moved that file or folder, the bookmark will be stale. Apple's document says this regarding staleness:
isStale
On return, if YES, the bookmark data is stale. Your app should
create a new bookmark using the returned URL and use it in place of
any stored copies of the existing bookmark.
Unfortunately, this rarely works for me. It may work 5% of the time. Attempting to create a new bookmark using the returned URL results in an error, code 256, and looking in Console reveals a message from sandboxd saying deny file-read-data on the updated URL.
Note If regenerating the bookmark does work, it seems to only work the first time it is regenerated. It seems to never work should the folder/file be moved/renamed again.
How I initially create & store the bookmark
-(IBAction)bookmarkFolder:(id)sender {
_openPanel = [NSOpenPanel openPanel];
_openPanel.canChooseFiles = NO;
_openPanel.canChooseDirectories = YES;
_openPanel.canCreateDirectories = YES;
[_openPanel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) {
if (_openPanel.URL != nil) {
NSError *error;
NSData *bookmark = [_openPanel.URL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (error != nil) {
NSLog(#"Error bookmarking selected URL: %#", error);
return;
}
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:bookmark forKey:#"bookmark"];
}
}];
}
Code that resolves the bookmark
-(void)resolveStoredBookmark {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSData *bookmark = [userDefaults objectForKey:#"bookmark"];
if (bookmark == nil) {
NSLog(#"No bookmark stored");
return;
}
BOOL isStale;
NSError *error;
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark
options:NSURLBookmarkResolutionWithSecurityScope
relativeToURL:nil
bookmarkDataIsStale:&isStale
error:&error];
if (error != nil) {
NSLog(#"Error resolving URL from bookmark: %#", error);
return;
} else if (isStale) {
if ([url startAccessingSecurityScopedResource]) {
NSLog(#"Attempting to renew bookmark for %#", url);
// NOTE: This is the bit that fails, a 256 error is
// returned due to a deny file-read-data from sandboxd
bookmark = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
[url stopAccessingSecurityScopedResource];
if (error != nil) {
NSLog(#"Failed to renew bookmark: %#", error);
return;
}
[userDefaults setObject:bookmark forKey:#"bookmark"];
NSLog(#"Bookmark renewed, yay.");
} else {
NSLog(#"Could not start using the bookmarked url");
}
} else {
NSLog(#"Bookmarked url resolved successfully!");
[url startAccessingSecurityScopedResource];
NSArray *contents = [NSFileManager.new contentsOfDirectoryAtPath:url.path error:&error];
[url stopAccessingSecurityScopedResource];
if (error != nil) {
NSLog(#"Error reading contents of bookmarked folder: %#", error);
return;
}
NSLog(#"Contents of bookmarked folder: %#", contents);
}
}
When the bookmark is stale, the resulting resolved URL does point to the correct location, I just can't actually access the file despite the fact that [url startAccessingSecurityScopedResource] returns YES.
Perhaps I'm misinterpreting the documentation regarding stale bookmarks, but I'm hoping I'm just doing something stupid. Popping an NSOpenPanel each time a bookmarked file/folder is renamed or moved, my only other option at this point, seems ridiculous.
I should add that I have com.apple.security.files.bookmarks.app-scope, com.apple.security.files.user-selected.read-write, and com.apple.security.app-sandbox all set to true in my entitlements file.
After a lot of disappointing testing I've come to the following conclusions. Though logical, they're disappointing since the resulting experience for users is far from ideal and a significant pain for developers depending on how far they're willing to go to help users re-establish references to bookmarked resources.
When I say "renew" below, I mean "generate a new bookmark to replace a stale bookmark using the URL resolved from the stale bookmark."
Renewal always works as long as the bookmarked resource is moved or renamed within a directory that your app already has permission to access. So, by default, it always works inside your application's container folder.
Renewal fails if a bookmarked resource is moved into a folder your application does not have permission to access. e.g. User drags a folder from your container folder to some folder outside the container folder. You will be able to resolve the URL, but not access nor renew the bookmark.
Renewal fails if a bookmarked resource lives in a folder your application doesn't have access to and is then renamed. This means a user can explicitly grant your application access to a resource, then inadvertently revoke that access just by renaming it.
Resolution fails if a resource is moved to another volume. Not sure if this is a limitation of bookmarks in general or just when used in a sandboxed application.
For issues 2 & 3 you're in a decent position as the developer since resolution of the bookmarked URL does work. You can at least lead the user by telling them exactly which resources they need to grant your app access to and where they are. The experience could be improved by having them select a folder that contains (directly or indirectly) all resources that you need to renew a bookmark for. This could even be the volume, which solves the problem completely if they're willing to give your application this much access.
For issue 4, resolution doesn't work at all. The user will have to relocate the file without any hints since you can't resolve the new location. One thing I've done in my current app that has reduced the pain of this issue is to add an extended attribute to any resource I store a bookmark for. Doing this at least lets me have the user choose a folder to search for previously associated resources.
Frustrating limitations, but bookmarks still win over storing static paths.

Resources