OpenVPN Authentication Script

Edit May 3, 2018: Updated to use a GitHub gist to enable syntax highlighting and added a link to GitHub.

My work related to OpenVPN can be found at https://github.com/tidgubi/openvpn.

I’ve been working on updating my OpenVPN installation and decided I wanted to switch to username/password authentication, so I don’t have to deal with managing client certificates. Personally, I use head -c 32 /dev/urandom | base64 to generate my client passwords, so they are as strong as any certificate authentication. I wrote the following script to perform username/password authentication for OpenVPN using the auth-user-pass-verify option and allow easy management of accounts.

This script uses /etc/openvpn/shadow as its username/password repository. The shadow file must be readable by the OpenVPN process; however, I suggest it is not writable by the OpenVPN process. I have the owner = root and group = openvpn, and permissions 640.

The script takes at least 1 second to verify a password. Passwords are salted with a unique 16 byte value and hashed 100,000 times.

The script works to verify that only one password verification is performed at any time.

Avoid misuse from special characters by Base64 encoding all values in the shadow file.

Usage:

  • ./openvpn-auth.py <credential file> – how the script is invoked by OpenVPN to verify a username and password.
  • ./openvpn-auth.py -a <username> [<password>] – add or update <username>. You will be prompted for a password if you do not provide one on the command line.
  • ./openvpn-auth.py -g <username> [<password>] – generate a string that contians the encoded username, salt, and hashed password. This can be useful if you want to generate strings and add them to the script (list of strings in the shadow_contents variable).
  • ./openvpn-auth.py -d <username> – delete the credentials for <username>.
  • ./openvpn-auth.py -l – list the configured usernames.

openvpn-auth.py

#!/usr/bin/python

# Username/Password Authentication Script for use with OpenVPN
# Copyright (c) 2018 Kenji Yoshino https://www.tidgubi.com
# This script is released under the Version 3 of the GNU General Public
# License https://www.gnu.org/licenses/gpl-3.0.txt

import sys
import os
from hashlib import pbkdf2_hmac
from time import time, sleep
from binascii import b2a_base64, a2b_base64
from getpass import getpass

SHADOW_FILE="/etc/openvpn/shadow"
LOCK_FILE="/dev/shm/openvpn-auth-lock"
HASH="sha256"
ITTERATIONS=100000
BASE_TIME=1.0 # set a minimum time in seconds for check function
SALT_SIZE=16
MAX_UN_PW_LEN=512
MAX_SHADOW_FILE=16384
INVALID=1
VALID=0
# If SHADOW_FILE is None, shadow_contents can be configured as a list of strings
# using the format :: for each line. Each value is base64
# encoded. Strings can be generated using the -g option
shadow_contents=None

def usage():
   print "./openvpn-auth.py "
   print "./openvpn-auth.py -[g|a|d]  [password]"
   print "./openvpn-auth.py -l"

def getHash(salt, password):
   return b2a_base64(pbkdf2_hmac(HASH, password, salt, ITTERATIONS)).strip()

def check(pw_file):
   while os.path.isfile(LOCK_FILE):
      sleep(0.1)
   with open(LOCK_FILE, 'a'):
      start=time()
   rtn=INVALID

   try:
      with open(pw_file, 'r') as f:
         username=b2a_base64(f.readline(MAX_UN_PW_LEN).rstrip("\r\n")).strip() + ":"
         password=f.readline(MAX_UN_PW_LEN).rstrip("\r\n")
         if len(f.readline(MAX_UN_PW_LEN)) > 0:
            return INVALID
   except Exception as e:
      print e
      return INVALID

   try:
      if SHADOW_FILE is not None:
         with open(SHADOW_FILE, 'r') as f:
            shadow_contents=f.readlines(MAX_SHADOW_FILE)
      
      for line in shadow_contents:
         if line.startswith(username):
            parts=line.split(":")
            if len(parts) != 3:
               break
            password=getHash(a2b_base64(parts[1]),password)
            if password == parts[2].strip():
               rtn=VALID
            break
   except Exception as e:
      print e
      rtn=INVALID

   # make this function run in constant time
   t=BASE_TIME-(time()-start)
   if t > 0.0:
      sleep(t)
   
   for x in range(3): #try to remove the lock file 3 times
      try:
         os.remove(LOCK_FILE)
         break
      except Exception:
         continue
   
   return rtn

def delete(username):
   if not os.path.isfile(SHADOW_FILE):
      return
   username=b2a_base64(username).strip() + ":"
   with open(SHADOW_FILE, 'r') as f:
      shadow_contents=f.readlines(MAX_SHADOW_FILE)
   
   with open(SHADOW_FILE, 'w') as f:
      for line in shadow_contents:
         if not line.startswith(username):
            f.write(line);
      f.truncate()

def generate(username, password, add):
   start=time()
   while password is None:
      password=getpass("Enter a password for %s: " % username)
      verifypass=getpass("Verify password: ")
      if password != verifypass:
         print("Passwords do not match. Try again.\n")
         password=None
   salt=os.urandom(SALT_SIZE)
   encusername=b2a_base64(username).strip()
   password=getHash(salt,password)
   salt=b2a_base64(salt).strip()
   #TODO: warn the user if SHADOW_FILE is larger than max shawow length
   if add:
      delete(username)
      with open(SHADOW_FILE, 'a') as f:
         f.write("%s:%s:%s\n" % (encusername, salt, password))
   else:
      print("%f seconds to compute hash" % (time()-start))
      print("%s:%s:%s" % (encusername, salt, password))

def list():
   try:
      if SHADOW_FILE is not None:
         with open(SHADOW_FILE, 'r') as f:
            shadow_contents=f.readlines(MAX_SHADOW_FILE)
      
      for line in shadow_contents:
         parts=line.split(":")
         if len(parts) != 3:
            continue
         print(a2b_base64(parts[0]) + "\n")
   except Exception as e:
      print e

args=len(sys.argv)
if args == 2:
   if sys.argv[1] == "-l":
      list()
   else:
      sys.exit(check(sys.argv[1]))
elif args == 3 or args == 4:
   if sys.argv[1] == "-g" or sys.argv[1] == "-a":
      if sys.argv[1] == "-a" and SHADOW_FILE is None:
         print("Error: You must configure a SHADOW_FILE to use -a.\n")
      else:
         password=None
         if args == 4:
            password=sys.argv[3]
         generate(sys.argv[2], password, sys.argv[1] == "-a")
   elif sys.argv[1] == "-d":
      delete(sys.argv[2])
   else:
      usage()
else:
   usage()

sys.exit(0)

Garmin Forerunner 35 Review

With the loss of the ability to sync with macOS High Sierra and long GPS acquisition times, I figured it was finally time to replace my Garmin Forerunner 305. Since Fitbit is shutting down most Pebble services, I figured I might as well replace my Pebble Time as well. I settled on the Garmin Forerunner 35 as an inexpensive smartwatch and exercise GPS with heart rate monitoring.

After two weeks with the Forerunner 35, I’m satisfied but not thrilled. It’s convenient going to a single device; however, it isn’t as good of a smartwatch or running GPS as the dedicated devices it replaces.

Pros:

  • Great Battery Life – After 10 days and 3×30 minute runs using GPS, the battery is still reading 25%
  • Fast GPS acquisition – When I step outside it’s usually less than 10 seconds to acquire GPS
  • Easy synchronization – synchronizing activities with a phone over Bluetooth is much more convenient than a USB connection

Cons:

  • Limited (almost no) customization:
    • Watch Face – The watch face only shows the battery, time and date. It cannot be customized (other than converting it from digital to analog)
    • Widgets – information screens (notifications, steps, weather, etc.) cannot be reordered
    • Notifications – There is no way to filter notifications. Selecting specific apps to show up on my Pebble was one of my favorite features
    • Exercise information – There are 4 screens of 3 fields for each. Fortunately these can be configured; however, they are not labeled. If you don’t have all screens configured, you still have to scroll through blank screens
  • Controls – Everything is controlled through the 4 physical buttons. There is only a down button for scrolling, so you have to scroll through everything if you hit down an extra time
  • Vibrator – I hear the vibration more than I feel it. I think it sounds cheap

Enabling the Google Advanced Protection Program with iOS

Required

Setup

  1. Plug the Bluetooth bridge into your computer. Wait for the Yellow LED to stop blinking. If it does not stop blinking (indicating a problem installing the drivers), try a different computer
  2. In Chrome, add the DIGIPASS SecureClick Manager app to Chrome
  3. Launch DIGIPASS SecureClick Manager and click "Add SecureClick"
  4. Follow the instructions and enter PIN "000000" when prompted
  5. Enable the Advanced Protection Program and pair your U2F tokens. The initial setup only allows the configuration of two tokens, but additional tokens can be added the normal way (My AccountSign-in & security → 2-Step Verification → ADD SECURITY KEY)
  6. On your iOS device, launch the Smart Lock App
  7. Login to your Google account
  8. When prompted to pair your security key, hold down the on the SecureClick until the LEDs flash red, select SClick U2F in Smart Lock, and enter PIN "000000".
  9. Your iOS device is now authenticated and your SecureClick is now paired as a BLE device
  10. Open your Google apps, and enable the account you authenticated in Smart Lock

Notes

You do not need to install the DIGIPASS Secure Click Manager app on your iOS devices. The Google Smart Lock app will handle the pairing.

Enabling Advanced Proteciton clears you 2 Step Verificaiotn methods, so you must re-enroll andy additional security keys.

The only feature I miss is contact syncing.

I had problems using the Bluetooth bridge on my macOS computers for initial setup, but it has started working on High Sierra (10.13.2) and El Capitan (10.11.6).

Picking a Rewards Network Dining Partner

Since all it takes is registering a credit card, it makes sense to sign up for the Rewards Network dining rewards program. The only question is which partner to sign up with.

Earning Rates / $
Program Member Online Member VIP Member Notes / Recommendations
Alaska Mileage Plan 0.5 miles 3 miles 5 miles info 12 dines to VIP status
Good value
American Airlines AAdvantage 1 miles 3 miles 5 miles info 12 dines to VIP status
Good value
Delta SkyMiles 0.5 miles 3 miles 5 miles info 12 dines to VIP status
Good value
jetBlue TrueBlue 1 point
Mosaic: 2 points
info jetBlue Mosaic status is unrelated to the number of dines
Not worth it
Southwest RapidRewards .5 points 3 points info
Good value if you don’t dine out often (< 12 times / year) and fly Southwest
FREE SPIRIT 0.5 miles 3 miles 5 miles info 12 dines to VIP status
Good value
United MileagePlus 0.5 miles 3 miles 5 miles info 12 dines to VIP status
Good value
Hilton Honors 2 points 5 points 8 points info 11 dines to VIP status
Good value
IHG Rewards Club 1 point 5 points 8 points info 12 dines to Elite/VIP status
Good value
iDine 5% 10% info earn 10% after spending $750
Best value if you spend > $750 / year
eScript 5% Good value
Shell Fuel Rewards 10 cents / gallon / $50 Not worth it
Orbitz Rewards 5% info
Good value
Club O 5% info
Good value
Sears Shop Your Way 7% Good value
Caesars Total Rewards 1 credit
2 credits
info Requires Total Rewards Visa to earn 2 credits
Not worth it
U Promise up to 5% no details for earning rates available
Good value

All programs do not use the Member, Online Member, or VIP Member statuses; however these are the most common terms and the earnings are placed in the closest category.

Which program works best for you depends on which loyalty programs you belong to and how much you value the various rewards currencies. If you’re not a frequent traveler, at least signing up for the iDine program makes sense.