Global events, the Mac App Store, and the sandbox
Asked Answered
S

3

6

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:

  1. Is there an error in my sample code about how to get the authentication prompt to appear?

  2. In order to get the authentication prompt to appear, am I required to use the Authorization Services API?

  3. Is it possible, or not possible, to have a sandboxed app that has access to global events?

Sisson answered 14/9, 2012 at 7:30 Comment(3)
Did you manage to get around this? I have stumbled upon the very same problem :(Immortalize
Not directly. For the purpose of global hotkeys, I had to go the route of using Cocoa hotkeys.Sisson
I am also looking for a way to prompt the user. My guess is an installer would run as root with the necessary permissions when the app is being installed and call AXMakeProcessTrusted. If the installer is the Mac App Store there must be a hook to request this permission.Moot
F
5

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.

Fit answered 7/6, 2014 at 18:45 Comment(4)
Option number 2 (directly altering the user's privacy trust database) would almost certainly disqualify his app from the Mac App Store as it would constitute a using private API, modifying a system file, and breaking the application sandbox. Apple has also stated that they will eliminate the viability of this method if people start shipping apps that rely on it.Gena
@Gena - would you mind to give a link to the Apple statement?Fit
This was stated privately to a longtime Mac developer -- it's referenced on the Apple developer forums but I do not have a direct link.Gena
Well, it finally happened: the 10.12 GM now places TCC.db under System Integrity Protection (SIP) control via the "restricted" extended attribute. This prevents all non-Apple-signed apps from modifying TCC.db. It looks like Dropbox may have been the developer who finally forced Apple's hand.Gena
P
1

Basically, MAS restrictions will require you to the route of having tge user turning on AX for all.

Projective answered 4/8, 2013 at 9:0 Comment(0)
M
0

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.

Moot answered 4/8, 2013 at 3:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.