Two Factor Authentication in OS X

Yet another take from aMacosxhints article.

This is a hacked up two factor authentication scheme for Mac OS X that relies on an external storage device like and iPod. You could also use a usb keychain, or anything that will mount. Used in combination with Filevault, this can provide a reasonable amount of security on your machine that requires the presence of your device.

The article on Macosxhints describes using a script to make sure a folder exists on your device. You call this script from a login hook which either prevents login if the folder does not exist, or allows it if it does.

I’d like to instead create a random data seed, and check it’s hash on the client computer to make sure it’s the same file. Plug in your ipod or usb keychain and open terminal. Type in the following:

dd if=/dev/random bs=1k count=1024 of=/Volumes/ipod/.login/randomseed

Make sure to change the ipod in the above to whatever you are calling your device. This will create a 1MB file filled with random data. It dumps it directly into your device in a hidden folder called .login. Unless someone got their hands on this file, it’s extremely unlikely they would be able to duplicate it. Next we need to pull the hash value into a file on our machine:

MD5 -q /Volumes/ipod/.login/randomseed > ~/.login/hash

This will give you an MD5 hash in a hidden folder in your home directory that looks something like this:

2b913b93be5f241f7416b673c85b6641

Now that we have our random file on our ipod and a copy of the hash in our home folder, we can get to the fun stuff.

First we need to make sure that drives are auto mounted even when a user is not logged in:

sudo defaults write /Library/Preferences/SystemConfiguration/autodiskmount AutomountDisksWithoutUserLogin -bool true

Otherwise, external drives are not mounted until a user logs in, and that defeats the purpose of our script. You’ll have to restart for this to take effect.

Now we have to adapt the script from Macosxhints to check for the hash instead of a folder. Here’s the script.

#!/bin/sh
devicehash=`/sbin/md5 -q /Volumes/iPod/.login/randomseed`
storedhash=`/bin/cat /Users/$1/.hash`
if [ "$devicehash" == "$storedhash" ];
then
exit 0
else
/usr/bin/killall -HUP loginwindow
fi

What this does is compare the hash of the randomseed file on the ipod to the stored hash on the hard drive. Since this is a loginhook script, we can’t usekill -9 -1as loginhooks run as root andkill -9 -1does bad things when it’s run as root (check the man page). I use a killall instead. Long and short of it is, if the hashes match, then you log in, if they don’t, you get the login screen again.

I saved this script in /Library/Hooks/login/myuser/login.sh and did achown root login.shandchmod 700 login.shto change the owner to root and give only root permission to access the script.

Then you simply type this into the terminal:

sudo Defaults write com.apple.loginwindow LoginHook /path/to/script

If for some reason you can no longer login after this, say there’s a typo in the script, boot into single user mode by holding down Command and S. When you get to the prompt, type:

mount -uw /
defaults delete com.apple.loginwindow LoginHook

Update

I have tried my best to find a way to make this work with fast user switching, and there is a limited way to do it. When you switch users from the fast user switching menu, the system writes a userid key to /System/Library/Preferences/com.apple.loginwindow.plist. We can use to see who is currently logged in. The problem is, when you chose a user from the login screen i.e. what we do when we have CGSession suspend our login, it doesn’t update this plist. For a suspend script to work as a cron job, or even a background job while we’re logged in, it has to know that we have control over the GUI at that moment, otherwise it will just indiscriminately suspend the current session regardless of who is running it.

Of course, if you were willing to be vigilant about saving your documents, you could make a script just kill the processes belonging to your user id. This will leave any other logged in users alone, but will kick you rather abruptly if your authentication device is not present. It wouldn’t be much different from the previous script:

#!/bin/sh
devicehash=`/sbin/md5 -q /Volumes/iPod/.login/randomseed`
storedhash=`/bin/cat ~/.hash`
pid=`ps -x | grep -w loginwindow | grep -v grep | sed -e ‘s/^ *//’ -e ‘s/ .*//’`

if [