GPG-Based Password Wallet
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:
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.
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).
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:
Set the $WALLET_FILENAME environment variable.
Set $WALLET_FILENAME in ~/.walletrc.
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.
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.
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).
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.
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.
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).
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.
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.