Hacking a Safe with Bash

Through the years, I have settled on maintaining my sensitive data in plain-text files that I then encrypt asymmetrically. Although I take care to harden my system and encrypt partitions with LUKS wherever possible, I want to secure my most important data using higher-level tools, thereby lessening dependence on the underlying system configuration. Many powerful tools and utilities exist in this space, but some introduce unacceptable levels of "bloat" in one way or another. Being a minimalist, I have little interest in dealing with GUI applications that slow down my work flow or application-specific solutions (such as browser password vaults) that are applicable only toward a subset of my sensitive data. Working with text files affords greater flexibility over how my data is structured and provides the ability to leverage standard tools I can expect to find most anywhere.

Asymmetric Encryption

Asymmetric encryption, or public-key cryptography, relies on the use of two keys: one of which is held private, while the other is published freely. This model offers greater security over the symmetric approach, which is based on a single key that must be shared between the sender and receiver. GnuPG is a free software implementation of the OpenPGP standard as defined by RFC4880. GnuPG supports both asymmetric and symmetric algorithms. Refer to https://gnupg.org for additional information.


This article makes extensive use of GPG to interact with files stored in your safe. Many tutorials and HOWTOs exist that will walk you through how to set up and manage your keys properly. It is highly recommended to configure gpg-agent in order to avoid having to type your passphrase each time you interact with your private key. One popular approach used for this job is Keychain, because it also is capable of managing ssh-agent.

Let's take the classic example of managing credentials. This is a necessary evil and while both pass and KeePassC look interesting, I am not yet convinced they would fit into my work flow. Also, I am definitely not lulled by any "copy to clipboard" feature. You've all seen the inevitable clipboard spills on IRC and such—no thanks! For the time being, let's fold this job into a "safe" concept by managing this data in a file. Each line in the file will conform to a simple format of:


Where "resource" is something mnemonic, such as an FQDN or even a hardware device like a router that is limited to providing telnet access. Both userid and password fields are represented as hints. This hinting approach works nicely given my conscious effort to limit the number of user IDs and passwords I routinely use. This means a hint is all that is needed for muscle memory to kick in. If a particular resource uses some exotic complexity rules, I quickly can understand the slight variation by modifying the hint accordingly. For example, a hint of "fo" might end up as "!fo" or "fO". Another example of achieving this balance between security and usability comes up when you need to use an especially long password. One practical solution would be to combine familiar passwords and document the hint accordingly. For example, a hint representing a combination of "fo" and "ba" could be represented as "fo..ba". Finally, the hinting approach provides reasonable fall-back protection since the limited information would be of little use to an intruder.

Despite the obscurity, leaving this data in the clear would be silly and irresponsible. Having GnuPG configured provides an opportunity to encrypt the data using your private key. After creating the file, my work flow was looking something like this:

$ gpg --ear <my key id> <file>
$ shred -u <file>

Updating the file would involve decrypting, editing and repeating the steps above. This was tolerable for a while since, practically speaking, I'm not establishing credentials on a daily basis. However, I knew the day would eventually come when the tedious routine would become too much of a burden. As expected, that day came when I found myself keeping insurance-related notes that I then considered encrypting using the same technique. Now, I am talking about managing multiple files—a clear sign that it is time to write a script to act as a wrapper. My requirements were simple:

  1. Leverage common tools, such as GPG, shred and bash built-ins.

  2. Reduce typing for common operations (encrypt, decrypt and so on).

  3. Keep things clean and readable in order to accommodate future growth.

  4. Accommodate plain-text files but avoid having to micro-manage them.

Interestingly, the vim-gnupg Vim plugin easily can handle these requirements, because it integrates seamlessly with files ending in .asc, .gpg or .pgp extensions. Despite its abilities, I wanted to avoid having to manage multiple encrypted files and instead work with a higher-level "vault" of sorts. With that goal in mind, the initial scaffolding was cobbled together:


[ -f $CONF ] && . $CONF
[ -z "$SOURCE_DIR" ] && SOURCE_DIR=${HOME}/safe
TAR="tar -C $(dirname $SOURCE_DIR)"

usage() {
cat <

This framework is simple enough to build from and establishes some ground rules. For starters, you're going to avoid micro-managing files by maintaining them in a single tar archive. The $SOURCE_DIR variable will fall back to $HOME/safe unless it is defined in ~/.saferc. Thinking ahead, this will allow people to collaborate on this project without clobbering the variable over and over. Either way, the value of $SOURCE_DIR is used as a base for the $SOURCE_BASE, $TAR_ENC and $TAR variables. If my ~/.saferc were to define $SOURCE_DIR as $HOME/foo, my safe will be maintained as $HOME/foo.tar.gz.asc. If I choose not to maintain a ~/.saferc file, my safe will reside in $HOME/safe.tar.gz.asc.

Back to this primitive script, let's limit the focus simply to being able to open and close the safe. Let's work on the create_safe() function first so you have something to extract later:

create_safe() {
  [ -d $SOURCE_DIR ] || { "Missing directory: $SOURCE_DIR"; exit 1; }
  $TAR -cz $SOURCE_BASE | gpg -ear $(whoami) --yes -o $TAR_ENC
  find $SOURCE_DIR -type f | xargs shred -u
  rm -fr $SOURCE_DIR

The create_safe() function is looking pretty good at this point, since it automates a number of tedious steps. First, you ensure that the archive's base directory exists. If so, you compress the directory into a tar archive and pipe the output straight into GPG in order to encrypt the end result. Notice how the result of whoami is used for GPG's -r option. This assumes the private GPG key can be referenced using the same ID that is logged in to the system. This is strictly a convenience, as I have taken care to keep these elements in sync, but it will need to be modified if your setup is different. In fact, I could see eventually supporting an override of sorts with the ~/.saferc approach. For now though, let's put that idea on the back burner. Finally, the function calls the shred binary on all files within the base directory. This solves the annoying "Do I have a plain-text version laying around?" dilemma by automating the cleanup.

Now you should be able to create the safe. Assuming no ~/.saferc exists and the $PATH environment variable contains the directory containing safe.sh, you can begin to test this script:

$ cd
$ mkdir safe
$ for i in $(seq 5); do echo "this is secret #$i" > 
 ↪safe/file${i}.txt; done
$ safe.sh -c

You now should have a file named safe.tar.gz.asc in your home directory. This is an encrypted tarball containing the five files previously written to the ~/safe directory. You then cleaned things up by shredding each file and finally removing the ~/safe directory. This is probably a good time to recognize you are basing the design around an expectation to manage a single directory of files. For my purposes, this is acceptable. If subdirectories are needed, the code would need to be refactored accordingly.

Now that you are able to create your safe, let's focus on being able to open it. The following extract_safe() function will do the trick nicely:

extract_safe() {
  [ -f $TAR_ENC ] || { "Missing file: $TAR_ENC"; exit 1; }
  gpg --batch -q -d $TAR_ENC | $TAR -zx

Essentially, you are just using GPG and tar in the opposite order. After opening the safe by running the script with -x, you should see the ~/safe directory.

Things seem to be moving along, but you easily can see the need to list the contents of your safe, because you do not want to have to open it each time in order to know what is inside. Let's add a list_safe() function:

list_safe() {
  [ -f $TAR_ENC ] || { "Missing file: $TAR_ENC"; exit 1; }
  gpg --batch -q -d $TAR_ENC | tar -zt

No big deal there, as you are just using tar's ability to list contents rather than extract them. While you are here, you can start DRYing this up a bit by consolidating all the file and directory tests into a single function. You even can add a handy little backup feature to scp your archive to a remote host. Listing 1 is an updated version of the script up to this point.

Listing 1. safe.sh

# safe.sh - wrapper to interact with my encrypted file archive

[ -f $CONF ] && . $CONF
[ -z "$SOURCE_DIR" ] && SOURCE_DIR=${HOME}/safe
TAR="tar -C $(dirname $SOURCE_DIR)"

usage() {
cat < /dev/null
  [ $? -eq 0 ] && echo OK || echo Failed

The new -b option requires a hostname passed as an argument. When used, the archive will be scp'd accordingly. As a bonus, you can use the -b option multiple times in order to back up to multiple hosts. This means you have the option to configure a routine cron job to automate your backups while still being able to run a "one off" at any point. Of course, you will want to manage your SSH keys and configure ssh-agent if you plan to automate your backups. Recently, I have converted over to pam_ssh () in order to fire up my ssh-agent, but that's a different discussion.

Back to the code, there is a small is_or_die() function that accepts an argument but falls back to the archive specified in $TAR_ENC. This will help keep the script lean and mean since, depending on the option(s) used, you know you are going to want to check for one or more files and/or directories before taking action.

For the remainder of this article, I'm going to avoid writing out the updated script in its entirety. Instead, I simply provide small snippets as new functionality is added.

For starters, how about adding the ability to output the contents of a single file being stored in your safe? All you would need to do is check for the file's presence and modify your tar options appropriately. In fact, you have an opportunity to avoid re-inventing the wheel by simply refactoring your extract_safe() function. The updated function will operate on a single file if called accordingly. Otherwise, it will operate on the entire archive. Worth noting is the extra step to provide a bit of user-friendliness. Using the default $SOURCE_DIR of ~/safe, the user can pass either safe/my_file or just my_file to the -o option:

list_safe() {
  gpg --batch -q -d $TAR_ENC | tar -zt | sort

search_safe() {
  for f in $(list_safe); do
    [ "$ARCHIVE_FILE" == "$FILE" ] && return

extract_safe() {
  OPTS=" -zx"
  [ $# -eq 1 ] && OPTS+=" $SOURCE_BASE/${1#*/} -O"
  gpg --batch -q -d $TAR_ENC | $TAR $OPTS

The final version of safe.sh is maintained at https://github.com/windowsrefund/safe. It supports a few more use cases, such as the ability to add and remove files. When adding these features, I tried to avoid actually having to extract the archive to disk as a precursor to modifying its contents. I was unsuccessful due to GNU tar's refusal to read from STDIN when -r is used. A nice alternative to connecting GPG with tar via pipes might exist in GnuPG's gpg-zip binary. However, the Arch package maintainer appears to have included only the gpg-zip man page. In short, I prefer the "keep things as simple as possible; but no simpler" approach. If anyone is interested in improving the methods used to add and remove files, feel free to submit your pull requests. This also applies to the edit_safe() function, although I foresee refactoring that at some point given some recent activity with the vim-gnupg plugin.

Integrating with Mutt

My MUA of choice is mutt. Like many people, I have configured my mail client to interact with multiple IMAP accounts, each requiring authentication. In general, these credentials simply could be hard-coded in one or more configuration files but that would lead to shame, regret and terrible things. Instead, let's use a slight variation of Aaron Toponce's clever approach that empowers mutt with the ability to decrypt and source sensitive data:

$ echo "set my_pass_imap = l@mepassw0rd" > /tmp/pass_mail
$ safe.sh -a /tmp/pass_mail

Now that your safe contains the pass_mail file; you have mutt read it with this line in your ~/.muttrc:

source "safe.sh -o pass_mail |"

By reading the file, mutt initializes a variable you have named my_pass_imap. That variable can be used in other areas of mutt's configuration. For example, another area of your mutt configuration can use these lines:

set imap_user = "my_user_id"
set imap_pass = $my_pass_imap
set folder = "imaps://example.com"
set smtp_url = smtp://$imap_user:$imap_pass@example.com

By combining appropriately named variables with mutt's ability to support multiple accounts, it is possible to use this technique to manage all of your mail-related credentials securely while never needing to store plain-text copies on your hard drive.

Load Disqus comments

Subscribe now


Corporate Patron

Pulseway Logo

Limited Time Offer

September Cover


Take Linux Journal for a test drive. Download our September issue for FREE.