More Secure Passwords In Applescript
Sometimes an Applecript needs authentication. For example, this happens if you use do shell script
to run a UNIX command with administrator priveleges:
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 runs. This is fine if your Applescript is always manually run by the user. But 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 every time?
If you’ve come across this problem before, you know that one 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, encrypted, and only accessed by the script when necessary. Luckily, OS X provides an application just for this. It’s called Keychain Access, and is located in the Utilities subfolder.
Keychains
Keychain Access lets you create collections of credentials called ‘keychains’, which are stored encrypted on disk. OS X makes one for you by default. It’s stored at ~/Library/Keychains/login.keychain, and is protected with your login password. It’s used for things like saved passwords in Safari, SSH keys, and anything an app want’s to keep encrypted on disk.
Setup A Keychain For Applescript
To keep things clean, I recommend creating a new keychain specifically for storing generic password items that you want to work with in Applescript. Here’s how:
Open Keychain Access.app and go to File > New Keychain… You can name the keychain anything and store it anywhere, but take note of its full path. You’ll need that for the script to access it later.
2) You’ll need to set a password for the keychain. This is separate from the credentials you want to store inside the keychain; it just restricts access to the keychain itself. Think of it like a LastPass or 1Password master password.
3) Once the keychain is ready, 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 in the script before, and give the item a name. I’ve called it “Admin” here. It doesn’t matter what you call it, but meaningful is better, and you’ll need to know the name for the script to find the right item later.
4) To make things more convenient for the user, I recommend changing the keychain’s settings so it doesn’t automatically lock on sleep or after a period of inactivity. You can adjust this to your needs, but the idea is that the user will grant Applescript access to the keychain once, and then doesn’t need to do so again until the next time he logs in.
Access Keychain Data 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 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 not have external dependencies, like I’ve needed on a couple occasions, this won’t work. I’ve come up with a pure AppleScript workaround. On OS X there’s a command line tool security
that can be used from the terminal to access keychains.
You can read more about security
here, but we’re mainly interested in is its find-generic-password
option.
If I run:
security 2>&1 find-generic-password -gs \
"Admin" "/Users/Vickash/Library/Keychains/ScriptingDemo.keychain"
It returns:
keychain: "/Users/Vickash/Library/Keychains/ScriptingDemo.keychain"
class: "genp"
attributes:
0x00000007 <blob>="Admin"
0x00000008 <blob>=<NULL>
"acct"<blob>="Vickash"
"cdat"<timedate>=0x32303132303131353136343231355A00 "20120115164215Z\000"
"crtr"<uint32>=<NULL>
"cusi"<sint32>=<NULL>
"desc"<blob>=<NULL>
"gena"<blob>=<NULL>
"icmt"<blob>=<NULL>
"invi"<sint32>=<NULL>
"mdat"<timedate>=0x32303132303131353136343231355A00 "20120115164215Z\000"
"nega"<sint32>=<NULL>
"prot"<blob>=<NULL>
"scrp"<sint32>=<NULL>
"svce"<blob>="Admin"
"type"<uint32>=<NULL>
password: "this is a test"
It’s obvious that the only lines we need are "acct"<blob>="Vickash"
and password: "this is a test"
. By executing the shell command via AppleScript, and writing a separate handler 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 "Keychain file not found at specified location: " & ¬
theKeychain as text
end try
try
set theResult to do shell script ¬
"security 2>&1 find-generic-password -gs " & ¬
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 "Generic password item specifeid does not exist in keychain: " & ¬
theKeychainItem
end try
end getCredentials
You can simply copy and paste these handlers 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 is equal to {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
-
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 access to the keychain until it relocks itself, or you log out. Click “Allow” if you want to be prompted to unlock the keychain each time the script runs.
-
If you’ve used the settings I recommended, the keychain won’t relock unless it’s manually relocked via the Keychain Access app, or until you log 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.