More Secure Passwords In AppleScript
Sometimes an Applescript might require user authentication. For example, this will happen if using do shell script to run a UNIX command that needs administrator access:
do shell script "chown -R Vickash " & quoted form of "/Users/Vickash/Desktop/My Files" with administrator privileges
A dialog box appears prompting the user for their password before the shell script executes. This is fine if your script is always run manually by the user. However, for Folder Actions, or any type of script that runs on a trigger, this can be a problem. What if the user isn’t at the machine when the script is triggered? And if the script runs frequently, do you really want to ask the user for a password each and every time?
If you’ve come across this problem, you know that one possible solution is to put the login credentials inline. However, the security problem here is obvious; anyone looking at the source code has the user’s login credentials:
do shell script "chown -R Vickash " & quoted form of "/Users/Vickash/Desktop/My Files" user name "Vickash" password "this is a test" with administrator privileges
Ideally, the password would be stored elsewhere, in an encrypted form, and only accessed by the script when necessary. Luckily, OS X provides an app called Keychain Access (check your Utilities folder) for storing credentials in encrypted form.
Using Keychains
Your default keychain is at ~/Library/Keychains/login.keychain, and holds things like Safari’s saved passwords. It’s protected with your login password by default. To keep things clean, I recommend creating a new keychain specifically for storing generic password items that you want Applescript to access. Here’s how:
1) Open Keychain Access.app and go to File > New Keychain… You can name the keychain whatever you want and store it wherever you want, but take note of it’s full path. You’ll need that for the script to access it later.
2) When creating a new keychain, you’ll need to set a password. This is separate from the credentials you want to save within the keychain; it’s a password that restricts access to the keychain itself. Think of it like a LastPass or 1Password master password. This is the password your user will need to type the first time the script runs to allow the script access to the keychain and the credentials within.
3) Once the keychain is created, create a generic password item by clicking on the “add” button below the empty list. Put in the credentials that you would have typed inline before, and give the item a name. I’ve called it “Admin” here. It doesn’t matter much what you call it, but you’ll need to know the name for the script to find the right item.
By doing this, you’ve now used the keychain’s encryption to protect the username and password that would have been typed as plain text in the script before. Now we need to set the script up to access the keychain.
4) To make things even more convenient I recommend changing the keychain’s settings so that it doesn’t automatically lock on sleep or after a period of inactivity. You can adjust this to suit your needs, but the idea is that the user allows the script access to the keychain once, the first time the script runs, and then doesn’t need to again until the user logs out.
Accessing the Keychain from AppleScript
We’ve solved half the problem. The password is stored in an encrypted form on disk rather than in plaintext. But now we need some way to pull those credentials out of the keychain and use them in our script.
Apple used to provide a standard extension for this. You’d call tell application "Keychain Scripting" and be able to access the keychains, but I’m not sure that’s still exists in Lion, and it had major performance issues before that.
Daniel Jalkut at Red Sweater Software has written a tiny scriptable app called “Usable Keychain Scripting” for Lion and Pre-Lion systems. It’s free, I’ve used it, and it works great. If you don’t have a problem with installing something extra to get your scripts to run, head over to his blog and check it out.
If you’d like your scripts to be dependent only on the keychain, like I needed on a couple occasions, I’ve come up with a pure AppleScript workaround. On OS X there’s a shell command called security that can be used from the terminal to access keychains.
You can read more about the security command here, but the main thing we’re interested in is its find-generic-password option. When using this option and passing in the keychain path, and keychain item name we’re searching for, it returns the full details for the password item as text.
Running
security 2>&1 find-generic-password -gs "Admin" "/Users/Vickash/Library/Keychains/ScriptingDemo.keychain"
Returns
keychain: "/Users/Vickash/Library/Keychains/ScriptingDemo.keychain"
class: "genp"
attributes:
0x00000007 <blob>="Admin"
0x00000008 <blob>=<NULL>
"acct"<blob>="Vickash"
"cdat"<timedate>=0x32303132303131353136343231355A00 "20120115164215Z00"
"crtr"<uint32>=<NULL>
"cusi"<sint32>=<NULL>
"desc"<blob>=<NULL>
"gena"<blob>=<NULL>
"icmt"<blob>=<NULL>
"invi"<sint32>=<NULL>
"mdat"<timedate>=0x32303132303131353136343231355A00 "20120115164215Z00"
"nega"<sint32>=<NULL>
"prot"<blob>=<NULL>
"scrp"<sint32>=<NULL>
"svce"<blob>="Admin"
"type"<uint32>=<NULL>
password: "this is a test"
Now, it’s obvious that the only two lines we care about are "acct"<blob>="Vickash" and password: "this is a test". By executing the shell command via AppleScript, and writing a separate subroutine to extract the data we need, I’ve come up with the following:
on extractData(theText, theFieldName, theEndDelimiter, spaces)
set theDataStart to the offset of theFieldName in theText
if theDataStart = 0 then
return ""
else
set theDataStart to theDataStart + (length of theFieldName) + spaces
set theData to text theDataStart through end of theText
set theDataEnd to ((offset of theEndDelimiter in theData) - 1)
set theData to text 1 through theDataEnd of theData
end if
end extractData
on getCredentials of theKeychainItem from theKeychain
set theKeychainPath to (POSIX path of theKeychain) as text
try
set theKeychainAlias to (POSIX file theKeychainPath) as alias
set theKeychainPath to (POSIX path of theKeychainAlias)
on error
return "The keychain file was not found at the specified location: " & theKeychain as text
end try
try
set theResult to do shell script "security 2>&1 find-generic-password -gs " & quoted form of theKeychainItem & " " & quoted form of theKeychainPath
set theAccount to extractData(theResult, "\"acct\"<blob>=\"", "\"", 0)
set thePassword to extractData(theResult, "password: \"", "\"", 0)
return {account:theAccount, password:thePassword}
on error
return "The generic password item was not found in the keychain. Please verify its name: " & theKeychainItem
end try
end getCredentials
Practical Use
Copy these subroutines into your script. Now, whenever your script script needs to get credentials from a keychain, do something like:
set theCredentials to getCredentials of "Admin" from "/Users/Vickash/Library/Keychains/ScriptingDemo.keychain"
This will return an AppleScript record, in my case:
theCredentials {account: "Vickash", password: "this is a test"}
Now that the script has the login credentials stored in a record, I can execute the command that would have required manual authentication like:
do shell script "chown -R Vickash " & quoted form of "/Users/Vickash/Desktop/My Files" user name (account of theCredentials) password (password of theCredentials) with administrator privileges
Which is much more secure than:
do shell script "chown -R Vickash " & quoted form of "/Users/Vickash/Desktop/My Files" user name "Vickash" password "this is a test" with administrator privileges
Notes
Keep in mind that the first time the script runs it will prompt you for the keychain password to unlock it. Put the password in and click “Always Allow” to give the script continuous access to the keychain. If you’ve used the settings I recommended, the keychain won’t relock until it’s manually relocked via the Keychain Access app, or until the user logs out.
The getCredentials handler returns text, rather than a record, on error, so you can write your script to catch this and display a dialog box.


