GPG-Based Password Wallet

Carl Welch

Issue #165, January 2008

Keep your passwords safe in an encrypted file.

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).

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.

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).

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.

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.

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.