How to use Keychain to store Perl variables
Fri, Sep 26 2008 at 7:30AM PDT • Contributed by: kroko
Suppose You want to write a Perl script that takes advantage of Keychain Access as a place to read settings from. It can be done. But what about manipulating those Keychain data? AppleScript can do that. As a generic example, "MyProgram" uses Keychain for:
- storing service name for MyProgram (which is actually the real identity for the program, while the system call security uses -s as an argument)
- username for MyProgram
- password for MyProgram
- URI for MyProgram
- runstate for MyProgram
The corresponding Keychain Access key fields would be:
- Name for RUNSTATE
- Account for PROGRAMID
- Where for SERVICENAME
- Comments for PROGRAMURI
- Password for PROGRAMPW
Read on for the AppleScript part for MyProgram, where you set up the field values, start or stop the program, etc. -- kind of the GUI side of the program -- as well as additional AppleScript and perl examples.
[
robg adds: This is a long, detailed hint that was
first posted on the author's blog. I'm recreating it here in case the original ever vanishes. You can read the fully entry here or on the author's blog.]
Here's the setup AppleScript:
(* MyProgram © kroko GNU GPL*)
global servvicename
set servvicename to "MyProgram"
if getrun() is servvicename then
-- we need to constantly call AppleScript Runner to activate, because user can
-- focus on other windows during this script exec proces
-- (this is a trimmed script from that I wrote as personalized iTunes script plugin- there calling
-- AppleScript Runner to activate was critical for itunes not to get in front of this window)
tell application "AppleScript Runner"
activate
display dialog servvicename & " is running" with title ¬
servvicename buttons {"Preferences", "Good to know", "Stop"} default button 2 with icon 1
end tell
if the button returned of the result is "Stop" then
setrun(servvicename & "Stopped")
else if button returned of the result is "Preferences" then
setprefs()
end if
else if getrun() is (servvicename & "Stopped") then
tell application "AppleScript Runner"
activate
display dialog servvicename & " is stopped" with title ¬
servvicename buttons {"Preferences", "Good to know", "Start"} default button 2 with icon 1
end tell
if the button returned of the result is "Start" then
setrun(servvicename)
else if button returned of the result is "Preferences" then
setprefs()
end if
else if getrun() is "cannotgetrun" then
tell application "AppleScript Runner"
activate
display dialog "Could't read " & servvicename & " Preferences.
Have You set them up?" with title ¬
servvicename buttons {"*censored* this", "Edit Preferences"} default button 2 with icon 0
end tell
if button returned of the result is "Edit Preferences" then
setprefs()
end if
else
tell application "AppleScript Runner"
activate
display dialog "Unknown error!
Couldn't talk to " & servvicename with title servvicename buttons {"Damn!"} default button 1 with icon 2
end tell
end if
on getrun()
tell application "Keychain Scripting"
try
set myKey to first key of current keychain ¬
whose service is servvicename
return name of myKey
on error
return "cannotgetrun"
end try
end tell
end getrun
on setrun(runstate)
tell application "Keychain Scripting"
try
set myKey to first key of current keychain ¬
whose service is servvicename
set programid to account of myKey
set programuri to comment of myKey
set programpw to password of myKey
delay 1
delete myKey
delay 1
make new generic key with properties {name:runstate, kind:"application password", account:programid, service:servvicename, comment:programuri, password:programpw}
on error
tell application "AppleScript Runner"
activate
display dialog "Error!" with title servvicename buttons {"Damn!"} default button 1 with icon 2
end tell
end try
end tell
end setrun
on setprefs()
tell application "AppleScript Runner"
activate
display dialog "Here You can (re)set " & servvicename & " account data " with title ¬
servvicename buttons {"Exit", "Continue"} default button 2 with icon 1
end tell
if the button returned of the result is "Continue" then
tell application "AppleScript Runner"
activate
display dialog "Enter new ID" with title ¬
servvicename default answer ""
end tell
set programid to (text returned of result)
tell application "AppleScript Runner"
activate
display dialog "Enter new Password" with title ¬
servvicename default answer "" with hidden answer
end tell
set programpw to (text returned of result)
tell application "AppleScript Runner"
activate
display dialog "Enter new URI" with title ¬
servvicename default answer "http://www.foo.com/"
end tell
set programuri to (text returned of result)
tell application "AppleScript Runner"
activate
display dialog "Do You want to start " & servvicename & "?" with title ¬
servvicename buttons {"No", "Yes"} default button 2 with icon 1
end tell
if the button returned of the result is "Yes" then
set runstate to servvicename
else
set runstate to (servvicename & "Stopped")
end if
tell application "Keychain Scripting"
try
set myKey to first key of current keychain ¬
whose service is servvicename
try
delete myKey
delay 0.2
make new generic key with properties {name:runstate, kind:"application password", account:programid, service:servvicename, comment:programuri, password:programpw}
on error
tell application "AppleScript Runner"
activate
display dialog "Error!" with title servvicename buttons {"Damn!"} default button 1 with icon 2
end tell
end try
on error
-- This will exec on "Could't read MyProgram Preferences", because set myKey has already failed once on getrun()
try
make new generic key with properties {name:runstate, kind:"application password", account:programid, service:servvicename, comment:programuri, password:programpw}
end try
end try
end tell
else
quit me
end if
end setprefs
Here is the Perl script that reads values stored in Keychain and uses them in the program (here the usage is simple printout of those values).
#!/usr/bin/perl
# MyProgram © kroko GNU GLP
use strict;
use warnings;
use diagnostics;
use vars qw { $PROGRAMID $PROGRAMPW $RUNSTATE $PROGRAMURI $SERVICENAME };
$PROGRAMID = "";
$PROGRAMPW = "";
$RUNSTATE = "";
$PROGRAMURI = "";
$SERVICENAME = "MyProgram";
if (getuserinfo() == -3) {
print("Exiting ${SERVICENAME}. Key does not exist.\n");
exit; }
elsif (getuserinfo() == -2) {
print("Exiting ${SERVICENAME}. You have set up ID, PASSWORD, URI incorectly; reconfigure!\n");
exit; }
elsif (getuserinfo() == -1) {
print("Keychain info read successfully!\n${SERVICENAME} is stopped.\n");
exit; }
# The program comes below
# Here simply print out all the info stored in Keychain
print "Keychain info read successfully!\n";
print "Program - ${SERVICENAME}, ID - ${PROGRAMID}, URI - ${PROGRAMURI}, PASSWORD - ${PROGRAMPW}, Program running.\n";
exit;
# Get user info from keychain, check it, get the runstate
sub getuserinfo {
my(@alluserdata);
# Read the MyProgram from Keychain Access, redirect all stderr to stdout
open(USERINFO, "security 2>&1 find-generic-password -g -s $SERVICENAME |") || return -3;
@alluserdata = <USERINFO>;
close(USERINFO);
# If MyProgram key does not exsist return -3
if ($alluserdata[0] =~ m/SecKeychainFindGenericPassword/i) {return -3;}
# Read out the needed info
$alluserdata[0] =~ /^password: "(.*)"$/ ;
$PROGRAMPW = $1;
$alluserdata[4] =~ /0x00000007 <blob>="${SERVICENAME}(.*)"/ ;
$RUNSTATE = $1;
$alluserdata[6] =~ /"acct"<blob>="(.*)"/ ;
$PROGRAMID = $1;
$alluserdata[12] =~ /"icmt"<blob>="(.*)"/ ;
$PROGRAMURI = $1;
# Check if the fields follow the format rules rules, else return -2
if ($PROGRAMID !~ /^([A-Za-z0-9\_]*)$/i || $PROGRAMPW !~ /^([A-Za-z0-9\_]*)$/i || $PROGRAMURI !~ /^([A-Za-z0-9\.\/\-\~\:\_]*)$/i) {return -2;}
# Check if the Runstate is running eles return -1
if ($RUNSTATE eq "Stopped" || $RUNSTATE ne "") {return -1;}
return 0;
}
Note that the AppleScript actually doesn't start or stop the Perl script. It just writes the RUNSTATE into Keychain. Gluing the two parts together can be accomplished in different ways. For me, the Perl script is a launchd job, thus the solution is to restart the corresponding launchd job after changing MyProgram preferences in Keychain.
Configure com.foo.MyProgram.plist file as follows and put under one of the launchd-monitored places; I chose ~/Library/LaunchAgents:
And the modified AppleScript simply restarts the launchd job - meaning it will restart Perl script to read the new preferences:
(* MyProgram © kroko GNU GPL*)
global servvicename
set servvicename to "MyProgram"
if getrun() is servvicename then
-- we need to constantly call AppleScript Runner to activate, because user can
-- focus on other windows during this script exec proces
-- (this is a trimmed script from that I wrote as personalized iTunes script plugin- there calling
-- AppleScript Runner to activate was critical for itunes not to get in front of this window)
tell application "AppleScript Runner"
activate
display dialog servvicename & " is running" with title ¬
servvicename buttons {"Preferences", "Good to know", "Stop"} default button 2 with icon 1
end tell
if the button returned of the result is "Stop" then
setrun(servvicename & "Stopped")
else if button returned of the result is "Preferences" then
setprefs()
end if
else if getrun() is (servvicename & "Stopped") then
tell application "AppleScript Runner"
activate
display dialog servvicename & " is stopped" with title ¬
servvicename buttons {"Preferences", "Good to know", "Start"} default button 2 with icon 1
end tell
if the button returned of the result is "Start" then
setrun(servvicename)
else if button returned of the result is "Preferences" then
setprefs()
end if
else if getrun() is "cannotgetrun" then
tell application "AppleScript Runner"
activate
display dialog "Could't read " & servvicename & " Preferences.
Have You set them up?" with title ¬
servvicename buttons {"*censored* this", "Edit Preferences"} default button 2 with icon 0
end tell
if button returned of the result is "Edit Preferences" then
setprefs()
end if
else
tell application "AppleScript Runner"
activate
display dialog "Unknown error!
Couldn't talk to " & servvicename with title servvicename buttons {"Damn!"} default button 1 with icon 2
end tell
end if
on getrun()
tell application "Keychain Scripting"
try
set myKey to first key of current keychain ¬
whose service is servvicename
return name of myKey
on error
return "cannotgetrun"
end try
end tell
end getrun
on setrun(runstate)
tell application "Keychain Scripting"
try
set myKey to first key of current keychain ¬
whose service is servvicename
set programid to account of myKey
set programuri to comment of myKey
set programpw to password of myKey
delay 1
delete myKey
delay 1
make new generic key with properties {name:runstate, kind:"application password", account:programid, service:servvicename, comment:programuri, password:programpw}
try
do shell script "launchctl stop com.foo.MyProgram && launchctl start com.foo.MyProgram"
tell application "AppleScript Runner"
activate
display dialog "DONE!
" & servvicename & " will restart in new state
after 30 seconds" with title servvicename buttons {"Yeah!"} default button 1 with icon 1
end tell
on error
do shell script "launchctl start com.foo.MyProgram"
tell application "AppleScript Runner"
activate
display dialog "DONE! (But with unknown error)
" & servvicename & " will restart in new state
after 30 seconds" with title servvicename buttons {"Yeah!"} default button 1 with icon 1
end tell
end try
on error
tell application "AppleScript Runner"
activate
display dialog "Error!" with title servvicename buttons {"Damn!"} default button 1 with icon 2
end tell
end try
end tell
end setrun
on setprefs()
tell application "AppleScript Runner"
activate
display dialog "Here You can (re)set " & servvicename & " account data " with title ¬
servvicename buttons {"Exit", "Continue"} default button 2 with icon 1
end tell
if the button returned of the result is "Continue" then
tell application "AppleScript Runner"
activate
display dialog "Enter new ID" with title ¬
servvicename default answer ""
end tell
set programid to (text returned of result)
tell application "AppleScript Runner"
activate
display dialog "Enter new Password" with title ¬
servvicename default answer "" with hidden answer
end tell
set programpw to (text returned of result)
tell application "AppleScript Runner"
activate
display dialog "Enter new URI" with title ¬
servvicename default answer "http://www.foo.com/"
end tell
set programuri to (text returned of result)
tell application "AppleScript Runner"
activate
display dialog "Do You want to start " & servvicename & "?" with title ¬
servvicename buttons {"No", "Yes"} default button 2 with icon 1
end tell
if the button returned of the result is "Yes" then
set runstate to servvicename
else
set runstate to (servvicename & "Stopped")
end if
tell application "Keychain Scripting"
try
set myKey to first key of current keychain ¬
whose service is servvicename
try
delete myKey
delay 0.2
make new generic key with properties {name:runstate, kind:"application password", account:programid, service:servvicename, comment:programuri, password:programpw}
try
do shell script "launchctl stop com.foo.MyProgram && launchctl start com.foo.MyProgram"
tell application "AppleScript Runner"
activate
display dialog "DONE!
" & servvicename & " will restart in new state
after 30 seconds" with title servvicename buttons {"Yeah!"} default button 1 with icon 1
end tell
on error
do shell script "launchctl start com.foo.MyProgram"
tell application "AppleScript Runner"
activate
display dialog "DONE! (But with unknown error)
" & servvicename & " will restart in new state
after 30 seconds" with title servvicename buttons {"Yeah!"} default button 1 with icon 1
end tell
end try
on error
tell application "AppleScript Runner"
activate
display dialog "Error!" with title servvicename buttons {"Damn!"} default button 1 with icon 2
end tell
end try
on error
-- This will exec on "Could't read MyProgram Preferences", because set myKey has already failed once on getrun()
try
make new generic key with properties {name:runstate, kind:"application password", account:programid, service:servvicename, comment:programuri, password:programpw}
try
do shell script "launchctl stop com.foo.MyProgram && launchctl start com.foo.MyProgram"
tell application "AppleScript Runner"
activate
display dialog "DONE!
" & servvicename & " will restart in new state
after 30 seconds" with title servvicename buttons {"Yeah!"} default button 1 with icon 1
end tell
on error
do shell script "launchctl start com.foo.MyProgram"
tell application "AppleScript Runner"
activate
display dialog "DONE! (But with unknown error)
" & servvicename & " will restart in new state
after 30 seconds" with title servvicename buttons {"Yeah!"} default button 1 with icon 1
end tell
end try
end try
end try
end tell
else
quit me
end if
end setprefs
Hopefully it's apparent that the fields can contain
anything that your Perl script uses. Store whatever varibles you want. And each key field can contain many paremeters, divided by some predefined char/string; reading them out in Perl script simply takes some modification the regex part. i.e., we have a key that's
Service Name whose value is "MyProgram," and we want to store all the variables for Perl to use in the
comments field, each value divided by colon (valueone:valuetwo:valuethree:valuefour:valuefive):
Then the possible Perl script would be:
Finally, some basic AppleScripts to work with Keychain Access:
[
robg adds: Any errors in the above are my fault; please check the author's blog if this version doesn't work.]