3

For a open source project we want to create a OS X Uninstaller, which should be able to delete user and system domain files. The goal is to write the Uninstaller in Swift only.

During my research I found examples using NSTask to send a rm command, or others recommend using SMJobBless and a HelperTool for authentication. Both seems for me not the right way. I'm new to the OS X development, so it's hard for me to say which is the right way to achive the task.

I'm looking for an example in Swift 2 which allows user to authenticate and delete user and system domain files. If any one colud help me, I would really appreciate it.

arkord
  • 201
  • 1
  • 12
  • 2
    i suggest the [Authorization Service Programming Guide](https://developer.apple.com/library/mac/documentation/Security/Conceptual/authorization_concepts/01introduction/introduction.html#//apple_ref/doc/uid/TP30000995-CH204-TP1). Also, be aware if El Capitan's rootless mode: you can't delete certain system files even if the users have allowed you root access – Code Different Apr 30 '16 at 17:25
  • Thanks, I will read the guide and try it. Any practical examples are still welcome. – arkord May 01 '16 at 16:32

1 Answers1

1

After a couple hours playing around with SecurityFoundation, I found it to be too complex for my taste. Plus, the documentation is outdated. Inspired by this question. You can do it via AppleScript.

You need to divide your uninstaller into 2 parts: a default target to delete user files and a helper to delete system files (with escalated privileges of course).


Option 1

You can write that helper as a shell script and include that as a resource:

File delete_system_file.sh:

#!/bin/bash
rm -f /usr/local/dummy.txt

In your app:

let helper = NSBundle.mainBundle().pathForResource("delete_system_file", ofType: "sh")!
let script = "do shell script \"\(helper)\" with administrator privileges"

if let appleScript = NSAppleScript(source: script) {
    var error: NSDictionary? = nil

    appleScript.executeAndReturnError(&error)
    if error != nil {
        print(error!)
    } else {
        print("Deleted /usr/local/dummy.txt")
    }
} else {
    print("Cannot create the AppleScript object")
}

Option 2

You can do this entirely in Swift by adding a new target to your project. Assuming your first target is called MyUninstaller.

Add a new target

  1. Click File > New > Target...
  2. Under OS X, select Application > Command Line Tool
  3. Give your new target a name, say DeleteSystemFile

In the Project Navigator on the left, expand the DeleteSystemFile group and click on the main.swift file. To delete the dummy.txt file:

do {
    try NSFileManager.defaultManager().removeItemAtPath("/usr/local/dummy.txt")
} catch {
    print(error)
}

Back to the first target

Now go back to the first target (MyUninstaller) and add DeleteSystemFile as a Target Dependancy.

enter image description here

You can run the second target with escalated privileges by calling it through AppleScript:

let helper = NSBundle.mainBundle().pathForAuxiliaryExecutable("DeleteSystemFile")!
let script = "do shell script \"\(helper)\" with administrator privileges"

if let appleScript = NSAppleScript(source: script) {
    var error: NSDictionary? = nil

    appleScript.executeAndReturnError(&error)
    if error != nil {
        print(error!)
    } else {
        print("Deleted /usr/local/dummy.txt")
    }
} else {
    print("Cannot create the AppleScript object")
}

Option 3

Use SMJobBless, which is the Apple's recommended way of running privileged helper:

import SecurityFoundation
import ServiceManagement

// 1. Obtain an Authorization Reference
// You can do this at the beginning of the app. It has no extra rights until later
var authRef: AuthorizationRef = nil

let status = AuthorizationCreate(nil, nil, [.Defaults], &authRef)

// There's really no reason for this to fail, but we should check or completeness
guard status == errAuthorizationSuccess else {
    fatalError("Cannot create AuthorizationRef: \(status)")
}

// 2. Ask user for admin privilege
var authItem = AuthorizationItem(name: kSMRightBlessPrivilegedHelper, valueLength: 0, value: nil, flags: 0)
var authRights = AuthorizationRights(count: 1, items: &authItem)
let flags: AuthorizationFlags = [.Defaults, .InteractionAllowed, .ExtendRights]

let status2 = AuthorizationCopyRights(authRef, &authRights, nil, flags, nil)
if status2 != errAuthorizationSuccess {
    // Can't obtain admin privilege, handle error
    print("Cannot obtain admin privilege")
}

// 3. Run the privileged helper
// This label must be globally unique and matches the product name of your helper
let label = "com.myCompany.myApp.myAppPrivilgedHelper"
var error: CFError? = nil

let result = withUnsafeMutablePointer(&error) {
    SMJobBless(kSMDomainSystemLaunchd, label, authRef, UnsafeMutablePointer($0))
}

if !result {
    print(error!)
}

// 4. Release the Authorization Reference
AuthorizationFree(authRef, [.Defaults])

This involves some setup, which you can read about in the documentation for SMJobBless. There's also a sample project in ObjC.

Disclaimer: I could not test this all the way as I don't have a personal signing key. I could do away with section 2 from above, but included it here for completeness--that's how the sample project does it.

Community
  • 1
  • 1
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • Thank you for your effort! I had already tested this method successfuly, but forget to mention it. Sorry for that. My co-developers mentioned that there might be some limitations using this approach within an signed app. We would also like to create a 'custom uninstaller', so the user can select the components he wants to uninstall. I was thinking more of a combination of using `AuthorizationRights / AuthorizationCopyRights` and the `NSFileManager` to delete the files. Do you know if that is even possible? – arkord May 02 '16 at 18:22
  • If you have a signing certificate, you can use `SMJobBless`, which is the Apple-blessed way to do this. See Option 3 in my edited answer. – Code Different May 02 '16 at 23:59