GPG-Based Password Wallet

by Carl Welch

Like many Internet addicts, I have way too many user name/password accounts to remember: accounts on social-networking sites, rarely used logins at work, on-line banking and so on. One solution to this problem is to use the same user name and password everywhere, but that's clearly not safe; if people get a hold of your account information in one place, they own all your other accounts too.

I wanted a relatively safe, flexible and easy way to store passwords and other useful confidential information. I also wanted it to be easily accessible, which meant that I'd like to get at it over a text-only SSH connection. And, I wanted it to be something that could move around from machine to machine without too much trouble.

A few months ago, I saw an article by Duane Odom on linux.ocm about a shell script that uses GPG to encrypt and decrypt a text file containing the user's list of passwords (or any kind of text). I liked this approach, as it met the following requirements:

  1. It stores passwords in a well-encrypted text file (protected by a master password). The text file could contain anything and be formatted any way I want.

  2. The entire interface is text (an ncurses password interface, followed by less or a text editor like vim), so you can access it over a nongraphical SSH session (see the Accessing Your Password Wallet from the Computer at Your Friend's House sidebar).

  3. The script is built on standard utilities common to most Linux distributions (gpg and dialog).

Accessing Your Password Wallet from the Computer at Your Friend's House

One of things that has made wallet useful to me is the ability to reach it from anywhere. Here are a few hints for enabling SSH access to your broadband-connected Linux box at home.

Rather than try to memorize your computer's IP address (which may change unexpectedly anyway), you could sign up for a free dynamic DNS service like DynDNS. This lets you choose a memorable hostname for your computer, something like carlslinuxbox.dyndns.org. Some of these services want you to update your DNS record periodically (I think dyndns.org wants that to happen at least once a month). Rather than do that by hand, you can run an auto-updater (like inadyn) in cron. Be careful when setting the update frequency—some dynamic DNS services suspend your account if you update too often (read the fine print).

If you're going to let the Internet talk to SSH on your Linux box, there are a few things you can do to make that a bit more secure. I recommend disabling the PermitRootLogin option in the sshd_config file. You also may want to run SSH on a nonstandard port using the Port option in sshd_config. If the script kiddies find SSH running on port 22, they'll throw a bunch of user names and passwords at it trying to break in. Running SSH on a port other than 22 discourages this sort of thing to a large degree. And, make sure your firewall allows access to whatever port you use. Finally, if your computer is behind a consumer cable/DSL router, you'll have to configure the router to forward SSH traffic to your Linux box.

With those things done, next time you're at a friend's house, you could jump on a computer, download an SSH client (such as putty) and SSH to your Linux box (remembering to tell the SSH client your dynamic DNS hostname and the port number on which you're running SSH).

Although I liked the way the original script worked, I wanted to add several features. So I made some alterations to the original, and the result is shown in Listing 1.

Listing 1. wallet Script


  1  #!/bin/bash
  2
  3  . ~/bin/functions
  4  is_installed gpg
  5  is_installed dialog
  6  is_installed mktemp
  7  is_installed basename
  8
  9  if [ -f ~/.walletrc ]; then
 10      . ~/.walletrc
 11  fi
 12
 13  if [ -z $VISUAL ]; then
 14      VISUAL=vi
 15  fi
 16
 17  EDIT_PWFILE=0
 18  while getopts 'ec:' OPTION
 19  do
 20    case $OPTION in
 21      e) EDIT_PWFILE=1;;
 22      c) WALLET_FILENAME="$OPTARG";;
 23      ?) printf "usage: %s [ -e ] [ -c encrypted_file ]\n" \
 24          $( basename $0 ) >&2
 25         exit 1
 26         ;;
 27    esac
 28  done
 29  shift $(($OPTIND - 1))
 30
 31  if [ -z "$WALLET_FILENAME" ]; then
 32      echo "need the encrypted file specified by WALLET_FILENAME"
 33      echo "(in ~/.walletrc or the envariable) or with the -c option"
 34      exit 2
 35  fi
 36
 37  if [ ! -f $WALLET_FILENAME ]; then
 38      echo "$WALLET_FILENAME doesn't exist--attempting to create..."
 39      echo "(you'll need to give gpg a master password)"
 40      mkdir -p $( dirname $WALLET_FILENAME )
 41      TEMPFILE=$( mktemp /tmp/wallet.XXXXXX )
 42      gpg -c -o $WALLET_FILENAME $TEMPFILE
 43      rm -f $TEMPFILE
 44      EDIT_PWFILE=1
 45  fi
 46
 47  if [ $EDIT_PWFILE -eq 1 ]; then
 48      is_installed $VISUAL
 49  fi
 50
 51  # prompt the user for the password
 52  PASSWORD=$( dialog --stdout --backtitle "Password Wallet" \
 53      --title "Master Password" --clear --passwordbox \
 54      "Please provide the master password." 8 40 )
 55  if [ $? -ne 0 ]; then
 56      echo "Failed to acquire master password"
 57      exit 4
 58  fi
 59  if [ -z $PASSWORD ]; then
 60      echo "Password is required"
 61      exit 8
 62  fi
 63
 64  # if we're not editing the file, just display it and quit
 65  if [ $EDIT_PWFILE -eq 0 ]; then
 66      echo $PASSWORD | gpg --decrypt --passphrase-fd 0 \
 67          $WALLET_FILENAME | less
 68      clear
 69      exit 0
 70  fi
 71
 72  # set up the directory in which the unencrypted wallet file
 73  # will be edited
 74  TMPDIR=$( mktemp -d /tmp/wallet.XXXXXX )
 75  CLEARTEXT_WALLET_FILENAME=$TMPDIR/wallet
 76
 77  # try to ensure that cleartext wallet file is deleted, 
 78  # even after unexpected terminations
 79  trap "{ rm -rf $TMPDIR; }" 0 1 2 5 15
 80
 81  # decrypt the password wallet--an error here probably means 
 82  # the user typed the wrong password to decrypt the wallet
 83  echo $PASSWORD | gpg -o $CLEARTEXT_WALLET_FILENAME \
 84      --passphrase-fd 0 \
 85      $WALLET_FILENAME &> /dev/null
 86  case $? in
 87      0)
 88          # decryption succeeded, so open the wallet in the editor 
 89          # and then re-encrypt it when the editor closes
 90          mv $WALLET_FILENAME ${WALLET_FILENAME}.bak
 91          $VISUAL $CLEARTEXT_WALLET_FILENAME 2> /dev/null
 92          echo $PASSWORD | gpg -c -o $WALLET_FILENAME \
 93              --passphrase-fd 0 \
 94              $CLEARTEXT_WALLET_FILENAME &> /dev/null
 95          if [ $? -eq 0 ]; then
 96              clear
 97          else
 98              LAST_RESORT_FILENAME=$( mktemp ~/wallet.XXXXXX )
 99              cp $CLEARTEXT_WALLET_FILENAME $LAST_RESORT_FILENAME
100              chmod 600 $LAST_RESORT_FILENAME
101              echo "gpg failed to enrypt your password wallet: I have"
102              echo "tried to put a CLEARTEXT copy of your wallet at"
103              echo $LAST_RESORT_FILENAME
104              exit 16
105          fi
106          exit 0;;
107      ?)
108          echo "error condition detected (invalid password?)"
109          exit 32;;
110  esac

It's pretty easy to install; simply save the script somewhere in your $PATH and make it executable. Then, you just need to tell it where your encrypted password file should be. There are three ways to do this:

  1. Set the $WALLET_FILENAME environment variable.

  2. Set $WALLET_FILENAME in ~/.walletrc.

  3. Specify the filename with the -c command-line option.

The second method (which overrides the first method) is my preference—I have the following line in ~/.walletrc:

WALLET_FILENAME=~/docs/wallet.gpg

But, if I needed to use a different wallet file, I could override either of the first two methods with the command-line option by calling the script like this:

wallet -c ~/docs/other_wallet.gpg

wallet defaults to its read-only mode, in which it displays the decrypted version of your wallet file using less. But, if you include the -e command-line option (edit mode), the script decrypts your wallet file to a temporary location and opens it in a text editor (the script defaults to using vi, but you can set the $VISUAL variable in the environment or in your ~/.walletrc file). When you close the editor, wallet encrypts the file and saves it to the original location.

The first time you run wallet, you won't have a wallet file, so wallet creates it for you and runs in edit mode.

How It Works

Let's dig in to the script to see how it works. The first thing it does is use the dot operator to source a file called functions, which appears as shown in Listing 2. Having wallet source an external file (with the dot operator) is essentially equivalent to inserting the contents of the sourced file (~/bin/functions) at line 3 of wallet. Doing it this way allows other scripts to use the same code (a code library for shell scripts).

Listing 2. functions File


is_installed() {

    PROGRAM=$1

    PATHNAME=$( type $PROGRAM 2> /dev/null )
    if [ -z "$PATHNAME" ]; then
        echo "cannot locate $PROGRAM in path"
        exit 1
    fi

}

The functions file includes a function called is_installed, which uses the bash built-in type to see whether a program is installed. If is_installed doesn't find the program in your $PATH, is_installed prints an error message and calls exit, which terminates wallet. So, if you run wallet and it quits with an error like “cannot locate dialog in path”, you probably haven't installed the dialog package. Use your distribution's package management system (yum, apt-get, whatever) to install dialog and try again.

Input Validation

Lines 18 through 28 of the wallet script parse the command-line arguments using the getopts bash built-in. The while loop loops through the options specified by the string ec:. This means that wallet can accept the -e and -c options, and that the -c option requires an argument. As the while loop moves through the command-line arguments, the current option is assigned to the variable $OPTION, and any argument to the current option is assigned to the variable $OPTARG. Any unrecognized option results in an error message, and wallet exits. After the while loop completes, it's important to reset the $OPTIND variable (this is necessary after any getopts call).

Running wallet the First Time

Lines 37 through 45 of the wallet script verify that the encrypted file exists, and create the file if it doesn't exist already. The -f test checks to see whether $WALLET_FILENAME exists as a normal file. If not, the test fails, and wallet assumes you are running wallet for the first time and that wallet needs to set up the working environment. wallet uses the command substitution syntax for creating the directory in which the encrypted file should exist (line 40):

mkdir -p $( dirname $WALLET_FILENAME )

The command inside the $(...) runs first, and the result becomes the argument to mkdir. The dirname command returns the encrypted file's directory, and mkdir -p creates that directory (and any necessary parent directories).

Next, wallet needs to create the encrypted file (even though the unencrypted version will be empty). Line 41 uses mktemp to create an empty file in /tmp whose name ends in six randomly chosen characters. mktemp prints the name of the file it creates, so running this in a command substitution shell and assigning the result to $TEMPFILE puts the name of the temporary file in $TEMPFILE.

Now we see the first use of gpg. Line 42 uses gpg to encrypt the (empty) temporary file ($TEMPFILE) via symmetric encryption (gpg's -c option) and to write the encrypted file to $WALLET_FILENAME. wallet then deletes the temporary file. Because this is the first time wallet has run, it assumes that edit mode is appropriate and sets the $EDIT_PWFILE flag.

Prompting the User for the Master Password

Line 52 uses the command substitution trick again, this time to prompt the user for the master password (used to encrypt the wallet file). The dialog man page describes the many ways that scripts using dialog can retrieve input from the user. This example uses dialog to create a simple password box. The --stdout option tells dialog to print the user's input (the master password) to standard output, so that it may be assigned to $PASSWORD.

Line 55 inspects the bash variable $?, which contains the exit code of the previous process (dialog, in this case). The convention is that an exit code of 0 indicates success (and wallet follows this convention in its own exit calls). If $? differs from 0 on line 55, this indicates that dialog encountered an error, and wallet terminates with an error message.

Read-Only Mode

If $EDIT_PWFILE is 0 (line 65), then wallet is running in read-only mode:

echo $PASSWORD | gpg --decrypt --passphrase-fd 0 
 ↪$WALLET_FILENAME | less

This tells gpg to decrypt $WALLET_FILENAME and to read the password from standard input (fd 0). Piping $PASSWORD into gpg enables gpg to decrypt the wallet file without interactively asking the user for the master password. The output (the decrypted wallet file) is printed to standard output, which is piped into less, allowing the user to page through the passwords, run searches and so on. When the user closes less, wallet clears the screen and exits.

The rest of the script assumes that $EDIT_PWFILE is nonzero (that wallet is running in edit mode).

Edit Mode

In edit mode, wallet needs to decrypt the wallet file, open the decrypted file in a text editor, and then encrypt the edited file back to the original location. Line 74 uses mktemp to create a temporary directory, into which the wallet file will be decrypted. Line 75 sets $CLEARTEXT_WALLET_FILENAME to be the name of a file inside the temporary directory.

Line 79 runs trap, a bash built-in. The first argument to trap is a command, and this is followed by a list of signals (for example, if someone runs kill on wallet). If wallet receives any of these signals after line 79, wallet will run the trapped command (deleting the decrypted wallet file) prior to exiting. This is an attempt to ensure that the decrypted file isn't left sitting around if wallet terminates unexpectedly.

Line 83 is like what we saw in read-only mode, with the addition of the -o option to gpg. This instructs gpg to write the decrypted file to $CLEARTEXT_WALLET_FILENAME.

If gpg's exit code was 0, wallet renames the encrypted wallet file with a .bak extension (thus preserving a copy, in case something goes wrong) and opens the decrypted file in the text editor $VISUAL. After the editor exits, wallet tells gpg to encrypt the edited plain-text file at $CLEARTEXT_WALLET_FILENAME and to write the encrypted wallet file back to $WALLET_FILENAME. A nonzero exit status from this gpg call means that something went wrong in re-encrypting the wallet file, so wallet makes a copy of the plain-text file in your home directory and prints an error message.

Listing 3. Password Generator Script


#!/bin/bash

. ~/bin/functions
is_installed openssl

DIGEST="sha1"
RULER=0
DASH_N=""
while getopts 'mrn' OPTION
do
  case $OPTION in
    m) DIGEST="md5";;
    r) RULER=1;;
    n) DASH_N="-n";;
    ?) printf "usage: %s [ -m ] [ -r ]\n" $( basename $0 ) >&2
       exit 2
       ;;
  esac
done
shift $(($OPTIND - 1))

if [ ! -z $DASH_N ]; then
    RULER=0
fi

DD=$( dd if=/dev/urandom bs=1k count=1 2> /dev/null \
    | openssl dgst -$DIGEST )
echo $DASH_N $DD
if [ $RULER -eq 1 ]; then
    echo '   5|  10|  15|  20|  25|  30|  35|  40|'
fi

Password Generator

Listing 3 shows a short shell script that generates very random, impossible-to-remember passwords—perfect for storing in your wallet. mkpass dumps a kilobyte of random data into a digest algorithm to produce an ASCII password. By default, mkpass uses the SHA1 digest algorithm, but it can use MD5 if you supply mkpass's -m command-line option. And, if you give the -r option, mkpass prints a ruler below the password (useful if you need or want a password of a particular length).

If you're a vim user, try adding the following line to your ~/.vimrc file:


map \mkpass i <CR><ESC>k$:r!~/bin/mkpass -n<CR>kJJ

Now when you're running vim (like when you're using wallet in edit mode), typing \mkpass in command mode will insert a password at the cursor location.

Conclusion

wallet is a bash script for managing a password wallet. It's written to be usable over a text-only interface. Hopefully, this description of the code has helped you add an item or two to your bag of scripting tricks.

Resources

“How to create a command-line password vault” by Duane Odom: www.linux.com/feature/114238

DynDNS Dynamic DNS: www.dyndns.org

inadyn Dynamic DNS Updater: inadyn.ina-tech.net

putty SSH Client: www.chiark.greenend.org.uk/~sgtatham/putty

Carl Welch is a Web developer and Linux system administrator. He enjoys science fiction, is ambivalent to dentists and dislikes standard light switches. He maintains the lamest blog on planet Earth at mbrisby.blogspot.com.

Load Disqus comments