Privileged file copy in macOS (Installing a helper binary to /usr/local/bin) - swift

I have a helper binary mytool inside my main app bundle that I need to copy to /usr/local/bin.
Now bin might not always exist or have write access, so the standard NSWorkspace calls will fail on it. I looked into different ways to do this, but none are satisfactory (or I am doing it wrong)
Getting an authorization for replaceFile for NSWorkspace.requestAuthorization
This does not seem to work, as I still get a privileges error after trying to "replace" the file in /usr/local/bin/mytool with the one from my bundle.
Manually getting Authorization via AuthorizationCreate.
The problem here is that AuthorizationExecuteWithPrivileges is deprecated (or in my case not even available in Swift), and SMJobBless seems to be only for longer running helper processes. Also SMJobBlessrequires my helper tool to have an Info.plist of its own, which it doesn't have since its just a plain binary
So how do I manage to perform a privileged file copy in Swift?
PS: The app is not sandboxed, so NSOpenPanel does not help.

Well I dug out the deprecated API using dlsym, because there is simply no other way besides asking the user manually for his password, which I don't want to do unless the deprecated API disappears entirely.
So what I do now is authenticate a call to mytool --install using AuthorizationExecuteWithPrivileges like this:
import Foundation
import Security
public struct Sudo {
private typealias AuthorizationExecuteWithPrivilegesImpl = #convention(c) (
AuthorizationRef,
UnsafePointer<CChar>, // path
AuthorizationFlags,
UnsafePointer<UnsafeMutablePointer<CChar>?>, // args
UnsafeMutablePointer<UnsafeMutablePointer<FILE>>?
) -> OSStatus
/// This wraps the deprecated AuthorizationExecuteWithPrivileges
/// and makes it accessible by Swift
///
/// - Parameters:
/// - path: The executable path
/// - arguments: The executable arguments
/// - Returns: `errAuthorizationSuccess` or an error code
public static func run(path: String, arguments: [String]) -> Bool {
var authRef: AuthorizationRef!
var status = AuthorizationCreate(nil, nil, [], &authRef)
guard status == errAuthorizationSuccess else { return false }
defer { AuthorizationFree(authRef, [.destroyRights]) }
var item = kAuthorizationRightExecute.withCString { name in
AuthorizationItem(name: name, valueLength: 0, value: nil, flags: 0)
}
var rights = withUnsafeMutablePointer(to: &item) { ptr in
AuthorizationRights(count: 1, items: ptr)
}
status = AuthorizationCopyRights(authRef, &rights, nil, [.interactionAllowed, .preAuthorize, .extendRights], nil)
guard status == errAuthorizationSuccess else { return false }
status = executeWithPrivileges(authorization: authRef, path: path, arguments: arguments)
return status == errAuthorizationSuccess
}
private static func executeWithPrivileges(authorization: AuthorizationRef,
path: String,
arguments: [String]) -> OSStatus {
let RTLD_DEFAULT = dlopen(nil, RTLD_NOW)
guard let funcPtr = dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges") else { return -1 }
let args = arguments.map { strdup($0) }
defer { args.forEach { free($0) }}
let impl = unsafeBitCast(funcPtr, to: AuthorizationExecuteWithPrivilegesImpl.self)
return impl(authorization, path, [], args, nil)
}
}

If you want to do this using public APIs (meaning not using deprecated APIs, invoking Apple Script, shelling out via Process, etc.) then the only way to achieve this is using SMJobBless. For better or worse, that's the only option still officially supported by Apple.
If you want to install your binary in /usr/local/bin then that binary itself doesn't need to have an Info.plist. You'd want to create a different helper tool which would be installed via SMJobBless that could copy your binary to /usr/bin/local. It will be able to do that because a helper tool installed by SMJobBless always runs as root. Once you're done with all of this you could have the helper tool you installed with SMJobBless uninstall itself. No denying it's rather involved.
If you do want to go down this route, take a look at SwiftAuthorizationSample.

Related

Spawning a process in an app built with UIKit for macOS (Catalyst)

I'm building an application that shares most of the code between macOS and iOS versions (targeting macOS 11 and iOS 14). UIKit for Mac seems like a natural choice to help with this. Unfortunately, one of the libraries uses the Process type under the hood. Building it produces "Cannot find type Process in scope" error when a dependency on it is added and when targeting macOS. I'm fine with excluding this library for iOS, but I still need to link with it on macOS while keeping the ability to use UIKit on all platforms.
I've selected this library to be linked only for macOS in Xcode, but this has no effect and the same build error persists. Also, I'm getting this error without adding a single import SwiftLSPClient statement in the app, so I don't think conditional imports would help in this case.
What would be the best way to resolve this issue within the constraints listed above?
I created a LSPCatalyst class in my Mac Catalyst app to replace the MacOS LanguageServerProcessHost. To make that work, I replaced the process property with a processProxy that accesses the process instance in a MacOS bundle using the FoundationApp protocol as explained below.
Following #Adam's suggestion, I created a MacOS bundle to proxy for the process instance. You follow the same idea as he pointed to for AppKit access from Catalyst apps, but you just need Foundation to get access to Process. I called the bundle FoundationGlue and put everything in a FoundationGlue folder in my Xcode project. The bundle needs an Info.plist that identifies the principal class as "FoundationGlue.MacApp", and the MacApp.swift looks like:
import Foundation
class MacApp: NSObject, FoundationApp {
var process: Process!
var terminationObserver: NSObjectProtocol!
func initProcess(_ launchPath: String!, _ arguments: [String]?, _ environment: [String : String]?) {
process = Process()
process.launchPath = launchPath
process.arguments = arguments
process.environment = environment
}
func setTerminationCompletion(_ completion: (()->Void)!) {
let terminationCompletion = {
NotificationCenter.default.removeObserver(self.terminationObserver!)
completion?()
}
terminationObserver =
NotificationCenter.default.addObserver(
forName: Process.didTerminateNotification,
object: process,
queue: nil) { notification -> Void in
terminationCompletion()
}
}
func setupProcessPipes(_ stdin: Pipe!, _ stdout: Pipe!, _ stderr: Pipe!) {
process.standardInput = stdin
process.standardOutput = stdout
process.standardError = stderr
}
func launchProcess() {
process.launch()
print("Launched process \(process.processIdentifier)")
}
func terminateProcess() {
process.terminate()
}
func isRunningProcess() -> Bool {
return process.isRunning
}
}
The corresponding header I called FoundationApp.h looks like:
#import <Foundation/Foundation.h>
#protocol FoundationApp <NSObject>
typedef void (^terminationCompletion) ();
- (void)initProcess: (NSString *) launchPath :(NSArray<NSString *> *) arguments :(NSDictionary<NSString *, NSString *> *) environment;
- (void)setTerminationCompletion: (terminationCompletion) completion;
- (void)setupProcessPipes: (NSPipe *) stdin :(NSPipe *) stdout :(NSPipe *) stderr;
- (void)launchProcess;
- (void)terminateProcess;
- (bool)isRunningProcess;
#end
And the FoundationAppGlue-Bridging-Header.h just contains:
#import "FoundationApp.h"
Once you have the bundle built for MacOS, add it as a framework to your Mac Catalyst project. I created a Catalyst.swift in that project for access to the FoundationGlue bundle functionality::
import Foundation
#available(macCatalyst 13, *)
struct Catalyst {
/// Catalyst.foundation gives access to the Foundation functionality identified in FoundationApp.h and implemented in FoundationGlue/MacApp.swift
static var foundation: FoundationApp! {
let url = Bundle.main.builtInPlugInsURL?.appendingPathComponent("FoundationGlue.bundle")
let bundle = Bundle(path: url!.path)!
bundle.load()
let cls = bundle.principalClass as! NSObject.Type
return cls.init() as? FoundationApp
}
}
Then, you use it from your app like:
let foundationApp = Catalyst.foundation!
foundationApp.initProcess("/bin/sh", ["-c", "echo 1\nsleep 1\necho 2\nsleep 1\necho 3\nsleep 1\necho 4\nsleep 1\nexit\n"], nil)
foundationApp.setTerminationCompletion({print("terminated")})
foundationApp.launchProcess()
This is a messy solution but I know it works: Add a “Mac bundle” to your Catalyst app and import the MacOS-only framework with that.
Here’s a guide to creating and loading a Mac bundle: https://medium.com/better-programming/how-to-access-the-appkit-api-from-mac-catalyst-apps-2184527020b5
Once you have the bundle, you can add Mac-only libraries and frameworks to it. You’ll have to bridge data and method calls between the bundle and your iOS app, but it’s manageable.

How to get macOS to present a dropped .stl file as public.standard-tesselated-geometry-format?

I'm trying to add some drag-and-drop support to my SwiftUI macOS app to allow the user to drop .stl files onto the window. I’ve got the drop working, but I only seem to be able to validate against public.file-url, rather than the specific public.standard-tesselated-geometry-format (provided by ModelIO as kUTTypeStereolithography).
In my code, I do something like this:
func
validateDrop(info inInfo: DropInfo)
-> Bool
{
debugLog("STL: \(kUTTypeStereolithography)")
let has = inInfo.hasItemsConforming(to: [kUTTypeFileURL as String, kUTTypeData as String])
return has
}
func
performDrop(info inInfo: DropInfo)
-> Bool
{
inInfo.itemProviders(for: [kUTTypeFileURL as String, kUTTypeData as String, kUTTypeStereolithography]).forEach
{ inItem in
debugLog("UTIs: \(inItem.registeredTypeIdentifiers)")
inItem.loadItem(forTypeIdentifier: kUTTypeFileURL as String, options: nil)
{ (inURLData, inError) in
if let error = inError
{
debugLog("Error: \(error)")
return
}
if let urlData = inURLData as? Data,
let url = URL(dataRepresentation: urlData, relativeTo: nil)
{
debugLog("Dropped '\(url.path)'")
}
}
}
return true
}
This works well enough if I look for kUTTypeFileURL, but not if I look for kUTTypeStereolithography when I drop a .stl file onto the view.
I tried declaring it as an imported type but a) that didn’t seem to work and b) shouldn’t be necessary, since the existence of a symbolic constant implies macOS know about the type. I also don’t really know what it conforms to, although I tried both public.data and public.file-url.
I’d really like to be able to validate the type so that the user isn’t mislead by the drop UI. Currently, it indicates that my view will accept the drop if it’s not an .stl file, which I don’t want it to do.

filePromiseProvider writePromiseTo URL not working when dragging image to another application

I have File Promises implemented in a Cocoa app that allows dragging images from a view and dropping them to folders on the machine or to apps like preview or evernote. In the past this has worked well using the NSDraggingSource delegate and 'namesOfPromisedFilesDropped' method. This method would return the drop location and would allow me to write the image data directly there. So when dropping to an application icon like preview, or within an app like evernote, the file would be written and the app would either load or the image would simply show within.
Unfortunately this method was deprecated in 10.13 so it no longer gets called in new OS versions. Instead I've switched over to using the filePromiseProvider writePromiseTo url method of the "NSFilePromiseProviderDelegate" delegate. This method gets called, and the image data is processed. I get the destination URL and attempt to write the image data to this location. This works perfectly fine when simply dragging to folders on the Mac. But, when dragging to other app icons like Preview.app, or directly to a folder in Evernote, I get an error 'Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"'.
I've attempted this using full URLs, and, URL paths. Regardless of either, when dragging to other apps it simply will not allow the drop or creation of the file at that location.
Is there some entitlement that may be missing? I've even attempted the Apple source code found here with the same error: File Promises Source
Here is the code I'm using now that's returning the error with inability to write to outside app locations. This only works for dragging to folders on the computer.
extension DragDropContainerView: NSFilePromiseProviderDelegate {
internal func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
let fileName = NameFormatter.getFormattedNameFromDate() + "." + fileType.getFileType().typeIdentifier
return fileName
}
internal func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
return workQueue
}
internal func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
if let snapshot = filePromiseProvider.userInfo as? SnapshotItem {
if let data = snapshot.representationData {
do {
try data.write(to: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
}
}
Any help on this issue would be great. Ultimately, I simply want to be able to drag the image to an app and have that app accept the drag. This used to work but no longer does.
Update 1:
After extensive experimentation I've managed to find a 'solution' that works. I don't see why this works but some how this ultimately kicks off a flow that fires the old 'deprecated' method.
I create the dragged item;
let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardWriter(forImageCanvas: self))
draggingItem.setDraggingFrame(screenshotImageView.frame, contents: draggingImage)
beginDraggingSession(with: [draggingItem], event: event, source: self)
This calls the below method;
private func pasteboardWriter(forImageCanvas imageCanvas: DragDropContainerView) -> NSPasteboardWriting {
let provider = FilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: self)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
This sets up the custom file promise session using the subclass below. As you can see, I'm not handling any logic here and it seems to be doing very little or nothing. As an added test to verify it's not really doing much, I'm setting the pasteboard type to 'audio'. I'm dragging an image not audio but it still works.
public class FilePromiseProvider : NSFilePromiseProvider {
public override func writableTypes(for pasteboard: NSPasteboard)
-> [NSPasteboard.PasteboardType] {
return [kUTTypeAudio as NSPasteboard.PasteboardType]
}
public override func writingOptions(forType type: NSPasteboard.PasteboardType,
pasteboard: NSPasteboard)
-> NSPasteboard.WritingOptions {
return super.writingOptions(forType: type, pasteboard: pasteboard)
}
}
So long at the above methods are implemented it apparently kicks off a flow that ultimately calls the 'deprecated' method below. This method works every time perfectly in Mojave showing that there must be an issue with the NSFilePromises API. If this method works, the file promises API should work the same but it does not;
override func namesOfPromisedFilesDropped(atDestination dropDestination: URL) -> [String]?
Once this method gets called everything works perfectly in Mojave for drags to app icons in the dock and directly into applications like Evernote returning the app to 100% drag drop functionality as it used to work in previous OS versions!
My file promises delegate is still in place but looks like the below. As you can see it's not doing anything any longer. However it's still required.
extension DragDropContainerView: NSFilePromiseProviderDelegate {
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
return ""
}
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
}
}
Any comments on this 'solution' would be great. Any suggestions on how to do this better would also be welcomed. Please note, that according to Apple, there "is no guarantee that the file will be written in time" using the File Promises API. But with the above, the old deprecated method is somehow called in Mojave and works flawlessly every time.
Thanks

IBM Watson Visual Recognition API Key fail - Xcode Swift

I'm trying to connect to Watson using VisualRecognitionV3.framework.
The framework's compiled fine (via Carthage), and I think I've got the Service Credentials configured correctly, but when I compile Xcode to the simulator I get an invalid-api-key error.
Has anyone experienced the same issues?
What am I doing wrong?
private let apiKey = "Xn5DUtQU8WzgFTL9qNEFwBjxxxxxxxxxxxxxxxxxx"
private let classifierId = "DefaultCustomModel_2051029379"
private let version = "2018-07-01"
var visualRecognition: VisualRecognition!
override func viewDidLoad() {
super.viewDidLoad()
self.visualRecognition = VisualRecognition(apiKey: apiKey, version: version)
}
override func viewDidAppear(_ animated: Bool) {
let localModels = try? visualRecognition.listLocalModels()
if let models = localModels, models.contains(self.classifierId) {
print("local model found")
} else {
self.updateModel()
}
}
Xcode Error:
Error Domain=com.ibm.watson.developer-cloud.VisualRecognitionV3 Code=403 "ERROR: invalid-api-key" UserInfo={NSLocalizedDescription=ERROR: invalid-api-key}
Watson configuration screenshot:
Watson Config
You need to initialize your VisualRecognition using another initializer, like the following
let visualRecognition = VisualRecognition(version: version, apiKey: apiKey, iamUrl: nil)
The difference is that you will need to call this 3 argument-ed constructor with the third argument, i.e. the iamUrl, even though you make it nil and anything else is the same. This tells the VisualRecognition class to authenticate your app using the IAM.
The git documentation is very confusing, which says in here https://github.com/watson-developer-cloud/swift-sdk#visual-recognition
Note: a different initializer is used for authentication with instances created before May 23, 2018:
Which means the old way should just work, UNLESS, they made a mistake and they actually mean AFTER May 23, 2018
Anyways, if you try it should just work. I was having this issue 30 mins before writing this answer.

Class hierarchy - class should only called by another class

I try to implement a Security class and a Secret class. In my whole project the Secret class should only called by Security.getSecretInstance().doSomeSecretAction()
So Secret.doSomeSecretAction() should throw an compile error.
I need the Security.getSecretInstance() for an authentication process.
I'm searching for a good pattern or something else, but I think my searching keywords are too bad or my requirement is stupid/or not possible.
At the moment I call Security.getSecretInstance() it returns a singleton instance of Secret, but I could call Secret.doSomeSecretAction() too. There is no difference.
Do you have some pattern, keywords or snippets for me?
Edit
My definition of awesome would be that I have one method like this:
Security.isAuthorized { secret in
secret.doSomeSecretAction
}, failure {
print("permission denied")
}
And I can get secret only with this .isAuthorized-Method
What I would recommend doing is declare Secret nested inside Security, make Secret private and create non-private methods inside Security that can access Secret. Something like this:
class Security {
class func doSomeSecretAction() {
Secret.doSomeSecretAction()
}
private class Secret {
class func doSomeSecretAction(){
print("Private method called")
}
}
}
Security.doSomeSecretAction()
Here, Security.doSomeSecretAction() can be called from outside the Security class, but Secret.doSomeSecretAction() can only be called inside the Security class.
Update based on comments:
A feasible solution would be declaring the initializer of Security private, so it can only be called from inside the Security class and declaring a computed variable (for now I called it shared) which is the only access point to the initializer. This computed variable either returns nil or a new instance of the Secret class based on Security.isAuthorized. This way, every time a function of Secret is called, the authorisation status is checked and the function can only be called if the status is authorised, otherwise the shared variable returns nil and hence the method is not called.
class Security {
static var isAuthorized = false //change this when the authorisation status changes
class Secret {
static var shared: Secret? {
if Security.isAuthorized {
return Secret()
} else {
return nil
}
}
private init(){} //a new instance of Secret can only be created using the `shared` computed variable, the initializer cannot be called directly from outside the Secret class
func doSomeSecretAction(){
print("Private method called")
}
}
}
Security.Secret.shared //nil
//Security.Secret.init() //if you uncomment this line, you'll get an error saying all initializers are inaccessible
Security.Secret.shared?.doSomeSecretAction() //nil
Security.isAuthorized = true
Security.Secret.shared?.doSomeSecretAction() //function is called
Security.isAuthorized = false
Security.Secret.shared?.doSomeSecretAction() //nil
I was working on this answer while Dávid was editing his; I didn't realize he'd posted an update awhile ago. There's a lot of overlap in our answers, so this is just another style of the same approach.
First, I want to be clear that what you're describing can only implement encapsulation, not "security." I mean that you can build a system that makes it easy for developers to use it correctly and difficult to use it incorrectly. That's pretty straightforward. But you won't be able to stop a developer from extracting the secret and running any code they want. It's their machine and you're giving them the code. They can always run it. They have a debugger; you're not going to hide anything.
But, preventing accidental misuse is a fine goal, and pretty straightforward. The first thing is that you should work with instance methods, not class methods. Class methods makes all of this harder than it needs to be. A solution to your problem will look something like this, relying on fileprivate for most of the access control.
class Security {
enum Error: Swift.Error {
case unauthorized
}
// This feels like it should be nested in Security, but doesn't have to be
class Secret {
// No one outside this file can instantiate one of these. It's likely
// that you'll be passing some parameters here of course.
fileprivate init() {}
// I'm assuming you want these to be single use, so people can't store
// a reference to them an reuse them. This is one simple way.
fileprivate var isAuthorized = true
private func validate() {
// I'm treating this kind of reuse as a programming error and
// crashing. You could throw if you wanted, but it feels like it
// should never happen given your design.
guard isAuthorized else {
fatalError("Secrets can only be used once")
}
}
func doSomeSecretAction() {
// Every "protected" method (which may be all of them) needs to
// call validate() before running.
validate()
print("SECRET!")
}
}
// Public so we can test; obviously this would at least private(set)
var isAuthorized = false
func withAuthorization(execute: (Secret) -> Void) throws {
guard isAuthorized else { throw Error.unauthorized }
// We create a new Secret for every access and invalidate it after.
// That prevents them from being stored and reused.
let secret = Secret()
execute(secret)
secret.isAuthorized = false
}
}
// -- Some other file
let security = Security()
security.isAuthorized = true // For testing
var stealingTheSecret: Security.Secret?
do {
try security.withAuthorization {
$0.doSomeSecretAction() // This is good
stealingTheSecret = $0 // Try to steal it for later use
}
} catch Security.Error.unauthorized {
print("Unauthorized")
}
stealingTheSecret?.doSomeSecretAction() // Let's use it: Crash!
In principle you could get rid of the validate() boilerplate by allocating the memory for Secret directly with UnsafeMutablePointer and destroying it at the end, but this is probably more trouble than it's worth to avoid one extra line of code.
(Note that allocating the memory yourself still wouldn't protect you against the caller saving the object; they can always make a copy of the memory and re-instantiate it with .load; any unsafe thing you can do, so can the caller. This also allows them to circumvent validate() by directly modifying the boolean or copying the object before you invalidate it. There is no technique that will prevent unsafe memory access; this is why you cannot protect secrets inside code.)
After research I find a good and simple solution for me:
class SecurityLayer {
private static var authorized: Bool = false
static func makeAuthorizeCheck() -> API2? {
if authorized {
return API2()
}
return nil
}
}
Second class (not subclass)
class Secret {
func test() {
print("test")
}
fileprivate init() {
}
}
Examples
SecurityLayer.makeAuthorizeCheck()?.test() //working
Secret() //forbidden
Secret.test() //compiler find this method, but there are no permissions to use this one
When the constructor inside Secret is private this wouldn't work anymore. For me the benefit of fileprivate is obvious now.
!The classes have to be in one file!