It was another classic scripting task: put together a friendly little one-dialog app to run three simple tests on user keychains, and almost everything went swimmingly. I then got tripped up by a combination of bugs in the command that I was calling, and Swift’s handling of strings.
Drawing out the single dialog window, wiring it up, and adding the Swift 3 code to do two of the three tasks took less than half an hour before they were working fine. As in my previous scripting work, KeychainCheck is driven from a single button. When the user clicks on it, three commands are run, and their results are written to the editable text boxes. For the first two commands
security list-keychains -d user
and
security default-keychain -d user
this was straightforward, using my doShellScript()
function for each, and inserting the resulting text in its respective text box.
It is important to note that the call to run the command, here wrapped in doShellScript()
, takes the parameters to the command as an array of strings, with each separable component as a separate string, e.g.
var theListPar = ["list-keychains", "-d", "user"]
and not
var theListPar = ["list-keychains", "-d user"]
which will fail.
The problem came with the third, which should have worked just the same
security show-keychain-info [path]
except that [path]
had to be the path to the default login keychain. My intention was to use the result from the second command as the path, but that was when things started to go wrong. For a worryingly long time, I could not get any result in the standard output from the command, and all that I could see in Xcode was an error being returned, with a code (50) which was meaningless in this context.
Even when I fed the command with my hand-crafted path, which was fully expanded and completely correct, I still couldn’t get any standard output from it. It then occurred to me that the result was being returned not in standard output, but even when there was no error, it was coming back in the standard error stream.
I then rolled a version of my function doShellScript()
which handled standard error, doShellScriptErr()
, and the command started to work at last. Not everything, though, because there was also a problem with the path that I was deriving from the standard output of the previous command. The path itself was good, but was prefaced by blank spaces and surrounded by double quotes, and that was unacceptable to the security
command.
My next plan was to use the really neat String.trimmingCharacters(in: CharacterSet)
call, first to strip leading (and trailing) spaces, then to do the same for the quotes, using code like
let trimmedString = string.trimmingCharacters(in: .whitespaces)
and
let trimmedString = string.trimmingCharacters(in: ["\""])
But that failed to strip the trailing quotes, and even when I just chopped those off, the shell command continued to throw errors.
At one stage, I realised that I could call this via AppleScript, using my function to call a script with privileges, but without having to invoke those. Because the command returned its result as standard error, that didn’t work either.
One rediscovery that I made was that the security command behaves quite differently to parameters passed through my Swift function, from the way it behaves when used in Terminal. In my app, I could elicit a correct result if I passed the full Posix pathname, as for a shell command of
security show-keychain-info /Users/hoakley/Library/Keychains/login.keychain-db
but if I passed it the abbreviated version of
security show-keychain-info ~/Library/Keychains/login.keychain-db
it consistently threw an error, although entered in Terminal that form works fine.
This demonstrates how parameters which are passed are not expanded in the way that they are in a shell, or in AppleScript – another important lesson which should of course have occurred to me earlier. But there are also apparently unpredictable and intractable issues in passing what should be fully expanded Posix paths, which I have not really got to the bottom of.
So one particular verb, show-keychain-info
, of the security
command has idiosyncracies in dealing with its parameters, and it passes output to standard error rather than standard output. That’s not good.
I was verging on abandoning the idea of using Swift, and reverting to AppleScript, which would have been a serious admission of failure. I decided then to try to construct what should be the complete path to the default login keychain, using NSFileManager. Inevitably this has now changed name to FileManager, but this is a useful snippet for the future:
let fileManager = FileManager.default
let path = fileManager.homeDirectoryForCurrentUser.path + theFolderPath
which turns a path within the user’s Home folder of theFolderPath
into the full Posix path.
Swift and the macOS libraries are full of such cherries that make coding quick and powerful. Unfortunately there are also plenty of snurgly areas, which once again were turning what should have been a little task into a marathon.
My completed code reads:
This is everything that I might wish for in a scripting language, even if at times I have to be a bit devious to arrive at a functional solution. The code overhead for dealing with the bugs in the command tool and the mismatch between some Swift string functions and that command is small. But the time wasted identifying and working around those bugs is unacceptable.
Here for the record is my final working code in usable form:
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var theSecCmd = "/usr/bin/security"
var theListPar = ["list-keychains", "-d", "user"]
var theDefPar = ["default-keychain", "-d", "user"]
var theInfoPar = "show-keychain-info"
var theFolderPath = "/Library/Keychains/login.keychain-db"
@IBOutlet weak var window: NSWindow!
@IBOutlet weak var theKeychList: NSTextField!
@IBOutlet weak var theLoginKeych: NSTextField!
@IBOutlet weak var theLoginKeychLock: NSTextField!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
@IBAction func theCheckButtonClick(_ sender: Any) {
let status1 = doShellScript(launchPath: theSecCmd, arguments: theListPar)
let status2 = doShellScript(launchPath: theSecCmd, arguments: theDefPar)
let fileManager = FileManager.default
let path = fileManager.homeDirectoryForCurrentUser.path + theFolderPath
let status3 = doShellScriptErr(launchPath: theSecCmd, arguments: [theInfoPar, path])
theKeychList.stringValue = status1[0] + status1[1]
theLoginKeych.stringValue = status2[0] + status2[1]
theLoginKeychLock.stringValue = status3[0] + status3[1]
}
}