Getting privilege to write a file to \Library\ColorSync\Profiles\ in a mac application - objective-c

From my mac application, I need to write a file to \Library\ColorSync\Profiles. For this, the app needs admin privilege to read write to the folder. Is possible to get the same with in the application? Any help will be appreciated.
I am able to pop up the permission dialog with the following code snippet
NSSavePanel *tvarNSSavePanelObj = [NSSavePanel savePanel];
[tvarNSSavePanelObj setDirectoryURL:[NSURL URLWithString:#"/Library/ColorSync/Profiles"]];
__block NSString *filePathexn = nil;
[tvarNSSavePanelObj beginSheetModalForWindow:[NSApplication sharedApplication].mainWindow completionHandler:^(NSInteger tvarInt) {
if(tvarInt == NSModalResponseOK){
NSURL* tvarUrl = [tvarNSSavePanelObj URL];
NSLog(#"doSaveAs filename = %#",[tvarUrl path]);
NSString *filePath = [tvarUrl path];
filePathexn = [filePath stringByAppendingPathExtension:#"rtf"];
OSStatus status;
if(![[NSFileManager defaultManager]isWritableFileAtPath:filePath]){
NSLog(#"Not Writable at path");
AuthorizationRef authRef;
AuthorizationItem right = {kAuthorizationRightExecute, 0, NULL, 0};
AuthorizationRights rights = {1, &right};
status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagPreAuthorize, &authRef);
AuthorizationFlags authFlags = kAuthorizationFlagDefaults | kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize;
status = AuthorizationCopyRights(authRef, &rights, kAuthorizationEmptyEnvironment, authFlags, NULL);
}
BOOL status1 = [[NSFileManager defaultManager]createFileAtPath:filePathexn contents:nil attributes:nil];
} else if(tvarInt == NSModalResponseCancel) {
return;
} else {
NSLog(#"doSave As tvarInt not equal 1 or zero = %3ld",(long)tvarInt);
return;
}
}];
I need to know how the file write could be done. Still file is not written to the path. Is any tool name need to be specified? Will it be possible with SMJobless() ? Kindly advise a solution!!

Another solution I am posting here. I went through the app distribution guide. It is mentioned that the Authorization APIS are not recommended for an app that is distributed out side of app store.
Any way this solution is working for me, it is a little hack, I don't know. I try to run the apple script form the application, which gives full privilege to the folder (chmod 777 path) and after finishing my task set back to previous privilege set.
- (void)runapplescript{
NSDictionary* errorDict;
NSAppleEventDescriptor* returnDescriptor = NULL;
NSAppleScript* scriptObject = [[NSAppleScript alloc] initWithSource:
#"do shell script \"chmod 777 /Library/ColorSync/Profiles\" with administrator privileges"];
returnDescriptor = [scriptObject executeAndReturnError: &errorDict];
}

I can suggest 2 approaches. One approach is fast but deprecated. Second approach is modern but needs a bit of coding and reading documentation:
You need to write bash script which will do what you need and execute it with privileges which will be granted using something like this: STPrivilegedTask
Write privileged helper tool which will be installed once on first access with root privileges and you will be able do any privileged operation How to write privileged helper tool

If it is saving a ColorSync profile you are after, ColorSyncProfileInstall will do the heavy lifting for you, like prompting the user for permission & authentication. 🏖️
It is also necessary to include the code-signing COLORSYNC_PROFILE_INSTALL_ENTITLEMENT entitlement:
<key>com.apple.developer.ColorSync.profile.install</key>
<true/>
The documentation for the aforementioned symbol can be found in the ColorSyncProfile.h header—partially reproduced here for convenience.
CSEXTERN bool ColorSyncProfileInstall(ColorSyncProfileRef profile, CFStringRef domain, CFStringRef subpath, CFErrorRef* error);
/*
* profile - profile to be installed
* domain - either kColorSyncProfileComputerDomain or kColorSyncProfileUserDomain.
* kColorSyncProfileComputerDomain is for sharing the profiles (from /Library/ColorSync/Profiles).
* kColorSyncProfileUserDomain is for user custom profiles (installed under home directory, i.e. in
* ~/Library/ColorSync/Profiles.
* NULL is the same as kColorSyncProfileUserDomain.
* subpath - CFString created from the file system representation of the path of the file to contain the installed
* profile. The last component of the path is interpreted as a file name if it ends with the extension ".icc".
* Otherwise, the subpath is interpreted as the directory path and file name will be created from the
* profile description tag, appended with the ".icc" extension.
* error - (optional) pointer to the error which will be returned in case of failure.
*
* bool value true is returned if success or false in case of error.
*/

Related

How to get permission with NSSavePanel in sandbox environment

There is a project with sandbox environment, right now i want to let users choose a dir to save files. For example, when user choose "Desktop" in the save panel, then i will save file to the dir "~/Desktop".
The code is as below:
NSSavePanel *savePanel = [NSSavePanel savePanel];
NSInteger result = [savePanel runModal];
if(result == NSFileHandlingPanelOKButton)
{
NSString *dir = [savePanel.URL.path.stringByDeletingLastPathComponent stringByAppendingPathComponent:#"2.txt"];
NSError *err;
NSLog(#"%#",dir);
[#"asasasa" writeToFile:dir atomically:YES encoding:NSUTF8StringEncoding error:&err];
NSLog(#"%#",err);
}
The error happens that Code=513 "You don’t have permission to save the file “2.txt” in the folder “Desktop”.
I have already enable Read/Write of User Selected File in the Capabilities settings. Anybody can tell me what should i do right now ?
Thanks in advance.
You are not saving the file to the URL you got permission for, you changed the URL. This is not allowed.
Specify 2.txt in the save panel so the URL you get back is ~/Desktop/2.txt. Then saving will work.
Side note: There is also an URL related API like writeToURL:atomically:encoding:error and URLByAppendingPathComponent:

My OSX app is sandboxed and I am not able to read data from file by specifying the absolute path

I am completely new to objective C and currently I am trying to advance the functionality of an already existing project.
There is a finder extension in the project which on getting clicked performs an action inside (IBAction) Share(id) sender.
Inside this action , I want to read a file from a particular location (the file contains the port number) and using that port I want to connect to the server.
But what I found was when I click on this extension , nothing happens because it tries to go and read data from the file and is not able to read anything.
I tried to debug this by printing out whatever it has read to some other file but all it printed was blank confirming that it is not able to read the data. Below is my code trying to read the port from a temporary location :
- (IBAction)privateShareAction:(id)sender {
NSFileManager *filemgr;
filemgr = [NSFileManager defaultManager];
if ([filemgr fileExistsAtPath: #"/var/folders/y3/jv117_75505fnk8htdrs0qm40000gr/T/com.aprivacy.xmlCorePort.properties" ] == YES)
{
//create file handle
NSFileHandle *file;
file = [NSFileHandle fileHandleForReadingAtPath:#"/var/folders/y3/jv117_75505fnk8htdrs0qm40000gr/T/com.aprivacy.xmlCorePort.properties"];
//read data into file in NSData format
NSData *filedata;
filedata = [file readDataToEndOfFile];
NSLog(#"fileDATA = %#", filedata);
//convert NSData to NSString
NSString *string;
string = [[NSString alloc] initWithData:filedata encoding:NSASCIIStringEncoding];
NSMutableString *directoryPath1 = [NSMutableString stringWithString: #"share1>"];
[directoryPath1 appendString: string];
NSData *dataToWrite3 = [directoryPath1 dataUsingEncoding: NSUTF8StringEncoding];
NSFileHandle* outputFile = [NSFileHandle fileHandleForWritingAtPath:#"/Users/yp/Downloads/a.txt"];
[outputFile seekToEndOfFile];
[outputFile writeData:dataToWrite3];
//convert from string to array
NSArray *lines = [string componentsSeparatedByString:#"="];
NSLog(#"arrau = %#", lines);
//take one of the string and store it in sword
NSString *sword = [lines objectAtIndex:1];
NSLog(#"port : %#", sword);
int port1=[sword intValue];
Communicator *c = [[Communicator alloc ]init];
c.host=#"http:127.0.0.1";
c.port=port1;
[c setup];
}
else
{
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:#"Error"];
[alert setInformativeText:#"You are not logged in.Kindly login to start performing the operations"];
[alert setAlertStyle:NSWarningAlertStyle];
[alert runModal];
}
}
The above code, on the action performed first tries to check if the file is present at the /var/folders/y3/jv117_75505fnk8htdrs0qm40000gr/T/com.aprivacy.xmlCorePort.properties location or not.
This works perfectly fine , If the file is present , it shows a popup alert (which happens).
But if the file is present , it goes inside the if condition and tries to read the file where it fails .It always prints a blank string showing that nothing is being read.
So then I went and checked the entitlements in App Sandbox.
I tried to add an entitlement named com.apple.security.temporary-exception.files.absolute-path.read-only with a string value set to /var/folders/y3/jv117_75505fnk8htdrs0qm40000gr/T/com.aprivacy.xmlCorePort.properties so that it gets the permission to read the file from this location but still it doesn't solve my problem.
Could anyone please suggest how to get this file reading permission accessible in my app because the same code works completely fine in a newly created test project.
Following steps : Original client app running -login with user name and password Once logged in -it writes the port in a file At the same time ,once you are logged in with your application , if now you right click on any file in your system you will see certain extra extensions like share ,grant access etc. (This is because a finder project used to add extensions is merged with the original client) Now when I click on say share (on right clicking a file) , I want an action to be performed.The logic for action is written in (IBAction)Share (id) sender method This app used to add extensions is sandboxed because of which the permissions are restricted. So while I clicked on share , my logic was to read that file ,get the port and then connect to server using that port. I want to do everything inside action but I am unable to do so . It is not able to find the file data from /var/folder/y3/jv117_755fdlvfldsvgr/T/com.aprivacy.xmlcorePort.properties
Sandboxed apps (all in iOS) are only allowed access to specific directories. Use NSSearchPathForDirectoriesInDomainsto obtain paths to available directories.
Ex:
Objective-C:
NSArray *documentDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
NSError *error;
BOOL status = [string writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (status == NSError) {
NSLog(#"error: %#", error)
}
Swift:
let filePath = "path/file.txt";
let documentDirectoryPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first as! String
let path = documentDirectoryPath + filePath
Note: Sandboxed paths is not consistent across clean builds.
Don't use absolute paths in sandboxed applications.
In OS X there is the NSTemporaryDirectory() function to have access to the temporary directory for this specific application in the container. Entitlements are not needed.
From the documentation
Some path-finding APIs (above the POSIX layer) refer to app-specific
locations outside of the user’s home directory. In a sandboxed app,
for example, the NSTemporaryDirectory function provides a path to a
directory that is outside of the user’s home directory but specific to
your app and within your sandbox; you have unrestricted read/write
access to it for the current user. The behavior of these path-finding
APIs is suitably adjusted for App Sandbox and no code change is
needed.
Source: App Sandbox in Depth

NSSavePanel not saving on desktop?

can i use NSSavePanel with a sandboxed OS X app to let user save on desktop? i gave user read/write entitlements for downloads and user selected folder, for some reason my app saves in downloads folder fine but when i change directory and select desktop it doesnt save at all.
here is the code am using for NSSavePanel
if([self.mActiveQRFileName isEqualToString:kQR_DEFAULT_FILE_NAME])
{
NSSavePanel *savePanel = [NSSavePanel savePanel];
//[savePanel setDirectoryURL:[NSURL URLWithString:[Utilities getQRDefaultDirectoryPath]]];
[savePanel setNameFieldStringValue:kQR_DEFAULT_FILE_NAME];
[savePanel beginSheetModalForWindow:[self window] completionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) {
NSString *qrFilePath = [NSString stringWithFormat:#"%#.%#",[[savePanel URL] path],kQR_FILE_EXT];
[qrd saveQRFile:qrFilePath];
self.mActiveQRFileName = [NSString stringWithString:qrFilePath];
blnChangesSaved = YES;
}
}];
}
else
{
[qrd saveQRFile:self.mActiveQRFileName];
blnChangesSaved = YES;
}
Any help would be greatly appreciated :)
A NSSavePanel will give you the user selected path for a file in it's URL property. The sandbox will only grant you access to this file, with the name specified by the user.
In your example code this line possibly modifies the selected path by giving it a different file extension:
NSString *qrFilePath = [NSString stringWithFormat:#"%#.%#",[[savePanel URL] path],kQR_FILE_EXT];
Which could result in a filename different from the originally selected file for which you don't have access in the sandbox. Try logging the qrFilePath and see if it still equals the path for the selected URL. Also check your sandbox exceptions to see what the exact error is.
If you want to restrict the NSSavePanel to let the user only specify files of a certain type use the setAllowedFileTypes: methos.
If you want the user to grant you access to a directory to write to where you can output any file, as opposed to a specific path: use a NSOpenPanel. This has the disadvantage that the user cannot specify a specific file name like in a NSSavePanel.

Global events, the Mac App Store, and the sandbox

I'm working on an app where using global key-down events will be a requirement for its operation. Additionally, I plan on distributing this strictly via the App Store. (It's a Mac app, not iOS.) I've gotten an example of listening for the global events working via addGlobalMonitorForEventsMatchingMask, but with caveats.
Note: I am making the choice to use the modern API's and not rely on the earlier Carbon hotkey methods. In the event that they are deprecated eventually, I don't want to have to figure this problem out later.
The principle issue is that the app has to be trusted in order for global events to be detected. Otherwise, accessibility has to be enabled for all apps. When I enable accessibility, events are detected successfully. This requirement is documented here, https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/MonitoringEvents/MonitoringEvents.html.
I would prefer that for my users, they will not have to enable accessibility. From other research I've done, you can get an application to be trusted by calling AXMakeProcessTrusted, then restarting the application.
In the code that I'm using, I do not get an authentication prompt. The app will restart, but is still not trusted (likely because I don't get an authentication prompt). Here's my code for this part:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
if (!AXAPIEnabled() && !AXIsProcessTrusted()) {
NSString *appPath = [[NSBundle mainBundle] bundlePath];
AXError error = AXMakeProcessTrusted( (CFStringRef)CFBridgingRetain(appPath) );
[self restartApp];
}
}
- (void)restartApp{
NSTask *task = [[NSTask alloc] init];
NSMutableArray *args = [NSMutableArray array];
[args addObject:#"-c"];
[args addObject:[NSString stringWithFormat:#"sleep %d; open \"%#\"", 3, [[NSBundle mainBundle] bundlePath]]];
[task setLaunchPath:#"/bin/sh"];
[task setArguments:args];
[task launch];
[NSApp terminate:nil];
}
Further, I've looked at the documentation for Authorization Service Tasks here https://developer.apple.com/library/archive/documentation/Security/Conceptual/authorization_concepts/03authtasks/authtasks.html#//apple_ref/doc/uid/TP30000995-CH206-BCIGAIAG.
The first thing that worries me that pops out is this info box, "Important The authorization services API is not supported within an app sandbox because it allows privilege escalation."
If this API is required to get the authentication prompt before restarting the app, it seems that I may not be able to get global events without the accessibility feature enabled.
In summary, my specific questions are:
Is there an error in my sample code about how to get the
authentication prompt to appear?
In order to get the authentication prompt to appear, am I required
to use the Authorization Services API?
Is it possible, or not possible, to have a sandboxed app that has
access to global events?
First of all, there is no way you can automatically allow an app to use accessibility API which would work in a sandbox environment and thus in app store. The recommended way is to simply guide users so they can easily enable it themselves. The new API call AXIsProcessTrustedWithOptions is exactly for that:
NSDictionary *options = #{(id) kAXTrustedCheckOptionPrompt : #YES};
AXIsProcessTrustedWithOptions((CFDictionaryRef) options);
Now, to your first and second question (just for the sake of completeness - again it won't work in sandbox):
The idea behind AXMakeProcessTrusted was that you actually create a new auxiliary application that you run as root from the main application. This utility then calls AXMakeProcessTrusted passing in the executable of the main application. Finally you have to restart the main app. The API call has been deprecated in OSX 10.9.
To spawn a new process as a root you have to use launchd using SMJobSubmit. This will prompt a user with an authentication prompt saying that an application is trying to install a helper tool and whether it should be allowed. Concretely:
+ (BOOL)makeTrustedWithError:(NSError **)error {
NSString *label = FMTStr(#"%#.%#", kShiftItAppBundleId, #"mktrusted");
NSString *command = [[NSBundle mainBundle] pathForAuxiliaryExecutable:#"mktrusted"];
AuthorizationItem authItem = {kSMRightModifySystemDaemons, 0, NULL, 0};
AuthorizationRights authRights = {1, &authItem};
AuthorizationFlags flags = kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights;
AuthorizationRef auth;
if (AuthorizationCreate(&authRights, kAuthorizationEmptyEnvironment, flags, &auth) == errAuthorizationSuccess) {
// this is actually important - if from any reason the job was not removed, it won't relaunch
// to check for the running jobs use: sudo launchctl list
// the sudo is important since this job runs under root
SMJobRemove(kSMDomainSystemLaunchd, (CFStringRef) label, auth, false, NULL);
// this is actually the launchd plist for a new process
// https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html#//apple_ref/doc/man/5/launchd.plist
NSDictionary *plist = #{
#"Label" : label,
#"RunAtLoad" : #YES,
#"ProgramArguments" : #[command],
#"Debug" : #YES
};
BOOL ret;
if (SMJobSubmit(kSMDomainSystemLaunchd, (CFDictionaryRef) plist, auth, (CFErrorRef *) error)) {
FMTLogDebug(#"Executed %#", command);
ret = YES;
} else {
FMTLogError(#"Failed to execute %# as priviledged process: %#", command, *error);
ret = NO;
}
// From whatever reason this did not work very well
// seems like it removed the job before it was executed
// SMJobRemove(kSMDomainSystemLaunchd, (CFStringRef) label, auth, false, NULL);
AuthorizationFree(auth, 0);
return ret;
} else {
FMTLogError(#"Unable to create authorization object");
return NO;
}
}
As for the restarting, this is usually done also using an external utility to which waits for a main application to finish and starts it again (by using PID). If you use sparkle framework you can reuse the existing one:
+ (void) relaunch {
NSString *relaunch = [[NSBundle bundleForClass:[SUUpdater class]] pathForResource:#"relaunch" ofType:#""];
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *pid = FMTStr(#"%d", [[NSProcessInfo processInfo] processIdentifier]);
[NSTask launchedTaskWithLaunchPath:relaunch arguments:#[path, pid]];
[NSApp terminate:self];
}
Another option is to hack the /Library/Application Support/com.apple.TCC/TCC.db sqlite database add the permissions manually using an auxiliary helper:
NSString *sqlite = #"/usr/bin/sqlite3";
NSString *sql = FMTStr(#"INSERT or REPLACE INTO access values ('kTCCServiceAccessibility', '%#', 1, 1, 1, NULL);", MY_BUNDLE_ID);
NSArray *args = #[#"/Library/Application Support/com.apple.TCC/TCC.db", sql];
NSTask *task = [NSTask launchedTaskWithLaunchPath:sqlite arguments:args];
[task waitUntilExit];
This however will disqualify the app from being app store. More over it is really just a hack and the db / schema can change any time. Some applications (e.g. Divvy.app used to do this) used this hack within the application installer post install script. This way prevents the dialog telling that an app is requesting to install an auxiliary tool.
Basically, MAS restrictions will require you to the route of having tge user turning on AX for all.
I found a potential solution on GitHub.
https://github.com/K8TIY/CW-Station
It has an auxiliary application which would be run at root to request access for the main application. It is a little outdated and is using some functions which have been deprecated so I am working on modernizing it. It looks like a good starting point.

fileExistsAtPath returns NO for a directory that exists

fileExistsAtPath is returning NO for a directory that exists. If I have the following code:
NSURL *newDirectoryPath = ... // path does not begin with a tilda
// and is created using URLByAppendingPathComponent:isDirectory:
BOOL directoryExists = [self.fileManager fileExistsAtPath:[newDirectoryPath absoluteString]];
if (NO == directoryExists)
{
BOOL ret = [self.fileManager moveItemAtURL:self.currentPresentationDirectoryPath toURL:newDirectoryPath error: &err];
// ret is YES, err is nil
directoryExists = [self.fileManager fileExistsAtPath:[newDirectoryPath absoluteString]];
}
Even though the directory has just been created successfully with moveItemAtURL, fileExistsAtPath is still returning NO.
I know the documentation says this:
Attempting to predicate behavior based on the current state of the
file system or a particular file on the file system is not
recommended. Doing so can cause odd behavior in the case of file
system race conditions.
But I want to understand what the issue is here - if I close the app and relaunch it then the first check for fileExistsAtPath in the code above is still returning NO, even though the directory was previously successfully created during the prior execution of the code, and I can see the directory in the Organizer, and I can also successfully read from the contents of the directory etc. etc.
P.S. is there no fileExistsAtURL: method?
If you have an NSURL, -absoluteURL won't return a usable path for NSFileManager. It will return the absolute URL with the file:// prefix. E.g.: file:///path/to/file.
Instead try to use an other method, like -path. Check if that works.
NSURL *myURL = /* some url */;
NSString *myPath;
BOOL exi;
myPath = [myURL path];
exi = [[NSFileManager defaultManager] fileExistsAtPath:myPath];
if(!exi) {
NSLog(#"File does not exist");
}