I’ve been occasionally using a VPN that requires a Google Authenticator code to connect. I say “occasionally” because it’s a pain to use — I have to launch Tunnelblick (the VPN client I’m using on my Mac), then get the VPN password out of my password manager and paste it in, then open my phone, launch Google Authenticator, and enter the displayed tokencode next to my password.
It’s not horrible — but it’s awkward enough that I find myself looking for ways to avoid using this particular connection. Then the other day, a co-worker suggested using a script to dump the credentials into the VPN config on the fly and re-launch. And so, my lunchtime project was decided for me.
Tunnelblick won’t let you write credentials to the configuration file, but it will happily pull them from the OS X Keychain. So now I just need to find a library to write to the keychain. Turns out that’s easy, too — there’s a command at
/usr/bin/security that does exactly what I need. Now I just need to make it look pretty.
And that’s what I’ve got now: “
gtb” — Google(auth) + TunnelBlick. This script:
- For setup:
- Prompts you for your base VPN password and Google Authenticator Key
- Writes them to the keychain
- For normal use:
- Reads the password and key from the keychain
- Computes the current Google Authenticator tokencode
to the keychain entry Tunnelblick uses for the VPN password
- Launches Tunnelblick and opens the selected VPN
Delighted (or maniacal, your choice) laughter is left as the responsibility of the user.
How does it work? Well (after setup) the first thing we need to do is read the data from the keychain. For a Tunnelblick VPN called “MyVPN” this can be done with the aforementioned
$ /usr/bin/security find-generic-password -gs Tunnelblick-Auth-MyVPN -a auth-data keychain: "/Users/dschuetz/Library/Keychains/login.keychain" class: "genp" attributes: 0x00000007 <blob>="Tunnelblick-Auth-MyVPN" 0x00000008 <blob>=<NULL> "acct"<blob>="auth-data" "cdat"<timedate>=0x32303134303533303134313430355A00 "20140530141405Z\000" "crtr"<uint32>=<NULL> "cusi"<sint32>=<NULL> "desc"<blob>=<NULL> "gena"<blob>=<NULL> "icmt"<blob>=<NULL> "invi"<sint32>=<NULL> "mdat"<timedate>=0x32303134303533303134313834385A00 "20140530141848Z\000" "nega"<sint32>=<NULL> "prot"<blob>=<NULL> "scrp"<sint32>=<NULL> "svce"<blob>="Tunnelblick-Auth-MyVPN" "type"<uint32>=<NULL> password: "GOOGLEAUTHKEYGOESHERE:myVPNPassword" $
The important bit is the last line, where the “secret” stored in the keychain entry lives (it’s always called “password,” no matter what you store there). My script reads that entry, splits on the ‘:’ into google_key and vpn_password fields, and then goes on to compute the tokencode.
Google Authenticator uses a system called the Time-based One-time Password Algorithm, or TOTP for short. Basically, it:
- Treats the key string as a Base-32 string and decodes it into a binary key
- Computes the current timestamp, based on the UNIX epoch, to a 30-second accurracy (that is, the timestamp number will increment by 1 every 30 seconds).
- Computes the HMAC-SHA1 of (key, timestamp)
- Reads the last byte of the resultant HMAC digest, and uses the lowermost 4 bits of that character to select an index into the digest
- Reads the four bytes from the digest, beginning with the index, as the base for the token
- Strips the most-significant bit off that 4-byte word and reduces the result (using modulo arithmetic) to a 6-digit number
- Returns the number (padded with leading zeroes if necessary)
One interesting problem I ran into: The Google key was 22 letters long (after stripping spaces). All the examples I could find online showed only 16 letter keys, but clearly this longer key worked fine in the iPhone application. However, it wouldn’t work in my script — I kept getting errors in the Base-32 decoding. Padding the string with additional “A”s worked — so my script takes the provided key string and tries that until it produces a valid Base-32 decoding.
There’s a great library that does all this on github (thanks, Tom Jaskowski), but I simply extracted the part that I cared about and incorporated it directly into the script. Here’s what it looks like in Python:
def get_totp_token(key_str): key = base64.b32decode(key_str) # the authentication key num = int(time.time()) // 30 # epoch time to 30 sec msg = struct.pack('>Q', num) # pack into a binary thing # take a SHA1 HMAC of key and binary-packed time value digest = hmac.new(key, msg, hashlib.sha1).digest() # last 4 bits of the digest tells us which 4 bytes to use offset = ord(digest) & 15 token_base = digest[offset : offset+4] # unpack that into an integer and strip it down token_val = struct.unpack('>I', token_base) & 0x7fffffff token = token_val % 1000000 return "%06d" % token # pad with leading zeroes
Once the tokencode has been computed, it’s appended to the base password, and written back into the keychain using security:
/usr/bin/security add-generic-password -U -s Tunnelblick-Auth-MyVPN -a password -w myVPNPassword123456
Then we use a little Applescript magic to launch the right VPN connection in Tunnelblick:
echo 'Tell app "Tunnelblick" to connect "MyVPN"' | osascript
…and that’s it!
Security of Keychain Items
A quick note on the keychain items themselves. The security application, by default, can access these items without prompting the user for their password. This means that, if you leave your desktop unlocked, anyone could walk up and extract the VPN credentials with a couple quick command line calls. So it’s best to open up the Keychain Access application, find the VPN’s “password” and “auth-data” entries, and secure them. Do this by removing the “security” application from the list of apps which can access the data at all times. (Leave Tunnelblick authorized to read the ‘password’ entry so it can launch more smoothly). You’ll also want to set the “Ask for Keychain Password” flag. Then, when you run the script, the Keychain will prompt you for your login password (to access the auth-data entry), and after that everything will happen magically without further intervention.
If you’d prefer to not store the Google Authenticator credentials in the keychain (but rely on the Google Auth app on your phone, for example), then enter “none” when settng up the Google Authenticator key, and the script will prompt for the current tokencode when it is run. Otherwise, technically, we’re kind of eliminating the “2” from “2-Factor” authentication.
I’ve posted the entire script as a gist on Github. I hope people find it useful, but remember, this is a hack written over lunch one day (and cleaned up during free moments over a couple days following). It might work perfectly for you. It might not work at all.
The basic concept should be applicable to other situations — I’d bet it could be changed to work with Viscosity (or other Mac VPN clients), or with other OTP codes, with minimal effort. But I use Tunnelblick, so that’s all it supports at the moment.